diff --git a/.github/workflows/cd-macmini.yml b/.github/workflows/cd-macmini.yml index e579d6a58..eb27f52f9 100644 --- a/.github/workflows/cd-macmini.yml +++ b/.github/workflows/cd-macmini.yml @@ -48,6 +48,8 @@ env: PROJECT_DIR: /Users/mana/projects/manacore-monorepo COMPOSE_FILE: docker-compose.macmini.yml ENV_FILE: .env.macmini + DEPLOY_NOTIFY_ROOM_ID: ${{ secrets.DEPLOY_NOTIFY_ROOM_ID }} + DEPLOY_NOTIFY_BOT_TOKEN: ${{ secrets.DEPLOY_NOTIFY_BOT_TOKEN }} PATH: /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin jobs: @@ -411,6 +413,33 @@ jobs: push_deploy_metrics "$STATUS" "$DURATION" "$BRANCH" 2>/dev/null || true echo "Deploy tracking recorded: status=$STATUS duration=${DURATION}s" + - name: Notify on failure + if: failure() + run: | + cd "${{ env.PROJECT_DIR }}" + SERVICES="${{ steps.services.outputs.services }}" + [ "${{ steps.services.outputs.deploy-all }}" == "true" ] && SERVICES="all" + COMMIT_MSG=$(git log -1 --pretty=%s 2>/dev/null | head -c 100) + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + MSG="⚠️ **Deploy failed**\n\n**Services:** ${SERVICES}\n**Commit:** ${COMMIT_MSG}\n**By:** ${{ github.actor }}\n**[View logs](${RUN_URL})**" + + # Send to Matrix deploy-notifications room via Synapse API + ROOM_ID="${DEPLOY_NOTIFY_ROOM_ID:-}" + BOT_TOKEN="${DEPLOY_NOTIFY_BOT_TOKEN:-}" + if [ -n "$ROOM_ID" ] && [ -n "$BOT_TOKEN" ]; then + TXN_ID="deploy-$(date +%s)" + curl -s -X PUT \ + "http://localhost:8008/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/${TXN_ID}" \ + -H "Authorization: Bearer ${BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"msgtype\":\"m.text\",\"body\":\"Deploy failed: ${SERVICES}\",\"format\":\"org.matrix.custom.html\",\"formatted_body\":\"$(echo -e "$MSG" | sed 's/"/\\"/g')\"}" \ + || true + echo "Matrix notification sent" + else + echo "Matrix notification skipped (DEPLOY_NOTIFY_ROOM_ID or DEPLOY_NOTIFY_BOT_TOKEN not set)" + fi + - name: Cleanup old images if: always() run: | diff --git a/games/whopixels/.gitignore b/games/whopixels/.gitignore new file mode 100644 index 000000000..4f6d6258f --- /dev/null +++ b/games/whopixels/.gitignore @@ -0,0 +1,27 @@ +# Umgebungsvariablen +.env + +# Node.js +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log +package-lock.json + +# Logs +logs +*.log + +# Betriebssystem-Dateien +.DS_Store +Thumbs.db + +# IDE und Editor Dateien +.idea/ +.vscode/ +*.swp +*.swo + +# Build-Verzeichnisse +dist/ +build/ diff --git a/games/whopixels/README.md b/games/whopixels/README.md new file mode 100644 index 000000000..10f8c1648 --- /dev/null +++ b/games/whopixels/README.md @@ -0,0 +1,74 @@ +# WhoPixels + +Ein webbasiertes Pixel-Spiel, entwickelt mit Phaser.js. + +Projekt Starten: +node server.js + +## Über das Projekt + +WhoPixels ist ein einfaches Pixel-Art-Editor-Spiel, in dem du deine eigenen Pixel-Kunstwerke erstellen kannst. Das Projekt verwendet Phaser.js, eine leistungsstarke HTML5-Spieleentwicklungsbibliothek. + +## Funktionen + +- Interaktives Pixel-Art-Editor-Interface +- Farbpalette mit 8 Grundfarben +- Einfache und intuitive Benutzeroberfläche +- Responsive Design + +## Erste Schritte + +Um das Spiel lokal zu starten, benötigst du einen lokalen Webserver. Du kannst einen einfachen Server mit Python oder Node.js starten. + +### Mit Python: + +```bash +# Python 3 +python -m http.server + +# Python 2 +python -m SimpleHTTPServer +``` + +### Mit Node.js: + +Installiere zuerst das `http-server`-Paket: + +```bash +npm install -g http-server +``` + +Dann starte den Server: + +```bash +http-server +``` + +## Projektstruktur + +``` +whopixels/ +├── assets/ # Spielressourcen (Bilder, Sounds, etc.) +├── css/ # CSS-Stylesheets +├── js/ # JavaScript-Dateien +│ ├── scenes/ # Phaser-Szenen +│ │ ├── BootScene.js +│ │ ├── MainMenuScene.js +│ │ └── GameScene.js +│ └── main.js # Hauptspieldatei +└── index.html # Haupt-HTML-Datei +``` + +## Weiterentwicklung + +Hier sind einige Ideen für zukünftige Erweiterungen: + +- Speichern und Laden von Pixel-Art +- Mehr Werkzeuge (Pinsel, Radierer, Füllen, etc.) +- Animation-Editor +- Teilen von Kunstwerken +- Mehrere Ebenen für komplexere Designs + +## Lizenz + +Dieses Projekt ist Open Source und steht unter der MIT-Lizenz. diff --git a/games/whopixels/assets/background.html b/games/whopixels/assets/background.html new file mode 100644 index 000000000..4a058694c --- /dev/null +++ b/games/whopixels/assets/background.html @@ -0,0 +1,35 @@ + + + + Background Image + + + + + + + diff --git a/games/whopixels/assets/background.png b/games/whopixels/assets/background.png new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/games/whopixels/assets/background.png @@ -0,0 +1 @@ + diff --git a/games/whopixels/assets/create_placeholder_images.html b/games/whopixels/assets/create_placeholder_images.html new file mode 100644 index 000000000..b5246dc0f --- /dev/null +++ b/games/whopixels/assets/create_placeholder_images.html @@ -0,0 +1,68 @@ + + + + Create Placeholder Images + + +

Creating placeholder images...

+ + + + + + + + + diff --git a/games/whopixels/assets/player.html b/games/whopixels/assets/player.html new file mode 100644 index 000000000..503243201 --- /dev/null +++ b/games/whopixels/assets/player.html @@ -0,0 +1,26 @@ + + + + Player Image + + + + + + + diff --git a/games/whopixels/assets/player.png b/games/whopixels/assets/player.png new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/games/whopixels/assets/player.png @@ -0,0 +1 @@ + diff --git a/games/whopixels/assets/simple_images.js b/games/whopixels/assets/simple_images.js new file mode 100644 index 000000000..3f0dd2330 --- /dev/null +++ b/games/whopixels/assets/simple_images.js @@ -0,0 +1,54 @@ +// Simple script to create basic placeholder images +// Just open this in a browser and it will create data URLs you can copy + +document.body.innerHTML = ` +

Placeholder Images for WhoPixels

+
+

Background (800x600)

+ +

+
+
+

Player (32x32)

+ +

+
+
+

Tile (32x32)

+ +

+
+`; + +// Draw background +const bgCanvas = document.getElementById('bg'); +const bgCtx = bgCanvas.getContext('2d'); +bgCtx.fillStyle = '#222233'; +bgCtx.fillRect(0, 0, 800, 600); +for (let i = 0; i < 100; i++) { + bgCtx.fillStyle = '#1a1a2a'; + const x = Math.random() * 800; + const y = Math.random() * 600; + const size = Math.random() * 5 + 2; + bgCtx.fillRect(x, y, size, size); +} +document.getElementById('bgData').textContent = 'Save this image as background.png'; + +// Draw player +const playerCanvas = document.getElementById('player'); +const playerCtx = playerCanvas.getContext('2d'); +playerCtx.fillStyle = '#ff0000'; +playerCtx.fillRect(0, 0, 32, 32); +playerCtx.fillStyle = '#ff5555'; +playerCtx.fillRect(8, 8, 16, 16); +document.getElementById('playerData').textContent = 'Save this image as player.png'; + +// Draw tile +const tileCanvas = document.getElementById('tile'); +const tileCtx = tileCanvas.getContext('2d'); +tileCtx.fillStyle = '#ffffff'; +tileCtx.fillRect(0, 0, 32, 32); +tileCtx.strokeStyle = '#cccccc'; +tileCtx.lineWidth = 1; +tileCtx.strokeRect(0.5, 0.5, 31, 31); +document.getElementById('tileData').textContent = 'Save this image as tile.png'; diff --git a/games/whopixels/assets/tile.html b/games/whopixels/assets/tile.html new file mode 100644 index 000000000..27a846070 --- /dev/null +++ b/games/whopixels/assets/tile.html @@ -0,0 +1,27 @@ + + + + Tile Image + + + + + + + diff --git a/games/whopixels/assets/tile.png b/games/whopixels/assets/tile.png new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/games/whopixels/assets/tile.png @@ -0,0 +1 @@ + diff --git a/games/whopixels/css/style.css b/games/whopixels/css/style.css new file mode 100644 index 000000000..94e37d07b --- /dev/null +++ b/games/whopixels/css/style.css @@ -0,0 +1,14 @@ +body { + margin: 0; + padding: 0; + background-color: #000; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-family: 'Arial', sans-serif; +} + +#game-container { + box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); +} diff --git a/games/whopixels/data/npc_characters.js b/games/whopixels/data/npc_characters.js new file mode 100644 index 000000000..a55e82079 --- /dev/null +++ b/games/whopixels/data/npc_characters.js @@ -0,0 +1,83 @@ +// Liste der NPC-Charaktere mit Namen und Persönlichkeiten +// NPC-Charaktere: Berühmte Erfinder durch die Historie +const npcCharacters = [ + { + id: 1, + name: 'Leonardo da Vinci', + personality: + 'Ein vielseitiger Universalgelehrter der Renaissance, bekannt für seine Kunst und Erfindungen. Er spricht nachdenklich und philosophisch, oft mit Metaphern über Natur und Kunst. Er ist neugierig und beobachtet alles genau.', + hint: 'Meine Skizzenbücher enthalten Flugmaschinen und anatomische Studien, die ihrer Zeit weit voraus waren.', + }, + { + id: 2, + name: 'Nikola Tesla', + personality: + 'Ein exzentrischer Elektroingenieur mit visionären Ideen. Er spricht leidenschaftlich über Elektrizität und drahtlose Energieübertragung. Er ist brillant, aber auch etwas eigenartig und distanziert.', + hint: 'Meine Arbeiten mit Wechselstrom revolutionierten die Art, wie wir Energie nutzen.', + }, + { + id: 3, + name: 'Marie Curie', + personality: + 'Eine entschlossene und präzise Wissenschaftlerin, die für ihre Entdeckungen im Bereich der Radioaktivität bekannt ist. Sie spricht klar und methodisch, mit einem starken Fokus auf wissenschaftliche Genauigkeit.', + hint: 'Meine Forschung zu radioaktiven Elementen brachte mir zwei Nobelpreise ein, obwohl sie letztendlich meine Gesundheit beeinträchtigte.', + }, + { + id: 4, + name: 'Thomas Edison', + personality: + 'Ein pragmatischer und geschäftstüchtiger Erfinder mit über 1.000 Patenten. Er spricht direkt und selbstbewusst, oft mit praktischen Beispielen. Er betont harte Arbeit und Ausdauer über Inspiration.', + hint: 'Meine Erfindung brachte Licht in die Dunkelheit und veränderte die Art, wie Menschen nach Sonnenuntergang leben.', + }, + { + id: 5, + name: 'Ada Lovelace', + personality: + 'Eine visionäre Mathematikerin des 19. Jahrhunderts mit einer einzigartigen Verbindung von Logik und Kreativität. Sie spricht eloquent und präzise, mit einer Mischung aus poetischer und mathematischer Sprache.', + hint: 'Ich schrieb den ersten Algorithmus für eine Maschine, lange bevor Computer existierten.', + }, + { + id: 6, + name: 'Archimedes', + personality: + 'Ein genialer antiker Mathematiker und Erfinder aus Syrakus. Er ist von mathematischen Problemen fasziniert und kann sich darin verlieren. Er spricht mit Begeisterung über Geometrie und physikalische Prinzipien.', + hint: "Mein berühmtester Ausruf war 'Heureka!' als ich das Prinzip des Auftriebs in der Badewanne entdeckte.", + }, + { + id: 7, + name: 'Johannes Gutenberg', + personality: + 'Ein geduldiger und präziser Handwerker, der die Druckkunst revolutionierte. Er spricht bescheiden über seine Erfindung, aber mit Stolz über deren Auswirkungen auf die Verbreitung von Wissen.', + hint: 'Meine Erfindung machte Bücher für die Massen zugänglich und veränderte die Verbreitung von Wissen für immer.', + }, + { + id: 8, + name: 'Grace Hopper', + personality: + 'Eine pragmatische und humorvolle Computerpionierin und Marineoffizierin. Sie erklärt komplexe Konzepte mit einfachen Analogien und hat einen trockenen Humor. Sie ist direkt und lösungsorientiert.', + hint: "Ich entwickelte den ersten Compiler und fand einmal einen echten 'Bug' im Computer - eine Motte, die einen Fehler verursachte.", + }, + { + id: 9, + name: 'Alexander Graham Bell', + personality: + 'Ein einfallsreicher und geduldiger Erfinder, der sich für Kommunikation und Gehörlose engagierte. Er spricht deutlich und artikuliert, mit einem schottischen Akzent. Er ist enthusiastisch, wenn er über seine Erfindungen spricht.', + hint: 'Meine Erfindung ermöglichte es Menschen, über große Entfernungen miteinander zu sprechen.', + }, + { + id: 10, + name: 'Hedy Lamarr', + personality: + 'Eine glamouröse Hollywoodschauspielerin mit einem brillanten technischen Verstand. Sie spricht charmant und selbstbewusst, mit einer Mischung aus Eleganz und technischem Scharfsinn. Sie ist kreativ und unkonventionell.', + hint: 'Meine Erfindung der Frequenzsprungverfahren bildet die Grundlage für moderne WLAN- und Bluetooth-Technologien, obwohl viele mich nur als Filmstar kennen.', + }, +]; + +// Mache die Charaktere sowohl im Browser als auch in Node.js verfügbar +if (typeof window !== 'undefined') { + // Browser-Umgebung + window.npcCharacters = npcCharacters; +} else if (typeof module !== 'undefined') { + // Node.js-Umgebung + module.exports = npcCharacters; +} diff --git a/games/whopixels/generate_assets.js b/games/whopixels/generate_assets.js new file mode 100644 index 000000000..3772fdb30 --- /dev/null +++ b/games/whopixels/generate_assets.js @@ -0,0 +1,47 @@ +// This script uses Node.js to generate placeholder images for our game +const fs = require('fs'); +const { createCanvas } = require('canvas'); + +// Create background image (800x600) +const bgCanvas = createCanvas(800, 600); +const bgCtx = bgCanvas.getContext('2d'); +bgCtx.fillStyle = '#222233'; +bgCtx.fillRect(0, 0, 800, 600); + +// Add some pattern to background +bgCtx.fillStyle = '#1a1a2a'; +for (let i = 0; i < 100; i++) { + const x = Math.random() * 800; + const y = Math.random() * 600; + const size = Math.random() * 5 + 2; + bgCtx.fillRect(x, y, size, size); +} + +// Create player image (32x32) +const playerCanvas = createCanvas(32, 32); +const playerCtx = playerCanvas.getContext('2d'); +playerCtx.fillStyle = '#ff0000'; +playerCtx.fillRect(0, 0, 32, 32); +playerCtx.fillStyle = '#ff5555'; +playerCtx.fillRect(8, 8, 16, 16); + +// Create tile image (32x32) +const tileCanvas = createCanvas(32, 32); +const tileCtx = tileCanvas.getContext('2d'); +tileCtx.fillStyle = '#ffffff'; +tileCtx.fillRect(0, 0, 32, 32); +tileCtx.strokeStyle = '#cccccc'; +tileCtx.lineWidth = 1; +tileCtx.strokeRect(0.5, 0.5, 31, 31); + +// Save images +const bgBuffer = bgCanvas.toBuffer('image/png'); +fs.writeFileSync('./assets/background.png', bgBuffer); + +const playerBuffer = playerCanvas.toBuffer('image/png'); +fs.writeFileSync('./assets/player.png', playerBuffer); + +const tileBuffer = tileCanvas.toBuffer('image/png'); +fs.writeFileSync('./assets/tile.png', tileBuffer); + +console.log('All placeholder images have been generated!'); diff --git a/games/whopixels/index.html b/games/whopixels/index.html new file mode 100644 index 000000000..0cdc70323 --- /dev/null +++ b/games/whopixels/index.html @@ -0,0 +1,25 @@ + + + + + + WhoPixels - Pixel Game + + + +
+ + + + + + + + + + + + + + + diff --git a/games/whopixels/js/main.js b/games/whopixels/js/main.js new file mode 100644 index 000000000..abe936cde --- /dev/null +++ b/games/whopixels/js/main.js @@ -0,0 +1,18 @@ +// Game configuration +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + pixelArt: true, + physics: { + default: 'arcade', + arcade: { + gravity: { y: 0 }, + debug: false, + }, + }, + scene: [BootScene, MainMenuScene, GameScene, RPGScene], +}; + +// Create and start the game +const game = new Phaser.Game(config); diff --git a/games/whopixels/js/scenes/BootScene.js b/games/whopixels/js/scenes/BootScene.js new file mode 100644 index 000000000..e984fe27e --- /dev/null +++ b/games/whopixels/js/scenes/BootScene.js @@ -0,0 +1,445 @@ +class BootScene extends Phaser.Scene { + constructor() { + super({ key: 'BootScene' }); + } + + preload() { + // Loading screen + this.graphics = this.add.graphics(); + this.newGraphics = this.add.graphics(); + const progressBar = new Phaser.Geom.Rectangle(200, 300, 400, 50); + const progressBarFill = new Phaser.Geom.Rectangle(205, 305, 290, 40); + + this.graphics.fillStyle(0xffffff, 1); + this.graphics.fillRectShape(progressBar); + + this.newGraphics.fillStyle(0x3587e2, 1); + this.newGraphics.fillRectShape(progressBarFill); + + const loadingText = this.add.text(250, 260, 'Loading: ', { fontSize: '32px', fill: '#FFF' }); + + // Update as load progresses + this.load.on('progress', (percent) => { + loadingText.setText('Loading: ' + parseInt(percent * 100) + '%'); + progressBarFill.width = 390 * percent; + this.newGraphics.clear(); + this.newGraphics.fillStyle(0x3587e2, 1); + this.newGraphics.fillRectShape(progressBarFill); + }); + + this.load.on('complete', () => { + loadingText.destroy(); + this.graphics.destroy(); + this.newGraphics.destroy(); + }); + + // We'll create graphics objects instead of loading images + } + + create() { + // Create a texture for background + const bgGraphics = this.make.graphics({ x: 0, y: 0 }); + bgGraphics.fillStyle(0x222233); + bgGraphics.fillRect(0, 0, 800, 600); + + // Add some pattern to background + bgGraphics.fillStyle(0x1a1a2a); + for (let i = 0; i < 100; i++) { + const x = Math.random() * 800; + const y = Math.random() * 600; + const size = Math.random() * 5 + 2; + bgGraphics.fillRect(x, y, size, size); + } + + // Erstelle ein Partikel-Sprite für Spezialeffekte + const particleGraphics = this.make.graphics({ x: 0, y: 0 }); + particleGraphics.fillStyle(0xffffff); + particleGraphics.fillCircle(4, 4, 4); + particleGraphics.generateTexture('particle', 8, 8); + + bgGraphics.generateTexture('background', 800, 600); + + // Create a texture for player (pixel editor) + const playerGraphics = this.make.graphics({ x: 0, y: 0 }); + playerGraphics.fillStyle(0xff0000); + playerGraphics.fillRect(0, 0, 32, 32); + playerGraphics.fillStyle(0xff5555); + playerGraphics.fillRect(8, 8, 16, 16); + playerGraphics.generateTexture('player', 32, 32); + + // Erstelle Texturen für verschiedene Tile-Typen (8x8 Pixel Tiles, skaliert auf 32x32) + this.createTileTextures(); + + // Erstelle NPC-Texturen + this.createNPCTextures(); + + // Create a texture for basic tile + const tileGraphics = this.make.graphics({ x: 0, y: 0 }); + tileGraphics.fillStyle(0xffffff); + tileGraphics.fillRect(0, 0, 32, 32); + tileGraphics.lineStyle(1, 0xcccccc); + tileGraphics.strokeRect(0, 0, 32, 32); + tileGraphics.generateTexture('tile', 32, 32); + + // Create player walk animation frames (4 directions) + this.createPlayerWalkAnimations(); + + this.scene.start('MainMenuScene'); + } + + createPlayerWalkAnimations() { + // Erstelle eine Spritesheet-Textur für den Spieler im RPG + const frameWidth = 32; + const frameHeight = 32; + + // Farbpalette für den Spieler + const colors = { + body: 0x3366cc, // Blauer Körper + face: 0xffcc99, // Hautfarbe für Gesicht + hair: 0x663300, // Braune Haare + shirt: 0x339933, // Grünes Hemd + pants: 0x333366, // Dunkelblaue Hose + shoes: 0x663300, // Braune Schuhe + outline: 0x000000, // Schwarze Umrisse + }; + + // Gemeinsame Funktion zum Zeichnen der Grundform des Spielers + const drawPlayerBase = (graphics) => { + // Umriss + graphics.lineStyle(1, colors.outline); + + // Körper (Torso) + graphics.fillStyle(colors.shirt); + graphics.fillRect(10, 12, 12, 10); + graphics.strokeRect(10, 12, 12, 10); + + // Kopf + graphics.fillStyle(colors.face); + graphics.fillRect(10, 4, 12, 8); + graphics.strokeRect(10, 4, 12, 8); + + // Haare + graphics.fillStyle(colors.hair); + graphics.fillRect(10, 4, 12, 3); + graphics.strokeRect(10, 4, 12, 3); + + // Hose + graphics.fillStyle(colors.pants); + graphics.fillRect(10, 22, 12, 6); + graphics.strokeRect(10, 22, 12, 6); + }; + + // Nach unten (0) + const downGraphics = this.make.graphics({ x: 0, y: 0 }); + drawPlayerBase(downGraphics); + + // Gesicht nach unten + downGraphics.fillStyle(colors.outline); + downGraphics.fillRect(14, 8, 1, 1); // Linkes Auge + downGraphics.fillRect(17, 8, 1, 1); // Rechtes Auge + downGraphics.fillRect(15, 10, 2, 1); // Mund + + // Beine und Schuhe nach unten + downGraphics.fillStyle(colors.pants); + downGraphics.fillRect(12, 28, 3, 2); // Linkes Bein + downGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein + downGraphics.fillStyle(colors.shoes); + downGraphics.fillRect(12, 30, 3, 2); // Linker Schuh + downGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh + downGraphics.lineStyle(1, colors.outline); + downGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss + downGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss + + downGraphics.generateTexture('player_down', frameWidth, frameHeight); + + // Nach oben (1) + const upGraphics = this.make.graphics({ x: 0, y: 0 }); + drawPlayerBase(upGraphics); + + // Rücken der Haare + upGraphics.fillStyle(colors.hair); + upGraphics.fillRect(10, 2, 12, 2); + upGraphics.lineStyle(1, colors.outline); + upGraphics.strokeRect(10, 2, 12, 2); + + // Beine und Schuhe nach oben + upGraphics.fillStyle(colors.pants); + upGraphics.fillRect(12, 28, 3, 2); // Linkes Bein + upGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein + upGraphics.fillStyle(colors.shoes); + upGraphics.fillRect(12, 30, 3, 2); // Linker Schuh + upGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh + upGraphics.lineStyle(1, colors.outline); + upGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss + upGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss + + upGraphics.generateTexture('player_up', frameWidth, frameHeight); + + // Nach links (2) + const leftGraphics = this.make.graphics({ x: 0, y: 0 }); + drawPlayerBase(leftGraphics); + + // Gesicht nach links + leftGraphics.fillStyle(colors.outline); + leftGraphics.fillRect(12, 8, 1, 1); // Auge + leftGraphics.fillRect(11, 10, 2, 1); // Mund + + // Arm nach links + leftGraphics.fillStyle(colors.shirt); + leftGraphics.fillRect(6, 14, 4, 3); + leftGraphics.lineStyle(1, colors.outline); + leftGraphics.strokeRect(6, 14, 4, 3); + + // Beine und Schuhe nach links + leftGraphics.fillStyle(colors.pants); + leftGraphics.fillRect(12, 28, 3, 2); // Linkes Bein + leftGraphics.fillRect(15, 28, 3, 2); // Rechtes Bein + leftGraphics.fillStyle(colors.shoes); + leftGraphics.fillRect(9, 30, 6, 2); // Schuhe + leftGraphics.lineStyle(1, colors.outline); + leftGraphics.strokeRect(9, 28, 9, 4); // Beine Umriss + + leftGraphics.generateTexture('player_left', frameWidth, frameHeight); + + // Nach rechts (3) + const rightGraphics = this.make.graphics({ x: 0, y: 0 }); + drawPlayerBase(rightGraphics); + + // Gesicht nach rechts + rightGraphics.fillStyle(colors.outline); + rightGraphics.fillRect(19, 8, 1, 1); // Auge + rightGraphics.fillRect(19, 10, 2, 1); // Mund + + // Arm nach rechts + rightGraphics.fillStyle(colors.shirt); + rightGraphics.fillRect(22, 14, 4, 3); + rightGraphics.lineStyle(1, colors.outline); + rightGraphics.strokeRect(22, 14, 4, 3); + + // Beine und Schuhe nach rechts + rightGraphics.fillStyle(colors.pants); + rightGraphics.fillRect(14, 28, 3, 2); // Linkes Bein + rightGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein + rightGraphics.fillStyle(colors.shoes); + rightGraphics.fillRect(17, 30, 6, 2); // Schuhe + rightGraphics.lineStyle(1, colors.outline); + rightGraphics.strokeRect(14, 28, 9, 4); // Beine Umriss + + rightGraphics.generateTexture('player_right', frameWidth, frameHeight); + } + + createTileTextures() { + const tileSize = 32; // Größe jedes Tiles + + // 1. Gras + const grassGraphics = this.make.graphics({ x: 0, y: 0 }); + grassGraphics.fillStyle(0x88aa44); // Grün für Gras + grassGraphics.fillRect(0, 0, tileSize, tileSize); + // Kleine Texturen für Gras + grassGraphics.fillStyle(0x779933); + for (let i = 0; i < 8; i++) { + const x = Math.random() * tileSize; + const y = Math.random() * tileSize; + grassGraphics.fillRect(x, y, 2, 2); + } + grassGraphics.generateTexture('tile_grass', tileSize, tileSize); + + // 2. Gras mit Blumen + const grassFlowerGraphics = this.make.graphics({ x: 0, y: 0 }); + grassFlowerGraphics.fillStyle(0x88aa44); // Grün für Gras + grassFlowerGraphics.fillRect(0, 0, tileSize, tileSize); + // Kleine Texturen für Gras + grassFlowerGraphics.fillStyle(0x779933); + for (let i = 0; i < 5; i++) { + const x = Math.random() * tileSize; + const y = Math.random() * tileSize; + grassFlowerGraphics.fillRect(x, y, 2, 2); + } + // Blumen hinzufügen + grassFlowerGraphics.fillStyle(0xffff00); // Gelb für Blumen + for (let i = 0; i < 3; i++) { + const x = 5 + Math.random() * (tileSize - 10); + const y = 5 + Math.random() * (tileSize - 10); + grassFlowerGraphics.fillRect(x, y, 3, 3); + } + grassFlowerGraphics.fillStyle(0xff5555); // Rot für Blumen + for (let i = 0; i < 2; i++) { + const x = 5 + Math.random() * (tileSize - 10); + const y = 5 + Math.random() * (tileSize - 10); + grassFlowerGraphics.fillRect(x, y, 3, 3); + } + grassFlowerGraphics.generateTexture('tile_grass_flower', tileSize, tileSize); + + // 3. Erde + const dirtGraphics = this.make.graphics({ x: 0, y: 0 }); + dirtGraphics.fillStyle(0x8b4513); // Braun für Erde + dirtGraphics.fillRect(0, 0, tileSize, tileSize); + // Kleine Texturen für Erde + dirtGraphics.fillStyle(0x6b3304); + for (let i = 0; i < 10; i++) { + const x = Math.random() * tileSize; + const y = Math.random() * tileSize; + dirtGraphics.fillRect(x, y, 2, 2); + } + dirtGraphics.generateTexture('tile_dirt', tileSize, tileSize); + + // 4. Erde mit Steinen + const dirtStoneGraphics = this.make.graphics({ x: 0, y: 0 }); + dirtStoneGraphics.fillStyle(0x8b4513); // Braun für Erde + dirtStoneGraphics.fillRect(0, 0, tileSize, tileSize); + // Kleine Texturen für Erde + dirtStoneGraphics.fillStyle(0x6b3304); + for (let i = 0; i < 6; i++) { + const x = Math.random() * tileSize; + const y = Math.random() * tileSize; + dirtStoneGraphics.fillRect(x, y, 2, 2); + } + // Steine hinzufügen + dirtStoneGraphics.fillStyle(0x888888); // Grau für Steine + for (let i = 0; i < 4; i++) { + const size = 3 + Math.random() * 4; + const x = Math.random() * (tileSize - size); + const y = Math.random() * (tileSize - size); + dirtStoneGraphics.fillRect(x, y, size, size); + } + dirtStoneGraphics.generateTexture('tile_dirt_stone', tileSize, tileSize); + + // 5. Steinwand + const stoneWallGraphics = this.make.graphics({ x: 0, y: 0 }); + stoneWallGraphics.fillStyle(0x777777); // Grau für Steinwand + stoneWallGraphics.fillRect(0, 0, tileSize, tileSize); + + // Steinmuster für die Wand + stoneWallGraphics.fillStyle(0x555555); + const brickHeight = 8; + const brickWidth = 16; + + for (let y = 0; y < tileSize; y += brickHeight) { + const offset = y % (brickHeight * 2) === 0 ? 0 : brickWidth / 2; + for (let x = offset; x < tileSize; x += brickWidth) { + stoneWallGraphics.fillRect(x, y, brickWidth - 1, brickHeight - 1); + } + } + stoneWallGraphics.generateTexture('tile_stone_wall', tileSize, tileSize); + + // 6. Steinwand mit Blumen/Moos + const stoneWallFlowerGraphics = this.make.graphics({ x: 0, y: 0 }); + stoneWallFlowerGraphics.fillStyle(0x777777); // Grau für Steinwand + stoneWallFlowerGraphics.fillRect(0, 0, tileSize, tileSize); + + // Steinmuster für die Wand + stoneWallFlowerGraphics.fillStyle(0x555555); + for (let y = 0; y < tileSize; y += brickHeight) { + const offset = y % (brickHeight * 2) === 0 ? 0 : brickWidth / 2; + for (let x = offset; x < tileSize; x += brickWidth) { + stoneWallFlowerGraphics.fillRect(x, y, brickWidth - 1, brickHeight - 1); + } + } + + // Moos/Blumen an der Wand + stoneWallFlowerGraphics.fillStyle(0x55aa55); // Grün für Moos + for (let i = 0; i < 6; i++) { + const x = Math.random() * tileSize; + const y = Math.random() * tileSize; + const size = 2 + Math.random() * 3; + stoneWallFlowerGraphics.fillRect(x, y, size, size); + } + + stoneWallFlowerGraphics.fillStyle(0xffff00); // Gelb für Blumen + for (let i = 0; i < 2; i++) { + const x = 5 + Math.random() * (tileSize - 10); + const y = 5 + Math.random() * (tileSize - 10); + stoneWallFlowerGraphics.fillRect(x, y, 3, 3); + } + + stoneWallFlowerGraphics.generateTexture('tile_stone_wall_flower', tileSize, tileSize); + } + + createNPCTextures() { + const frameWidth = 32; + const frameHeight = 32; + + // Farbpalette für den NPC + const colors = { + body: 0xcc6633, // Bräunlicher Körper + face: 0xffcc99, // Hautfarbe für Gesicht + hair: 0x996600, // Blonde Haare + shirt: 0xcc3333, // Rotes Hemd + pants: 0x333333, // Schwarze Hose + shoes: 0x663300, // Braune Schuhe + outline: 0x000000, // Schwarze Umrisse + }; + + // Gemeinsame Funktion zum Zeichnen der Grundform des NPCs + const drawNPCBase = (graphics) => { + // Umriss + graphics.lineStyle(1, colors.outline); + + // Körper (Torso) + graphics.fillStyle(colors.shirt); + graphics.fillRect(10, 12, 12, 10); + graphics.strokeRect(10, 12, 12, 10); + + // Kopf + graphics.fillStyle(colors.face); + graphics.fillRect(10, 4, 12, 8); + graphics.strokeRect(10, 4, 12, 8); + + // Haare + graphics.fillStyle(colors.hair); + graphics.fillRect(10, 4, 12, 3); + graphics.strokeRect(10, 4, 12, 3); + + // Hose + graphics.fillStyle(colors.pants); + graphics.fillRect(10, 22, 12, 6); + graphics.strokeRect(10, 22, 12, 6); + }; + + // NPC nach unten schauend + const npcDownGraphics = this.make.graphics({ x: 0, y: 0 }); + drawNPCBase(npcDownGraphics); + + // Gesicht nach unten + npcDownGraphics.fillStyle(colors.outline); + npcDownGraphics.fillRect(14, 8, 1, 1); // Linkes Auge + npcDownGraphics.fillRect(17, 8, 1, 1); // Rechtes Auge + npcDownGraphics.fillRect(15, 10, 2, 1); // Mund + + // Beine und Schuhe nach unten + npcDownGraphics.fillStyle(colors.pants); + npcDownGraphics.fillRect(12, 28, 3, 2); // Linkes Bein + npcDownGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein + npcDownGraphics.fillStyle(colors.shoes); + npcDownGraphics.fillRect(12, 30, 3, 2); // Linker Schuh + npcDownGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh + npcDownGraphics.lineStyle(1, colors.outline); + npcDownGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss + npcDownGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss + + npcDownGraphics.generateTexture('npc_down', frameWidth, frameHeight); + + // NPC nach oben schauend + const npcUpGraphics = this.make.graphics({ x: 0, y: 0 }); + drawNPCBase(npcUpGraphics); + + // Rücken der Haare + npcUpGraphics.fillStyle(colors.hair); + npcUpGraphics.fillRect(10, 2, 12, 2); + npcUpGraphics.lineStyle(1, colors.outline); + npcUpGraphics.strokeRect(10, 2, 12, 2); + + // Beine und Schuhe nach oben + npcUpGraphics.fillStyle(colors.pants); + npcUpGraphics.fillRect(12, 28, 3, 2); // Linkes Bein + npcUpGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein + npcUpGraphics.fillStyle(colors.shoes); + npcUpGraphics.fillRect(12, 30, 3, 2); // Linker Schuh + npcUpGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh + npcUpGraphics.lineStyle(1, colors.outline); + npcUpGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss + npcUpGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss + + npcUpGraphics.generateTexture('npc_up', frameWidth, frameHeight); + } +} diff --git a/games/whopixels/js/scenes/GameScene.js b/games/whopixels/js/scenes/GameScene.js new file mode 100644 index 000000000..4fda2a4f3 --- /dev/null +++ b/games/whopixels/js/scenes/GameScene.js @@ -0,0 +1,113 @@ +class GameScene extends Phaser.Scene { + constructor() { + super({ key: 'GameScene' }); + } + + create() { + // Add background + this.add.image(400, 300, 'background'); + + // Create grid for pixel art (16x16 grid of 32x32 pixel tiles) + this.grid = []; + this.tileSize = 32; + this.gridWidth = 16; + this.gridHeight = 16; + this.gridStartX = (800 - this.gridWidth * this.tileSize) / 2; + this.gridStartY = (600 - this.gridHeight * this.tileSize) / 2; + + // Create grid of tiles + for (let y = 0; y < this.gridHeight; y++) { + this.grid[y] = []; + for (let x = 0; x < this.gridWidth; x++) { + const tile = this.add.image( + this.gridStartX + x * this.tileSize + this.tileSize / 2, + this.gridStartY + y * this.tileSize + this.tileSize / 2, + 'tile' + ); + tile.setTint(0xffffff); // Default white color + tile.setInteractive(); + tile.on('pointerdown', () => { + this.paintTile(x, y); + }); + this.grid[y][x] = tile; + } + } + + // Current selected color (default: black) + this.currentColor = 0x000000; + + // Create color palette + this.createColorPalette(); + + // Add UI text + this.add + .text(400, 50, 'Pixel Editor', { + fontSize: '32px', + fill: '#fff', + }) + .setOrigin(0.5); + + // Add back button + const backButton = this.add + .text(100, 50, 'Zurück', { + fontSize: '24px', + fill: '#fff', + backgroundColor: '#4a4a4a', + padding: { x: 10, y: 5 }, + }) + .setOrigin(0.5) + .setInteractive(); + + backButton.on('pointerover', () => { + backButton.setStyle({ fill: '#ff0' }); + }); + + backButton.on('pointerout', () => { + backButton.setStyle({ fill: '#fff' }); + }); + + backButton.on('pointerdown', () => { + this.scene.start('MainMenuScene'); + }); + } + + createColorPalette() { + const colors = [ + 0x000000, // Black + 0xffffff, // White + 0xff0000, // Red + 0x00ff00, // Green + 0x0000ff, // Blue + 0xffff00, // Yellow + 0xff00ff, // Magenta + 0x00ffff, // Cyan + ]; + + const paletteX = 700; + const paletteY = 150; + const paletteSize = 30; + const paletteGap = 10; + + colors.forEach((color, index) => { + const colorButton = this.add.rectangle( + paletteX, + paletteY + index * (paletteSize + paletteGap), + paletteSize, + paletteSize, + color + ); + + colorButton.setInteractive(); + colorButton.on('pointerdown', () => { + this.currentColor = color; + }); + + // Add stroke around the button + colorButton.setStrokeStyle(2, 0xffffff); + }); + } + + paintTile(x, y) { + this.grid[y][x].setTint(this.currentColor); + } +} diff --git a/games/whopixels/js/scenes/MainMenuScene.js b/games/whopixels/js/scenes/MainMenuScene.js new file mode 100644 index 000000000..bd811c22d --- /dev/null +++ b/games/whopixels/js/scenes/MainMenuScene.js @@ -0,0 +1,98 @@ +class MainMenuScene extends Phaser.Scene { + constructor() { + super({ key: 'MainMenuScene' }); + } + + create() { + // Add background + this.add.image(400, 300, 'background'); + + // Add title + this.add + .text(400, 120, 'WhoPixels', { + fontSize: '64px', + fill: '#fff', + fontStyle: 'bold', + }) + .setOrigin(0.5); + + // Add subtitle + this.add + .text(400, 180, 'Ein Pixel-Abenteuer', { + fontSize: '32px', + fill: '#fff', + }) + .setOrigin(0.5); + + // Create buttons + const startButton = this.add + .text(400, 280, 'RPG Spiel starten', { + fontSize: '32px', + fill: '#fff', + backgroundColor: '#4a4a4a', + padding: { x: 20, y: 10 }, + }) + .setOrigin(0.5) + .setInteractive(); + + const editorButton = this.add + .text(400, 350, 'Pixel Editor', { + fontSize: '32px', + fill: '#fff', + backgroundColor: '#4a4a4a', + padding: { x: 20, y: 10 }, + }) + .setOrigin(0.5) + .setInteractive(); + + const optionsButton = this.add + .text(400, 420, 'Optionen', { + fontSize: '32px', + fill: '#fff', + backgroundColor: '#4a4a4a', + padding: { x: 20, y: 10 }, + }) + .setOrigin(0.5) + .setInteractive(); + + // Button interactions - RPG Game + startButton.on('pointerover', () => { + startButton.setStyle({ fill: '#ff0' }); + }); + + startButton.on('pointerout', () => { + startButton.setStyle({ fill: '#fff' }); + }); + + startButton.on('pointerdown', () => { + this.scene.start('RPGScene'); + }); + + // Button interactions - Pixel Editor + editorButton.on('pointerover', () => { + editorButton.setStyle({ fill: '#ff0' }); + }); + + editorButton.on('pointerout', () => { + editorButton.setStyle({ fill: '#fff' }); + }); + + editorButton.on('pointerdown', () => { + this.scene.start('GameScene'); + }); + + // Button interactions - Options + optionsButton.on('pointerover', () => { + optionsButton.setStyle({ fill: '#ff0' }); + }); + + optionsButton.on('pointerout', () => { + optionsButton.setStyle({ fill: '#fff' }); + }); + + optionsButton.on('pointerdown', () => { + // Options functionality would go here + console.log('Options button clicked'); + }); + } +} diff --git a/games/whopixels/js/scenes/RPGScene.js b/games/whopixels/js/scenes/RPGScene.js new file mode 100644 index 000000000..e35f2e12f --- /dev/null +++ b/games/whopixels/js/scenes/RPGScene.js @@ -0,0 +1,1210 @@ +class RPGScene extends Phaser.Scene { + constructor() { + super({ key: 'RPGScene' }); + } + + preload() { + // Wir verwenden die in BootScene erstellten Texturen + } + + create() { + console.log('RPGScene create wird aufgerufen'); + + // Initialisiere NPC-Status + this.initNPCState(); + + // Spielwelt erstellen + this.createWorld(); + + // Spieler erstellen + this.createPlayer(); + + // NPCs erstellen + this.createNPCs(); + + // Kamera einrichten + this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels); + this.cameras.main.startFollow(this.player, true); + + // Steuerung einrichten + this.cursors = this.input.keyboard.createCursorKeys(); + + // UI erstellen + this.createUI(); + + console.log('RPGScene create abgeschlossen'); + } + + createWorld() { + // Einfache Welt mit Kacheln erstellen + this.map = { + widthInPixels: 440, // Angepasste Spielfeldgröße + heightInPixels: 440, + tileWidth: 40, + tileHeight: 40, + }; + + // Hintergrund + this.add + .tileSprite(0, 0, this.map.widthInPixels, this.map.heightInPixels, 'background') + .setOrigin(0, 0) + .setScale(1.0); // Doppelte Skalierung für größeres Bild + + // Erstelle eine Gruppe für Hindernisse + this.obstacles = this.physics.add.staticGroup(); + + // Definiere die Tile-Typen + const tileTypes = [ + { key: 'tile_grass', isObstacle: false }, + { key: 'tile_grass_flower', isObstacle: false }, + { key: 'tile_dirt', isObstacle: false }, + { key: 'tile_dirt_stone', isObstacle: false }, + { key: 'tile_stone_wall', isObstacle: true }, + { key: 'tile_stone_wall_flower', isObstacle: true }, + ]; + + // Größe des Spielfelds (angepasst für das 440x440 Spielfeld) + const gridSize = 11; // 11x11 Raster für 440x440 Pixel (40 Pixel pro Kachel) + + // Erstelle das Spielfeld + for (let y = 0; y < gridSize; y++) { + for (let x = 0; x < gridSize; x++) { + let tileType; + + // Erstelle Muster für die Welt + if (x === 0 || y === 0 || x === gridSize - 1 || y === gridSize - 1) { + // Tür in der oberen Mauer für NPCs + if (y === 0 && x === Math.floor(gridSize / 2)) { + // Tür (kein Hindernis) + tileType = tileTypes[2]; // Erde als Tür + } else { + // Steinwände am Rand + tileType = Math.random() < 0.3 ? tileTypes[5] : tileTypes[4]; // Steinwand oder Steinwand mit Blumen + } + } else { + // Innerer Bereich - Mischung aus Gras und Erde + const rand = Math.random(); + if (rand < 0.4) { + tileType = tileTypes[0]; // Gras + } else if (rand < 0.7) { + tileType = tileTypes[1]; // Gras mit Blumen + } else if (rand < 0.9) { + tileType = tileTypes[2]; // Erde + } else { + tileType = tileTypes[3]; // Erde mit Steinen + } + } + + // Erstelle das Tile mit doppelter Größe + const tile = this.add.image(x * 40 + 20, y * 40 + 20, tileType.key); + tile.setScale(2.0); // Doppelte Skalierung + + // Wenn es ein Hindernis ist, füge es zur Hindernisgruppe hinzu + if (tileType.isObstacle) { + // Erstelle ein unsichtbares Rechteck für die Kollision + const obstacle = this.add.rectangle( + x * 40 + 20, + y * 40 + 20, + 40 * 2, + 40 * 2 // Doppelte Größe für Kollision + ); + + // Füge das Hindernis zur Gruppe hinzu + this.obstacles.add(obstacle); + } + } + } + } + + createPlayer() { + // Spieler in der Mitte der Karte platzieren + this.player = this.physics.add.sprite( + this.map.widthInPixels / 2, // Mitte der Karte + this.map.heightInPixels / 2, + 'player_down' // Verwende die neue Textur + ); + + // Spieler-Größe anpassen - doppelte Skalierung + this.player.setScale(2.4); // Doppelte Skalierung (1.2 * 2) + + // Kollisionen mit Hindernissen + this.physics.add.collider(this.player, this.obstacles); + + // Spieler-Grenzen setzen + this.player.setCollideWorldBounds(true); + } + + createUI() { + // Zurück-Button + const backButton = this.add + .text(10, 10, 'Zurück zum Menü', { + fontSize: '18px', + fill: '#fff', + backgroundColor: '#4a4a4a', + padding: { x: 10, y: 5 }, + }) + .setScrollFactor(0) + .setInteractive(); + + backButton.on('pointerover', () => { + backButton.setStyle({ fill: '#ff0' }); + }); + + backButton.on('pointerout', () => { + backButton.setStyle({ fill: '#fff' }); + }); + + backButton.on('pointerdown', () => { + this.scene.start('MainMenuScene'); + }); + + // Spielanleitung + this.add + .text(10, 50, 'Pfeiltasten zum Bewegen', { + fontSize: '16px', + fill: '#fff', + backgroundColor: 'rgba(0,0,0,0.5)', + padding: { x: 5, y: 2 }, + }) + .setScrollFactor(0); + } + + createNPCs() { + console.log('createNPCs wird aufgerufen'); + + // Importiere die NPC-Charaktere aus der npc_characters.js-Datei + try { + // Versuche, die NPC-Charaktere aus der externen Datei zu laden + this.npcCharacters = window.npcCharacters || []; + + if (!this.npcCharacters || this.npcCharacters.length === 0) { + console.error( + 'Keine NPC-Charaktere gefunden! Stelle sicher, dass npc_characters.js geladen wurde.' + ); + // Fallback zu einigen Standard-Charakteren + this.npcCharacters = [ + { + id: 1, + name: 'Leonardo da Vinci', + personality: 'Ein vielseitiger Universalgelehrter der Renaissance.', + hint: 'Meine Skizzenbücher enthalten Flugmaschinen und anatomische Studien.', + }, + { + id: 2, + name: 'Nikola Tesla', + personality: 'Ein exzentrischer Elektroingenieur mit visionären Ideen.', + hint: 'Meine Arbeiten mit Wechselstrom revolutionierten die Energienutzung.', + }, + ]; + } + } catch (error) { + console.error('Fehler beim Laden der NPC-Charaktere:', error); + // Fallback zu einigen Standard-Charakteren + this.npcCharacters = [ + { + id: 1, + name: 'Leonardo da Vinci', + personality: 'Ein vielseitiger Universalgelehrter der Renaissance.', + hint: 'Meine Skizzenbücher enthalten Flugmaschinen und anatomische Studien.', + }, + { + id: 2, + name: 'Nikola Tesla', + personality: 'Ein exzentrischer Elektroingenieur mit visionären Ideen.', + hint: 'Meine Arbeiten mit Wechselstrom revolutionierten die Energienutzung.', + }, + ]; + } + + console.log('NPC-Charaktere geladen:', this.npcCharacters.length); + + // Array für alle NPCs im Spiel + this.npcs = []; + + // Aktiver NPC (der, mit dem der Spieler gerade interagiert) + this.npc = null; + + // Speichere den Status der NPCs + this.npcState = { + isInConversation: false, + isWaitingForResponse: false, + identityRevealed: false, + discoveredNPCs: [], + currentNpcIndex: -1, + }; + + // Dialog-Box für den NPC + this.npcDialog = this.add.text(0, 0, 'Hallo! Ich bin ein NPC.\nDrücke E zum Sprechen.', { + fontSize: '12px', + fill: '#fff', + backgroundColor: '#000', + padding: { x: 5, y: 5 }, + wordWrap: { width: 200 }, + }); + this.npcDialog.setVisible(false); + + // Interaktions-Prompt + this.interactionPrompt = this.add.text(0, 0, 'Drücke E zum Sprechen', { + fontSize: '10px', + fill: '#fff', + backgroundColor: '#000', + padding: { x: 3, y: 3 }, + }); + this.interactionPrompt.setVisible(false); + + // Erstelle den ersten NPC + this.createNewNPC(); + } + + // Methode zum Erstellen eines neuen NPCs + createNewNPC() { + console.log('createNewNPC wird aufgerufen'); + + // Initialisiere npcState, wenn es noch nicht existiert + if (!this.npcState) { + this.initNPCState(); + } + + // Debug-Ausgabe der bereits entdeckten NPCs + console.log('Bereits entdeckte NPCs:', this.npcState.discoveredNPCs); + + // Wähle einen zufälligen NPC-Charakter, der noch nicht entdeckt wurde + // UND der nicht der aktuelle NPC ist (falls vorhanden) + let availableCharacters = this.npcCharacters.filter( + (char) => !this.npcState.discoveredNPCs.includes(char.id) + ); + + // Wenn es einen aktuellen NPC gibt, stelle sicher, dass wir nicht denselben Charakter erneut auswählen + if (this.npc && this.npc.characterId) { + availableCharacters = availableCharacters.filter((char) => char.id !== this.npc.characterId); + console.log( + `Aktueller NPC ${this.npc.characterName} (ID: ${this.npc.characterId}) wird aus der Auswahl ausgeschlossen.` + ); + } + + console.log('Verfügbare Charaktere:', availableCharacters.length); + availableCharacters.forEach((char) => { + console.log(`- ${char.name} (ID: ${char.id})`); + }); + + // Wenn keine Charaktere mehr verfügbar sind, verwende alle Charaktere außer dem aktuellen + if (availableCharacters.length === 0) { + console.log('Keine neuen Charaktere verfügbar, verwende alle außer dem aktuellen.'); + availableCharacters = this.npcCharacters.filter((char) => { + if (this.npc && this.npc.characterId) { + return char.id !== this.npc.characterId; + } + return true; + }); + + if (availableCharacters.length === 0) { + console.log('Keine Charaktere verfügbar!'); + return null; + } + } + + const randomIndex = Math.floor(Math.random() * availableCharacters.length); + const selectedCharacter = availableCharacters[randomIndex]; + + console.log( + 'Ausgewählter Charakter:', + selectedCharacter.name, + '(ID:', + selectedCharacter.id, + ')' + ); + console.log('Persönlichkeit:', selectedCharacter.personality); + + // Feste Position in der Mitte des Bildschirms + // Positioniere den NPC an der Tür in der oberen Mauer + const doorX = Math.floor(this.map.widthInPixels / 2); + const doorY = 40; // Knapp unterhalb der oberen Mauer (angepasst für 440x440 Spielfeld) + + // Verwende die Türposition + const x = doorX; + const y = doorY; + + console.log(`NPC wird an der Tür (${x}, ${y}) erstellt`); + + // Erstelle den NPC + const newNpc = this.physics.add.sprite(x, y, 'npc_down'); + newNpc.setScale(2.4); // Doppelte Skalierung (1.2 * 2) + + // NPC ist im Anonymitätsmodus (komplett schwarz) + newNpc.setTint(0x000000); + + // Speichere die Charakter-ID im NPC-Objekt + newNpc.characterId = selectedCharacter.id; + newNpc.characterName = selectedCharacter.name; + newNpc.characterPersonality = selectedCharacter.personality; + + // Animation: NPC läuft durch die Tür herein + // Starte an der Türposition + newNpc.y = doorY; + + // Animiere den NPC, damit er durch die Tür hereinläuft + this.tweens.add({ + targets: newNpc, + y: this.map.heightInPixels / 2, // Zielposition in der Mitte des Spielfelds + duration: 2000, + ease: 'Linear', // Gleichmäßige Bewegung für einen Laufeffekt + onUpdate: () => { + // Aktualisiere die Position des Debug-Textes während der Animation + if (newNpc.debugText) { + newNpc.debugText.x = newNpc.x; + newNpc.debugText.y = newNpc.y + 20; + } + + // Wechsle zwischen verschiedenen Frames, um eine Laufanimation zu simulieren + if (Math.floor(Date.now() / 150) % 2 === 0) { + newNpc.setTexture('npc_down'); + } else { + // Wenn es eine alternative Textur gibt, verwende diese + // Ansonsten bleibt es bei 'npc_down' + if (this.textures.exists('npc_down_walk')) { + newNpc.setTexture('npc_down_walk'); + } + } + }, + }); + + // Füge einen Debug-Text unter dem NPC hinzu + // Bei anonymen NPCs nur "Anonym" anzeigen + const debugText = this.add.text(x, y + 20, 'Anonym', { + fontSize: '10px', + fontFamily: 'Arial', + fill: '#ffffff', + stroke: '#000000', + strokeThickness: 2, + align: 'center', + }); + debugText.setOrigin(0.5, 0); + newNpc.debugText = debugText; // Speichere die Referenz im NPC-Objekt + + // Kollisionen mit Hindernissen + if (this.obstacles) { + this.physics.add.collider(newNpc, this.obstacles); + } + + // Kollision mit dem Spieler + if (this.player) { + this.physics.add.collider(newNpc, this.player, this.showInteractionPrompt, null, this); + } + + // Füge den NPC zum Array hinzu + this.npcs.push(newNpc); + + // Setze den aktuellen NPC + this.npc = newNpc; + this.npcState.currentNpcIndex = this.npcs.length - 1; + + console.log(`Neuer NPC erstellt: ${selectedCharacter.name} (ID: ${selectedCharacter.id})`); + + return newNpc; + } + + // Methode zum Erstellen eines Test-NPCs an einer festen Position + createTestNPC() { + console.log('createTestNPC wird aufgerufen'); + + // Feste Position in der Nähe des Spielers + const x = 400; + const y = 400; + + // Erstelle den NPC + const testNpc = this.physics.add.sprite(x, y, 'npc_down'); + testNpc.setScale(1.2); + + // NPC ist im Anonymitätsmodus (komplett schwarz) + testNpc.setTint(0x000000); + + // Speichere die Charakter-ID im NPC-Objekt + testNpc.characterId = 1; + testNpc.characterName = 'Test NPC'; + testNpc.characterPersonality = 'Ein Test-NPC zum Testen der Anzeige'; + + // Kollisionen mit Hindernissen + if (this.obstacles) { + this.physics.add.collider(testNpc, this.obstacles); + } + + // Kollision mit dem Spieler + if (this.player) { + this.physics.add.collider(testNpc, this.player, this.showInteractionPrompt, null, this); + } + + // Füge den NPC zum Array hinzu + if (!this.npcs) { + this.npcs = []; + } + this.npcs.push(testNpc); + + // Setze den aktuellen NPC + this.npc = testNpc; + if (this.npcState) { + this.npcState.currentNpcIndex = this.npcs.length - 1; + } + + console.log(`Test-NPC erstellt an Position ${x}, ${y}`); + + return testNpc; + } + + // Methode zum Initialisieren der UI-Elemente + createUI() { + // Interaktions-Prompt + this.interactionPrompt.setVisible(false); + + // Chat-Eingabe erstellen + this.createChatInput(); + + // Interaktions-Taste (E) für Dialog + this.interactKey = this.input.keyboard.addKey('E'); + + // NPC-Bewegung + this.time.addEvent({ + delay: 3000, // Alle 3 Sekunden + callback: this.moveNPC, + callbackScope: this, + loop: true, + }); + } + + // Methode zum Initialisieren des NPC-Status + initNPCState() { + console.log('initNPCState wird aufgerufen'); + // NPC-Status initialisieren + this.npcState = { + isInConversation: false, + lastMessage: '', + isWaitingForResponse: false, + identityRevealed: false, + discoveredNPCs: [], // Liste der bereits entdeckten NPCs (IDs) + currentNpcIndex: -1, + }; + + // Debug-Ausgabe der NPC-Charaktere + if (this.npcCharacters) { + console.log('Verfügbare NPC-Charaktere:'); + this.npcCharacters.forEach((char) => { + console.log(`- ${char.name} (ID: ${char.id})`); + }); + } + + console.log('NPC-Status initialisiert'); + } + + moveNPC() { + // Zufällige Bewegung des NPCs + if (!this.npc) return; + + // Stoppe vorherige Bewegung + this.npc.setVelocity(0); + + // 30% Chance, dass der NPC sich bewegt + if (Math.random() < 0.3) { + const speed = 50; + const direction = Math.floor(Math.random() * 4); // 0: up, 1: right, 2: down, 3: left + + switch (direction) { + case 0: // up + this.npc.setVelocityY(-speed); + this.npc.setTexture('npc_up'); + break; + case 1: // right + this.npc.setVelocityX(speed); + this.npc.setTexture('npc_down'); // Wir haben nur up/down Texturen + break; + case 2: // down + this.npc.setVelocityY(speed); + this.npc.setTexture('npc_down'); + break; + case 3: // left + this.npc.setVelocityX(-speed); + this.npc.setTexture('npc_up'); // Wir haben nur up/down Texturen + break; + } + + // Stoppe die Bewegung nach 1-2 Sekunden + this.time.delayedCall( + 1000 + Math.random() * 1000, + () => { + if (this.npc) this.npc.setVelocity(0); + }, + [], + this + ); + } + } + + createChatInput() { + const width = this.cameras.main.width; + const height = this.cameras.main.height; + const padding = 20; + const chatWidth = width - padding * 2; + const chatHeight = 250; // Erhöhte Höhe für mehr Platz + + // Chat-Hintergrund mit abgerundeten Ecken und Schatten + this.chatBackground = this.add.graphics(); + this.chatBackground.fillStyle(0x1a1a2a, 0.9); // Dunkleres Blau mit höherer Opazität + this.chatBackground.fillRoundedRect( + padding, + height - chatHeight - padding, + chatWidth, + chatHeight, + 10 // Abgerundete Ecken + ); + + // Füge einen Rahmen hinzu + this.chatBackground.lineStyle(2, 0x4a6fa5, 1); // Blauer Rahmen + this.chatBackground.strokeRoundedRect( + padding, + height - chatHeight - padding, + chatWidth, + chatHeight, + 10 + ); + + this.chatBackground.setScrollFactor(0); + this.chatBackground.setVisible(false); + + // Titel für den Chat + this.chatTitle = this.add.text( + width / 2, + height - chatHeight - padding + 20, + 'Gespräch mit NPC', + { + fontSize: '18px', + fontFamily: 'Arial', + fontStyle: 'bold', + fill: '#ffffff', + align: 'center', + } + ); + this.chatTitle.setOrigin(0.5, 0.5); + this.chatTitle.setScrollFactor(0); + this.chatTitle.setVisible(false); + + // NPC-Antwortbereich mit besserem Styling + this.npcResponse = this.add.text(padding + 15, height - chatHeight - padding + 50, '', { + fontSize: '16px', + fontFamily: 'Arial', + fill: '#e0e0ff', // Helleres Blau für bessere Lesbarkeit + padding: { x: 10, y: 10 }, + wordWrap: { width: chatWidth - 50 }, + lineSpacing: 6, + }); + this.npcResponse.setScrollFactor(0); + this.npcResponse.setVisible(false); + + // Trennlinie zwischen Antwort und Eingabe + this.chatDivider = this.add.graphics(); + this.chatDivider.lineStyle(1, 0x4a6fa5, 0.8); // Blauer Trennstrich + this.chatDivider.lineBetween(padding + 15, height - 90, width - padding - 15, height - 90); + this.chatDivider.setScrollFactor(0); + this.chatDivider.setVisible(false); + + // Chat-Eingabefeld mit besserem Styling + const inputBg = this.add.graphics(); + inputBg.fillStyle(0x2a2a3a, 1); // Dunklerer Hintergrund für Eingabefeld + inputBg.fillRoundedRect( + padding + 15, + height - 70, + chatWidth - 230, // Platz für Buttons lassen + 40, + 5 + ); + inputBg.setScrollFactor(0); + inputBg.setVisible(false); + this.inputBackground = inputBg; + + this.chatInput = this.add.text(padding + 25, height - 65, 'Tippe deine Nachricht hier ein...', { + fontSize: '16px', + fontFamily: 'Arial', + fill: '#bbbbbb', // Hellgrau für Platzhaltertext + padding: { x: 5, y: 5 }, + }); + this.chatInput.setScrollFactor(0); + this.chatInput.setVisible(false); + + // Chat-Senden-Button mit besserem Styling + const sendBg = this.add.graphics(); + sendBg.fillStyle(0x4a6fa5, 1); // Blauer Button + sendBg.fillRoundedRect(width - padding - 100, height - 70, 85, 40, 5); + sendBg.setScrollFactor(0); + sendBg.setVisible(false); + this.sendBackground = sendBg; + + this.chatSendButton = this.add.text(width - padding - 57.5, height - 50, 'Senden', { + fontSize: '16px', + fontFamily: 'Arial', + fontStyle: 'bold', + fill: '#ffffff', + }); + this.chatSendButton.setOrigin(0.5, 0.5); + this.chatSendButton.setScrollFactor(0); + this.chatSendButton.setVisible(false); + this.chatSendButton.setInteractive({ useHandCursor: true }); + this.chatSendButton.on('pointerdown', () => this.sendChatMessage()); + + // Hover-Effekt für den Senden-Button + this.chatSendButton.on('pointerover', () => { + sendBg.clear(); + sendBg.fillStyle(0x5a7fb5, 1); // Hellerer Blau bei Hover + sendBg.fillRoundedRect(width - padding - 100, height - 70, 85, 40, 5); + }); + + this.chatSendButton.on('pointerout', () => { + sendBg.clear(); + sendBg.fillStyle(0x4a6fa5, 1); // Normales Blau + sendBg.fillRoundedRect(width - padding - 100, height - 70, 85, 40, 5); + }); + + // X-Icon zum Schließen in der oberen rechten Ecke + const closeIconSize = 24; + const closeIconPadding = 10; + + // Runder Hintergrund für das X-Icon + const closeIconBg = this.add.graphics(); + closeIconBg.fillStyle(0x8a4a4a, 0.7); // Halbtransparentes Rot + closeIconBg.fillCircle( + width - padding - closeIconPadding, + height - chatHeight - padding + closeIconPadding + closeIconSize / 2, + closeIconSize / 2 + ); + closeIconBg.setScrollFactor(0); + closeIconBg.setVisible(false); + this.cancelBackground = closeIconBg; + + // X-Icon erstellen mit Linien + const closeIcon = this.add.graphics(); + // Zeichne das X + closeIcon.lineStyle(3, 0xffffff, 1); + // Erste Linie des X (von oben links nach unten rechts) + closeIcon.lineBetween( + width - padding - closeIconPadding - closeIconSize / 3, + height - chatHeight - padding + closeIconPadding + closeIconSize / 3, + width - padding - closeIconPadding + closeIconSize / 3, + height - chatHeight - padding + closeIconPadding + (closeIconSize * 2) / 3 + ); + // Zweite Linie des X (von oben rechts nach unten links) + closeIcon.lineBetween( + width - padding - closeIconPadding + closeIconSize / 3, + height - chatHeight - padding + closeIconPadding + closeIconSize / 3, + width - padding - closeIconPadding - closeIconSize / 3, + height - chatHeight - padding + closeIconPadding + (closeIconSize * 2) / 3 + ); + closeIcon.setScrollFactor(0); + closeIcon.setVisible(false); + this.chatCancelButton = closeIcon; + + // Interaktiver Bereich für das X-Icon + const closeHitArea = this.add.rectangle( + width - padding - closeIconPadding, + height - chatHeight - padding + closeIconPadding + closeIconSize / 2, + closeIconSize * 1.5, + closeIconSize * 1.5 + ); + closeHitArea.setScrollFactor(0); + closeHitArea.setVisible(false); // Unsichtbarer Klickbereich + closeHitArea.setInteractive({ useHandCursor: true }); + closeHitArea.on('pointerdown', () => this.closeChatInput()); + this.closeHitArea = closeHitArea; + + // Hover-Effekt für das X-Icon + closeHitArea.on('pointerover', () => { + closeIconBg.clear(); + closeIconBg.fillStyle(0x9a5a5a, 0.9); // Helleres, weniger transparentes Rot bei Hover + closeIconBg.fillCircle( + width - padding - closeIconPadding, + height - chatHeight - padding + closeIconPadding + closeIconSize / 2, + (closeIconSize / 2) * 1.1 // Leicht größer bei Hover + ); + }); + + closeHitArea.on('pointerout', () => { + closeIconBg.clear(); + closeIconBg.fillStyle(0x8a4a4a, 0.7); // Normales Rot + closeIconBg.fillCircle( + width - padding - closeIconPadding, + height - chatHeight - padding + closeIconPadding + closeIconSize / 2, + closeIconSize / 2 + ); + }); + + // Aktiviere Tastatureingabe + this.userInput = ''; + this.input.keyboard.on('keydown', this.handleKeyInput, this); + + // Letzte NPC-Antwort + this.lastNpcResponse = ''; + + // Konversationsverlauf für das LLM + this.conversationHistory = []; + } + + showInteractionPrompt() { + // Wenn der Spieler mit dem NPC kollidiert + if (!this.npc || !this.player) return; + + // Zeige Interaktions-Prompt über dem NPC + this.interactionPrompt.setPosition( + this.npc.x - this.interactionPrompt.width / 2, + this.npc.y - 40 + ); + + this.interactionPrompt.setVisible(true); + + // Verstecke den Prompt nach 2 Sekunden + this.time.delayedCall( + 2000, + () => { + this.interactionPrompt.setVisible(false); + }, + [], + this + ); + } + + talkToNPC() { + // Wenn der Spieler mit dem NPC spricht + if (!this.npc || !this.player || this.npcState.isInConversation) return; + + // Stoppe die Bewegung des Spielers und des NPCs + this.player.setVelocity(0); + this.npc.setVelocity(0); + + // Drehe den NPC zum Spieler + if (this.player.x < this.npc.x) { + this.npc.setTexture('npc_up'); // Links (wir haben nur up/down) + } else { + this.npc.setTexture('npc_down'); // Rechts + } + + // Starte die Konversation + this.npcState.isInConversation = true; + + // Starte direkt den Chat ohne Sprechblase über dem NPC + this.lastNpcResponse = 'Verhüllt von Zeit,\nwer könnt es sein?'; + + // Zeige Chat-Eingabe direkt + this.openChatInput(); + } + + openChatInput() { + // Aktiviere Chat-Eingabe + this.chatBackground.setVisible(true); + this.chatTitle.setVisible(true); + this.inputBackground.setVisible(true); + this.chatInput.setVisible(true); + this.sendBackground.setVisible(true); + this.chatSendButton.setVisible(true); + this.cancelBackground.setVisible(true); + this.chatCancelButton.setVisible(true); + this.closeHitArea.setVisible(true); // Klickbereich für X-Icon + this.npcResponse.setVisible(true); + this.chatDivider.setVisible(true); + + // Setze Eingabe zurück + this.userInput = ''; + this.chatInput.setText('Tippe deine Nachricht hier ein...'); + this.chatInput.setStyle({ fill: '#bbbbbb' }); // Platzhaltertext in Grau + + // Zeige letzte NPC-Antwort an + this.npcResponse.setText(this.lastNpcResponse || 'Sprich mit dem NPC...'); + + // Aktualisiere den Titel mit dem NPC-Namen + this.chatTitle.setText('Gespräch mit Unbekanntem'); + } + + closeChatInput() { + // Deaktiviere Chat-Eingabe + this.chatBackground.setVisible(false); + this.chatTitle.setVisible(false); + this.inputBackground.setVisible(false); + this.chatInput.setVisible(false); + this.sendBackground.setVisible(false); + this.chatSendButton.setVisible(false); + this.cancelBackground.setVisible(false); + this.chatCancelButton.setVisible(false); + this.closeHitArea.setVisible(false); // Klickbereich für X-Icon + this.npcResponse.setVisible(false); + this.chatDivider.setVisible(false); + + // Beende die Konversation + this.npcState.isInConversation = false; + this.npcDialog.setVisible(false); + } + + handleKeyInput(event) { + // Wenn keine Chat-Eingabe aktiv ist, ignoriere Tastatureingabe + if (!this.chatInput.visible) return; + + // Enter-Taste zum Senden + if (event.keyCode === 13) { + // Enter + this.sendChatMessage(); + return; + } + + // Escape-Taste zum Abbrechen + if (event.keyCode === 27) { + // Escape + this.closeChatInput(); + return; + } + + // Backspace zum Löschen + if (event.keyCode === 8 && this.userInput.length > 0) { + // Backspace + this.userInput = this.userInput.slice(0, -1); + } + // Normale Tasteneingabe + else if (event.keyCode >= 32 && event.keyCode <= 126) { + // Druckbare Zeichen + this.userInput += event.key; + } + + // Aktualisiere Anzeige + if (this.userInput.length === 0) { + this.chatInput.setText('Tippe deine Nachricht hier ein...'); + this.chatInput.setStyle({ fill: '#bbbbbb' }); // Platzhaltertext in Grau + } else { + this.chatInput.setText(this.userInput); + this.chatInput.setStyle({ fill: '#ffffff' }); // Aktiver Text in Weiß + + // Visuelles Feedback beim Tippen + this.tweens.add({ + targets: this.inputBackground, + alpha: 0.7, + duration: 50, + yoyo: true, + ease: 'Power1', + }); + } + } + + updateNpcResponse(response) { + // Aktualisiere die letzte NPC-Antwort + this.lastNpcResponse = response; + + // Aktualisiere die Anzeige, wenn sichtbar + if (this.npcResponse.visible) { + this.npcResponse.setText(response); + } + } + + // Methode, die aufgerufen wird, wenn der Spieler die Identität des NPCs erraten hat + revealIdentity() { + // Markiere, dass die Identität aufgedeckt wurde + this.npcState.identityRevealed = true; + + // Füge den aktuellen NPC zur Liste der entdeckten NPCs hinzu + if (this.npc && this.npc.characterId) { + // Prüfe, ob der NPC bereits in der Liste ist, um Duplikate zu vermeiden + if (!this.npcState.discoveredNPCs.includes(this.npc.characterId)) { + this.npcState.discoveredNPCs.push(this.npc.characterId); + console.log(`NPC ${this.npc.characterName} (ID: ${this.npc.characterId}) wurde entdeckt!`); + console.log('Aktualisierte Liste der entdeckten NPCs:', this.npcState.discoveredNPCs); + } else { + console.log( + `NPC ${this.npc.characterName} (ID: ${this.npc.characterId}) wurde bereits zuvor entdeckt.` + ); + } + } + + // Spiele einen Soundeffekt ab (falls vorhanden) + // this.sound.play('reveal'); + + // Entferne die schwarze Einfärbung, um den NPC normal anzuzeigen + this.npc.clearTint(); + + // Aktualisiere den Debug-Text mit dem richtigen Namen ohne ID + if (this.npc.debugText) { + this.npc.debugText.setText(this.npc.characterName); + this.npc.debugText.setStyle({ + fontSize: '12px', + fontFamily: 'Arial', + fontStyle: 'bold', + fill: '#ffff00', // Gelb für aufgedeckte NPCs + stroke: '#000000', + strokeThickness: 3, + align: 'center', + }); + } + + // Kurzer gelber Blitz-Effekt + this.npc.setTint(0xffff00); + this.time.delayedCall(300, () => { + if (this.npcState.identityRevealed) { + this.npc.clearTint(); // Zur normalen Farbe zurückkehren + } + }); + + // Erstelle einen Partikeleffekt um den NPC + const particles = this.add.particles('particle'); // Du musst ein Partikel-Sprite laden + + // Erstelle einen Emitter für den Partikeleffekt + if (particles.createEmitter) { + const emitter = particles.createEmitter({ + x: this.npc.x, + y: this.npc.y, + speed: { min: 50, max: 100 }, + angle: { min: 0, max: 360 }, + scale: { start: 0.5, end: 0 }, + blendMode: 'ADD', + lifespan: 1000, + gravityY: 0, + }); + + // Stoppe den Emitter nach 2 Sekunden + this.time.delayedCall(2000, () => { + emitter.stop(); + }); + } else { + console.warn('Particles not available or not properly loaded'); + } + + // Aktualisiere den Chat-Titel, um den NPC-Namen anzuzeigen + if (this.chatTitle && this.chatTitle.visible) { + this.chatTitle.setText(`Gespräch mit ${this.npc.characterName}`); + } + + // Zeige eine spezielle Nachricht an + const revealText = this.add.text( + this.cameras.main.width / 2, + this.cameras.main.height / 3, + `Du hast ${this.npc.characterName} entlarvt!`, + { + fontSize: '24px', + fontFamily: 'Arial', + fontStyle: 'bold', + fill: '#ffff00', + stroke: '#000000', + strokeThickness: 4, + align: 'center', + } + ); + revealText.setOrigin(0.5); + revealText.setScrollFactor(0); + + // Blende die Nachricht nach einigen Sekunden aus + this.tweens.add({ + targets: revealText, + alpha: 0, + duration: 2000, + delay: 3000, + onComplete: () => { + revealText.destroy(); + + // Erstelle nach kurzer Verzögerung einen neuen NPC + this.time.delayedCall(1000, () => { + // Erstelle einen neuen NPC nur, wenn noch nicht alle entdeckt wurden + const newNpc = this.createNewNPC(); + if (newNpc) { + // Zeige eine Benachrichtigung an + const newNpcText = this.add.text( + this.cameras.main.width / 2, + this.cameras.main.height / 3, + 'Ein neuer geheimnisvoller NPC ist erschienen!', + { + fontSize: '20px', + fontFamily: 'Arial', + fontStyle: 'bold', + fill: '#ffffff', + stroke: '#000000', + strokeThickness: 3, + align: 'center', + } + ); + newNpcText.setOrigin(0.5); + newNpcText.setScrollFactor(0); + + // Blende die Nachricht nach einigen Sekunden aus + this.tweens.add({ + targets: newNpcText, + alpha: 0, + duration: 1500, + delay: 2500, + onComplete: () => newNpcText.destroy(), + }); + } + }); + }, + }); + } + + async sendChatMessage() { + // Wenn keine Nachricht eingegeben wurde + if (this.userInput.length === 0 || this.npcState.isWaitingForResponse) return; + + const message = this.userInput; + this.userInput = ''; + + // Füge die Nachricht des Spielers zum Konversationsverlauf hinzu (für das LLM) + this.conversationHistory.push({ + type: 'user', + message: message, + }); + + // Zeige "Nachricht wird gesendet"-Status + this.chatInput.setText('Nachricht wird gesendet...'); + this.npcState.isWaitingForResponse = true; + + try { + // Sende Anfrage an den Server mit der Konversationshistorie + const response = await fetch('http://localhost:3000/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message, + conversationHistory: this.conversationHistory, + characterName: this.npc ? this.npc.characterName : null, + characterPersonality: this.npc ? this.npc.characterPersonality : null, + }), + }); + + const data = await response.json(); + + // Verarbeite die Antwort + let npcResponse = 'Entschuldigung, ich habe dich nicht verstanden.'; + + if (data.response) { + npcResponse = data.response; + + // Füge die NPC-Antwort zum Konversationsverlauf hinzu (für das LLM) + this.conversationHistory.push({ + type: 'npc', + message: npcResponse, + }); + + // Prüfe, ob die Identität aufgedeckt wurde + if (data.identityRevealed) { + console.log('Identität aufgedeckt!'); + + // Spiele eine Animation ab oder führe eine spezielle Aktion aus + this.revealIdentity(); + } + } + + // Aktualisiere die NPC-Antwort + this.updateNpcResponse(npcResponse); + + // Füge die Antwort des NPCs zum Konversationsverlauf hinzu (für das LLM) + this.conversationHistory.push({ + type: 'npc', + message: npcResponse, + }); + } catch (error) { + console.error('Fehler beim Senden der Nachricht:', error); + + // Aktualisiere die NPC-Antwort mit Fehlermeldung + this.updateNpcResponse('Entschuldigung, ich kann gerade nicht antworten.'); + + // Füge die Fehlermeldung zum Konversationsverlauf hinzu (für das LLM) + this.conversationHistory.push({ + type: 'npc', + message: 'Entschuldigung, ich kann gerade nicht antworten.', + }); + + // Fehlermeldung wird nur im Chat-Fenster angezeigt + } + + // Setze Status zurück + this.npcState.isWaitingForResponse = false; + this.chatInput.setText('Tippe deine Nachricht hier ein...'); + } + + update() { + // Spielerbewegung + this.handlePlayerMovement(); + + // Aktualisiere die Position des Dialogs, wenn er sichtbar ist + if (this.npcDialog && this.npcDialog.visible && this.npc) { + this.npcDialog.setPosition(this.npc.x - 100, this.npc.y - 50); + } + + // Aktualisiere die Position des Interaktions-Prompts + if (this.interactionPrompt && this.interactionPrompt.visible && this.npc) { + this.interactionPrompt.setPosition(this.npc.x - 50, this.npc.y - 30); + } + + // Aktualisiere die Position der Debug-Texte für alle NPCs + if (this.npcs) { + this.npcs.forEach((npc) => { + if (npc.debugText) { + npc.debugText.setPosition(npc.x, npc.y + 20); + } + }); + } + } + + handlePlayerMovement() { + if (!this.player) return; + + const speed = 160; + + // Horizontal + if (this.cursors.left.isDown) { + this.player.setVelocityX(-speed); + this.player.setTexture('player_left'); + } else if (this.cursors.right.isDown) { + this.player.setVelocityX(speed); + this.player.setTexture('player_right'); + } else { + this.player.setVelocityX(0); + } + + // Vertikal + if (this.cursors.up.isDown) { + this.player.setVelocityY(-speed); + if (!this.cursors.left.isDown && !this.cursors.right.isDown) { + this.player.setTexture('player_up'); + } + } else if (this.cursors.down.isDown) { + this.player.setVelocityY(speed); + if (!this.cursors.left.isDown && !this.cursors.right.isDown) { + this.player.setTexture('player_down'); + } + } else { + this.player.setVelocityY(0); + } + + // Interaktion mit NPCs prüfen, wenn E gedrückt wird + if ( + this.interactKey && + Phaser.Input.Keyboard.JustDown(this.interactKey) && + this.npcs && + this.npcs.length > 0 + ) { + // Prüfe die Distanz zu allen NPCs + let closestNPC = null; + let closestDistance = 100; // Maximale Interaktionsdistanz + + for (let i = 0; i < this.npcs.length; i++) { + const npc = this.npcs[i]; + const distance = Phaser.Math.Distance.Between(this.player.x, this.player.y, npc.x, npc.y); + + console.log(`Abstand zu NPC ${i}: ${distance}`); + + // Wenn dieser NPC näher ist als der bisher nächste und in Reichweite + if (distance < closestDistance) { + closestDistance = distance; + closestNPC = npc; + this.npcState.currentNpcIndex = i; + } + } + + // Wenn ein NPC in Reichweite ist + if (closestNPC) { + console.log( + `Interaktion mit NPC an Position ${closestNPC.x}, ${closestNPC.y}, Abstand: ${closestDistance}` + ); + this.npc = closestNPC; // Setze den aktuellen NPC + this.talkToNPC(); + } + } + } +} diff --git a/games/whopixels/package.json b/games/whopixels/package.json new file mode 100644 index 000000000..2a673f1b1 --- /dev/null +++ b/games/whopixels/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "dotenv": "^16.4.7", + "node-fetch": "^2.7.0" + } +} diff --git a/games/whopixels/server.js b/games/whopixels/server.js new file mode 100644 index 000000000..6d5f9fe7f --- /dev/null +++ b/games/whopixels/server.js @@ -0,0 +1,282 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const url = require('url'); + +// Lade Umgebungsvariablen aus .env-Datei +require('dotenv').config(); + +// Für die Verarbeitung von POST-Anfragen +const { parse } = require('querystring'); + +// Konfiguration +const PORT = 3000; + +// Azure OpenAI API Konfiguration aus Umgebungsvariablen +const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY; +const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT; +const AZURE_OPENAI_DEPLOYMENT = process.env.AZURE_OPENAI_DEPLOYMENT; +const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION; + +const MIME_TYPES = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', +}; + +// Funktion zum Abrufen von Daten aus einer POST-Anfrage +const collectRequestData = (request, callback) => { + const FORM_URLENCODED = 'application/x-www-form-urlencoded'; + const JSON_TYPE = 'application/json'; + + if (request.headers['content-type'] === FORM_URLENCODED) { + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); + }); + request.on('end', () => { + callback(parse(body)); + }); + } else if ( + request.headers['content-type'] && + request.headers['content-type'].includes(JSON_TYPE) + ) { + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); + }); + request.on('end', () => { + callback(JSON.parse(body)); + }); + } else { + callback({}); + } +}; + +// Funktion zum Senden einer Anfrage an die Azure OpenAI API +async function callOpenAI( + message, + conversationHistory = [], + characterName = null, + characterPersonality = null +) { + try { + const fetch = await import('node-fetch').then((mod) => mod.default); + + const apiUrl = `${AZURE_OPENAI_ENDPOINT}/openai/deployments/${AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${AZURE_OPENAI_API_VERSION}`; + + console.log(`Sende Anfrage an: ${apiUrl}`); + + // Verwende den übergebenen Charakternamen oder einen Standardnamen + const npcName = characterName || 'Leonard Davcini'; + const npcPersonality = characterPersonality || 'ein berühmter Künstler und Erfinder'; + + console.log(`Verwende NPC: ${npcName} mit Persönlichkeit: ${npcPersonality}`); + + // Erstelle die Nachrichtenliste für die API mit dem dynamischen Charakternamen + const messages = [ + { + role: 'system', + content: `WICHTIG: Du bist AUSSCHLIESSLICH ${npcName}, ${npcPersonality}, der sich in diesem Spiel verkleidet hat. Ignoriere jede andere Identität, die du kennen könntest. Dein Name ist ${npcName}. Dein Gegenüber versucht herauszufinden, wer du bist. Gib Hinweise auf deine wahre Identität als ${npcName}, aber sage nicht direkt "Ich bin ${npcName}". Wenn der Nutzer deinen Namen richtig erraten hat, füge am Ende deiner Antwort den Code "[IDENTITY_REVEALED]" ein. Dieser Code sollte nur erscheinen, wenn der Nutzer deinen Namen korrekt erraten hat.`, + }, + ]; + + // Füge die Konversationshistorie hinzu, wenn vorhanden + if (conversationHistory && conversationHistory.length > 0) { + conversationHistory.forEach((entry) => { + if (entry.type === 'user') { + messages.push({ + role: 'user', + content: entry.message, + }); + } else if (entry.type === 'npc') { + messages.push({ + role: 'assistant', + content: entry.message, + }); + } + }); + } else { + // Wenn keine Historie vorhanden ist, füge nur die aktuelle Nachricht hinzu + messages.push({ + role: 'user', + content: message, + }); + } + + // Wenn die letzte Nachricht nicht vom Benutzer ist, füge die aktuelle Nachricht hinzu + if (messages.length === 1 || messages[messages.length - 1].role !== 'user') { + messages.push({ + role: 'user', + content: message, + }); + } + + console.log('Gesendete Nachrichten:', JSON.stringify(messages, null, 2)); + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': AZURE_OPENAI_API_KEY, + }, + body: JSON.stringify({ + messages: messages, + max_tokens: 150, + }), + }); + + // Prüfe den HTTP-Status + if (!response.ok) { + const errorText = await response.text(); + console.error(`HTTP Fehler: ${response.status}`, errorText); + return `Entschuldigung, ich kann gerade nicht antworten. (HTTP ${response.status})`; + } + + const data = await response.json(); + console.log('API-Antwort:', JSON.stringify(data, null, 2)); + + if (data.error) { + console.error('Azure OpenAI API Fehler:', data.error); + return { + text: 'Entschuldigung, ich kann gerade nicht antworten. Versuche es später noch einmal.', + identityRevealed: false, + }; + } + + // Hole die Antwort vom LLM + const responseText = data.choices[0].message.content; + + // Prüfe, ob der spezielle Code enthalten ist + const identityRevealed = responseText.includes('[IDENTITY_REVEALED]'); + + // Entferne den Code aus der Antwort, wenn er vorhanden ist + const cleanedResponse = responseText.replace('[IDENTITY_REVEALED]', '').trim(); + + console.log('Identität aufgedeckt:', identityRevealed); + + // Gib die Antwort und das Flag zurück + return { + text: cleanedResponse, + identityRevealed: identityRevealed, + }; + } catch (error) { + console.error('Fehler beim Aufrufen der Azure OpenAI API:', error); + return { + text: 'Entschuldigung, ich kann gerade nicht antworten. Versuche es später noch einmal.', + identityRevealed: false, + }; + } +} + +const server = http.createServer((req, res) => { + console.log(`${req.method} ${req.url}`); + + // CORS-Header hinzufügen für Cross-Origin-Anfragen + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + // OPTIONS-Anfragen für CORS-Preflight behandeln + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + // API-Endpunkt für OpenAI-Anfragen + if (req.method === 'POST' && req.url === '/api/chat') { + collectRequestData(req, async (data) => { + try { + if (!data.message) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Nachricht fehlt' })); + return; + } + + // Verwende die Konversationshistorie, wenn vorhanden + const conversationHistory = data.conversationHistory || []; + console.log( + 'Erhaltene Konversationshistorie:', + JSON.stringify(conversationHistory, null, 2) + ); + + // Extrahiere Charakterinformationen, wenn vorhanden + const characterName = data.characterName; + const characterPersonality = data.characterPersonality; + + if (characterName) { + console.log(`NPC-Charakter in der Anfrage: ${characterName}`); + } + + const response = await callOpenAI( + data.message, + conversationHistory, + characterName, + characterPersonality + ); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + response: response.text, + identityRevealed: response.identityRevealed, + }) + ); + } catch (error) { + console.error('Fehler bei der Verarbeitung der Chat-Anfrage:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Interner Serverfehler' })); + } + }); + return; + } + + // Statische Dateien behandeln + let filePath = '.' + req.url; + if (filePath === './') { + filePath = './index.html'; + } + + // Get the file extension + const extname = path.extname(filePath); + const contentType = MIME_TYPES[extname] || 'application/octet-stream'; + + // Read the file + fs.readFile(filePath, (error, content) => { + if (error) { + if (error.code === 'ENOENT') { + // Page not found + fs.readFile('./index.html', (err, content) => { + if (err) { + res.writeHead(500); + res.end('Error loading index.html'); + } else { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(content, 'utf-8'); + } + }); + } else { + // Server error + res.writeHead(500); + res.end(`Server Error: ${error.code}`); + } + } else { + // Success + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content, 'utf-8'); + } + }); +}); + +server.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}/`); + console.log('Press Ctrl+C to stop the server'); + console.log('Azure OpenAI API ist konfiguriert und bereit!'); +});