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)
+
+
+
+
+
+`;
+
+// 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!');
+});