From ef780cf069d80eb32627ab510317a317779b3669 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 13:11:36 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20remove=20whopixels=20=E2=80=94=20super?= =?UTF-8?q?seded=20by=20the=20who=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone games/whopixels has been replaced by the who module that landed in the previous four commits. The whopixels Phaser RPG world wrapper around the chat (~80% of the source) was deliberately dropped during the port; the chat loop, the 26 historical-figure personalities, and the [IDENTITY_REVEALED] sentinel trick all live on inside apps/api/src/modules/who/. What's gone in this commit: games/whopixels/ — 33 source files, ~3.6k LOC Phaser scenes (Boot, MainMenu, Game, RPG) Managers (Player, NPC, World, Touch, Sound, Storage, ChatUI) Vanilla http server with hand-rolled rate limit + Azure OpenAI Static assets, css, jsconfig docker-compose.macmini.yml — `whopixels` service block Build context, Azure OpenAI env wiring, healthcheck. Port 5100 is now free. Comment left in place explaining the migration so a future reader doesn't wonder why this gap exists. What still has to happen outside this PR (Mac Mini side): - docker rm -f mana-game-whopixels - cloudflared route for whopixels.mana.how needs a redirect or archive (sub-domain stops resolving once the container is gone unless DNS / tunnel routes are touched separately) The migration is non-destructive in terms of data: whopixels stored no per-user state — sessions were in-memory, conversation history lived only in the browser tab. There's nothing to migrate. Net delta of the entire who module migration (5 commits combined): +1880 LOC (RFC + backend + module + UI + branding) -3666 LOC (whopixels) ─────── -1786 LOC Closes Phase A.6 of docs/WHO_MODULE.md. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.macmini.yml | 26 +- games/whopixels/.gitignore | 27 - games/whopixels/Dockerfile | 13 - games/whopixels/IMPROVEMENTS.md | 42 -- games/whopixels/README.md | 74 --- games/whopixels/assets/background.html | 35 -- games/whopixels/assets/background.png | 1 - .../assets/create_placeholder_images.html | 68 --- games/whopixels/assets/player.html | 26 - games/whopixels/assets/player.png | 1 - games/whopixels/assets/simple_images.js | 54 -- games/whopixels/assets/tile.html | 27 - games/whopixels/assets/tile.png | 1 - games/whopixels/css/style.css | 14 - games/whopixels/data/npc_characters.js | 200 -------- games/whopixels/generate_assets.js | 47 -- games/whopixels/index.html | 38 -- games/whopixels/js/config/constants.js | 121 ----- games/whopixels/js/config/i18n.js | 183 ------- games/whopixels/js/main.js | 18 - games/whopixels/js/managers/ChatUI.js | 366 ------------- games/whopixels/js/managers/NPCManager.js | 442 ---------------- games/whopixels/js/managers/PlayerManager.js | 82 --- games/whopixels/js/managers/SoundManager.js | 101 ---- games/whopixels/js/managers/StorageManager.js | 92 ---- games/whopixels/js/managers/TouchControls.js | 165 ------ games/whopixels/js/managers/WorldManager.js | 81 --- games/whopixels/js/scenes/BootScene.js | 481 ------------------ games/whopixels/js/scenes/GameScene.js | 241 --------- games/whopixels/js/scenes/MainMenuScene.js | 130 ----- games/whopixels/js/scenes/RPGScene.js | 149 ------ games/whopixels/jsconfig.json | 13 - games/whopixels/package.json | 6 - games/whopixels/server.js | 305 ----------- 34 files changed, 4 insertions(+), 3666 deletions(-) delete mode 100644 games/whopixels/.gitignore delete mode 100644 games/whopixels/Dockerfile delete mode 100644 games/whopixels/IMPROVEMENTS.md delete mode 100644 games/whopixels/README.md delete mode 100644 games/whopixels/assets/background.html delete mode 100644 games/whopixels/assets/background.png delete mode 100644 games/whopixels/assets/create_placeholder_images.html delete mode 100644 games/whopixels/assets/player.html delete mode 100644 games/whopixels/assets/player.png delete mode 100644 games/whopixels/assets/simple_images.js delete mode 100644 games/whopixels/assets/tile.html delete mode 100644 games/whopixels/assets/tile.png delete mode 100644 games/whopixels/css/style.css delete mode 100644 games/whopixels/data/npc_characters.js delete mode 100644 games/whopixels/generate_assets.js delete mode 100644 games/whopixels/index.html delete mode 100644 games/whopixels/js/config/constants.js delete mode 100644 games/whopixels/js/config/i18n.js delete mode 100644 games/whopixels/js/main.js delete mode 100644 games/whopixels/js/managers/ChatUI.js delete mode 100644 games/whopixels/js/managers/NPCManager.js delete mode 100644 games/whopixels/js/managers/PlayerManager.js delete mode 100644 games/whopixels/js/managers/SoundManager.js delete mode 100644 games/whopixels/js/managers/StorageManager.js delete mode 100644 games/whopixels/js/managers/TouchControls.js delete mode 100644 games/whopixels/js/managers/WorldManager.js delete mode 100644 games/whopixels/js/scenes/BootScene.js delete mode 100644 games/whopixels/js/scenes/GameScene.js delete mode 100644 games/whopixels/js/scenes/MainMenuScene.js delete mode 100644 games/whopixels/js/scenes/RPGScene.js delete mode 100644 games/whopixels/jsconfig.json delete mode 100644 games/whopixels/package.json delete mode 100644 games/whopixels/server.js diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index ba11fb23c..3e3f20dcb 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -1444,28 +1444,10 @@ services: # ============================================ # Games # ============================================ - - whopixels: - build: - context: . - dockerfile: games/whopixels/Dockerfile - container_name: mana-game-whopixels - restart: unless-stopped - mem_limit: 128m - environment: - PORT: 5100 - AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY:-} - AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT:-} - AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT:-} - AZURE_OPENAI_API_VERSION: ${AZURE_OPENAI_API_VERSION:-} - ports: - - "5100:5100" - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:5100/"] - interval: 180s - timeout: 10s - retries: 3 - start_period: 45s + # whopixels was removed 2026-04-09 — its core mechanic (LLM-driven + # historical figure guessing) lives now as the `who` module inside + # apps/mana/apps/web. The standalone Phaser/Node container is gone; + # see docs/WHO_MODULE.md for the migration rationale. volumes: redis_data: diff --git a/games/whopixels/.gitignore b/games/whopixels/.gitignore deleted file mode 100644 index 4f6d6258f..000000000 --- a/games/whopixels/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# 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/Dockerfile b/games/whopixels/Dockerfile deleted file mode 100644 index 800b967d6..000000000 --- a/games/whopixels/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY games/whopixels/package.json ./ -RUN npm install --production - -COPY games/whopixels/ ./ - -ENV PORT=5100 -EXPOSE 5100 - -CMD ["node", "server.js"] diff --git a/games/whopixels/IMPROVEMENTS.md b/games/whopixels/IMPROVEMENTS.md deleted file mode 100644 index f0c2f05bf..000000000 --- a/games/whopixels/IMPROVEMENTS.md +++ /dev/null @@ -1,42 +0,0 @@ -# WhoPixels - Verbesserungen - -Übersicht aller Verbesserungen für das WhoPixels-Spiel. - -## Architektur & Code-Qualität - -- [x] **1. RPGScene.js aufteilen** — 1210 Zeilen → 5 Module: WorldManager, PlayerManager, NPCManager, ChatUI, RPGScene (Orchestrator) -- [x] **2. Doppelter Code entfernen** — `createTestNPC()` entfernt, war Duplikat von `spawnNewNPC()` -- [x] **3. Magic Numbers eliminieren** — `js/config/constants.js` mit `GAME_CONFIG`-Objekt erstellt -- [x] **4. TypeScript-Migration** — JSDoc-Typen + `jsconfig.json` für IDE-Type-Safety (kein Build-System nötig) - -## Gameplay & Features - -- [x] **5. Persistenz/Speichersystem** — `StorageManager` mit LocalStorage: entdeckte NPCs, Statistiken, Fortschritt -- [x] **6. Sound & Musik** — `SoundManager` mit Web Audio API: programmatische Sounds für Chat, Reveal, NPC-Spawn -- [x] **7. Mehr NPCs** — Von 10 auf 26 NPCs erweitert in 3 Kategorien: Erfinder, Wissenschaftler, Künstler & Denker -- [x] **8. Leaderboard/Punktesystem** — Statistiken im Hauptmenü (Entlarvt, Durchschn. Fragen, Beste Serie), Reset-Option -- [x] **9. Pixel-Editor Integration** — Avatar im Editor malen, speichern und als Spieler-Sprite im RPG verwenden - -## UX & Visuelles - -- [x] **10. Mobile-Unterstützung** — `TouchControls` mit virtuellem Joystick (links) und Interaktions-Button (rechts) -- [x] **11. Chat-UI verbessern** — Typing-Indicator, Chat-Historie (letzte 4 Nachrichten), bessere Anzeige -- [x] **12. Animations-Feedback** — Schwebendes Fragezeichen-Icon über NPCs in Interaktions-Reichweite -- [x] **13. Tutorial/Onboarding** — Overlay beim ersten Start mit Steuerungshinweisen (Desktop/Mobile) - -## Sicherheit & Backend - -- [x] **14. Rate Limiting** — 30 Requests/Minute pro IP, 429-Status bei Überschreitung -- [x] **15. Input-Sanitization** — Längenbegrenzung (2000 Zeichen), Control-Character-Entfernung, Typ-Validierung -- [x] **16. CORS einschränken** — Nur erlaubte Origins, konfigurierbar via `ALLOWED_ORIGINS` Env-Variable -- [x] **17. Retry-Logik & Timeouts** — 15s Timeout mit AbortController, saubere Fehlerbehandlung -- [x] **18. Conversation History begrenzen** — Max 20 Einträge, ältere werden abgeschnitten - -## Performance - -- [x] **19. Object Pooling** — Partikel-Pool einmalig erstellt, Emitter wird wiederverwendet statt neu erstellt -- [x] **20. Phaser-Version updaten** — Von 3.55.2 auf 3.80.1, Particle-API auf neue `add.particles(x, y, key, config)` Syntax migriert - -## Lokalisierung - -- [x] **21. i18n-Framework** — `I18N`-System mit Deutsch/Englisch, Sprach-Umschalter im Hauptmenü, alle Texte lokalisiert diff --git a/games/whopixels/README.md b/games/whopixels/README.md deleted file mode 100644 index 10f8c1648..000000000 --- a/games/whopixels/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# 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 deleted file mode 100644 index 4a058694c..000000000 --- a/games/whopixels/assets/background.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - Background Image - - - - - - - diff --git a/games/whopixels/assets/background.png b/games/whopixels/assets/background.png deleted file mode 100644 index 8b1378917..000000000 --- a/games/whopixels/assets/background.png +++ /dev/null @@ -1 +0,0 @@ - diff --git a/games/whopixels/assets/create_placeholder_images.html b/games/whopixels/assets/create_placeholder_images.html deleted file mode 100644 index b5246dc0f..000000000 --- a/games/whopixels/assets/create_placeholder_images.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - Create Placeholder Images - - -

Creating placeholder images...

- - - - - - - - - diff --git a/games/whopixels/assets/player.html b/games/whopixels/assets/player.html deleted file mode 100644 index 503243201..000000000 --- a/games/whopixels/assets/player.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - Player Image - - - - - - - diff --git a/games/whopixels/assets/player.png b/games/whopixels/assets/player.png deleted file mode 100644 index 8b1378917..000000000 --- a/games/whopixels/assets/player.png +++ /dev/null @@ -1 +0,0 @@ - diff --git a/games/whopixels/assets/simple_images.js b/games/whopixels/assets/simple_images.js deleted file mode 100644 index 3f0dd2330..000000000 --- a/games/whopixels/assets/simple_images.js +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index 27a846070..000000000 --- a/games/whopixels/assets/tile.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - Tile Image - - - - - - - diff --git a/games/whopixels/assets/tile.png b/games/whopixels/assets/tile.png deleted file mode 100644 index 8b1378917..000000000 --- a/games/whopixels/assets/tile.png +++ /dev/null @@ -1 +0,0 @@ - diff --git a/games/whopixels/css/style.css b/games/whopixels/css/style.css deleted file mode 100644 index 94e37d07b..000000000 --- a/games/whopixels/css/style.css +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 06a4081dc..000000000 --- a/games/whopixels/data/npc_characters.js +++ /dev/null @@ -1,200 +0,0 @@ -// Liste der NPC-Charaktere mit Namen und Persönlichkeiten -// Kategorien: Erfinder, Wissenschaftler, Künstler, Entdecker, Vordenker -const npcCharacters = [ - // === ERFINDER (IDs 1-10) === - { - 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.', - }, - - // === WISSENSCHAFTLER (IDs 11-18) === - { - id: 11, - name: 'Albert Einstein', - personality: - 'Ein genialer theoretischer Physiker mit einem verschmitzten Humor. Er spricht in Gleichnissen und Gedankenexperimenten. Er liebt es, scheinbar einfache Fragen zu stellen, die tiefgreifende Wahrheiten offenbaren.', - hint: 'Meine berühmteste Gleichung verbindet Masse und Energie mit der Lichtgeschwindigkeit.', - }, - { - id: 12, - name: 'Isaac Newton', - personality: - 'Ein brillanter, aber etwas mürrischer Naturphilosoph. Er spricht präzise und duldet keine Ungenauigkeiten. Er ist stolz auf seine Entdeckungen, kann aber nachtragend sein gegenüber Rivalen.', - hint: 'Ein fallender Apfel inspirierte mich zu einer Theorie, die das Universum erklärte.', - }, - { - id: 13, - name: 'Charles Darwin', - personality: - 'Ein geduldiger und detailverliebter Naturforscher. Er spricht bedächtig und untermauert jede Aussage mit Beobachtungen. Er ist bescheiden, aber überzeugt von seiner Theorie.', - hint: 'Meine Reise auf der Beagle zu den Galápagos-Inseln veränderte unser Verständnis des Lebens grundlegend.', - }, - { - id: 14, - name: 'Galileo Galilei', - personality: - 'Ein mutiger und streitbarer Wissenschaftler, der sich nicht scheut, Autoritäten herauszufordern. Er spricht leidenschaftlich über seine Beobachtungen und verteidigt die Wahrheit, auch wenn sie unpopulär ist.', - hint: 'Ich richtete mein Fernrohr zum Himmel und bewies, dass die Erde nicht der Mittelpunkt des Universums ist.', - }, - { - id: 15, - name: 'Rosalind Franklin', - personality: - 'Eine akribische und entschlossene Wissenschaftlerin. Sie spricht sachlich und direkt, mit wenig Geduld für Ungenauigkeiten. Sie ist brillant in der Kristallographie und Röntgenbeugung.', - hint: 'Mein Foto 51 war der Schlüssel zur Entschlüsselung der Doppelhelix-Struktur der DNA.', - }, - { - id: 16, - name: 'Stephen Hawking', - personality: - 'Ein humorvoller und tiefgründiger Kosmologe, der das Universum für alle verständlich macht. Er nutzt bildhafte Sprache und Witze, um komplexe Konzepte zu erklären.', - hint: 'Meine Forschung über Schwarze Löcher zeigte, dass sie nicht ganz so schwarz sind, wie man dachte.', - }, - { - id: 17, - name: 'Alexander von Humboldt', - personality: - 'Ein enthusiastischer Naturforscher und Weltreisender. Er spricht mit grenzenloser Begeisterung über die Natur, sieht alles als zusammenhängendes Ganzes und erzählt gern von seinen Expeditionen.', - hint: 'Meine Reisen durch Südamerika und meine Kosmos-Werke begründeten die moderne Geographie und Ökologie.', - }, - { - id: 18, - name: 'Lise Meitner', - personality: - 'Eine bescheidene aber brillante Physikerin. Sie spricht ruhig und bedacht, erklärt Kernphysik mit erstaunlicher Klarheit. Sie ist enttäuscht über fehlende Anerkennung, aber nie verbittert.', - hint: 'Ich erklärte die Kernspaltung und benannte sie, doch der Nobelpreis dafür ging an meinen Kollegen.', - }, - - // === KÜNSTLER & DENKER (IDs 19-26) === - { - id: 19, - name: 'Wolfgang Amadeus Mozart', - personality: - 'Ein lebhafter und verspielter Komponist mit unglaublichem Talent. Er spricht schnell und enthusiastisch, wechselt zwischen ernsthaften musikalischen Diskussionen und kindlichem Humor.', - hint: 'Ich komponierte meine erste Sinfonie mit acht Jahren und schrieb über 600 Werke in meinem kurzen Leben.', - }, - { - id: 20, - name: 'Frida Kahlo', - personality: - 'Eine leidenschaftliche und unbeugsame Künstlerin. Sie spricht direkt und emotional, mit einem starken Bezug zur mexikanischen Kultur. Ihr Schmerz und ihre Stärke durchdringen jedes Wort.', - hint: 'Meine Selbstporträts zeigen meinen Schmerz und meine Identität, und mein blaues Haus in Coyoacán ist heute ein Museum.', - }, - { - id: 21, - name: 'William Shakespeare', - personality: - 'Ein wortgewandter Dramatiker mit tiefem Verständnis der menschlichen Natur. Er spricht in eleganten Formulierungen und liebt Wortspiele. Er sieht die Welt als Bühne.', - hint: 'Meine Stücke werden seit über 400 Jahren aufgeführt und haben die englische Sprache mit zahllosen neuen Wörtern bereichert.', - }, - { - id: 22, - name: 'Cleopatra VII.', - personality: - 'Eine charismatische und kluge Herrscherin. Sie spricht mehrere Sprachen fließend und ist eine meisterhafte Diplomatin. Sie verbindet Intelligenz mit strategischem Denken.', - hint: 'Ich war die letzte Pharaonin Ägyptens und sprach neun Sprachen, um mein Reich durch Diplomatie zu schützen.', - }, - { - id: 23, - name: 'Ludwig van Beethoven', - personality: - 'Ein leidenschaftlicher und stürmischer Komponist. Er spricht intensiv und emotional, manchmal aufbrausend. Trotz seines Gehörverlusts komponierte er seine größten Werke.', - hint: 'Meine neunte Sinfonie schrieb ich, als ich bereits vollständig taub war, und sie enthält die berühmte Ode an die Freude.', - }, - { - id: 24, - name: 'Konfuzius', - personality: - 'Ein weiser und geduldiger Lehrer der chinesischen Philosophie. Er spricht in kurzen, bedeutungsvollen Sätzen und beantwortet Fragen oft mit Gegenfragen. Er betont Respekt, Bildung und moralisches Handeln.', - hint: 'Meine Lehren über Tugend und gesellschaftliche Harmonie prägen die chinesische Kultur seit über 2.500 Jahren.', - }, - { - id: 25, - name: 'Hypatia von Alexandria', - personality: - 'Eine brillante Mathematikerin und Philosophin der Spätantike. Sie spricht klar und lehrreich, mit der Autorität einer Gelehrten. Sie verteidigt die Vernunft gegen Fanatismus.', - hint: 'Ich war eine der ersten Mathematikerinnen der Geschichte und lehrte Astronomie im antiken Alexandria.', - }, - { - id: 26, - name: 'Nikola Kopernikus', - personality: - 'Ein nachdenklicher und vorsichtiger Gelehrter. Er spricht bedacht und diplomatisch, da seine Erkenntnisse die kirchliche Lehre infrage stellten. Er ist überzeugt von der Kraft der Beobachtung.', - hint: 'Mein heliozentrisches Weltbild stellte die Erde aus dem Zentrum des Universums und setzte die Sonne an ihre Stelle.', - }, -]; - -// 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 deleted file mode 100644 index 3772fdb30..000000000 --- a/games/whopixels/generate_assets.js +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 4d1f7d81b..000000000 --- a/games/whopixels/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - WhoPixels - Pixel Game - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/games/whopixels/js/config/constants.js b/games/whopixels/js/config/constants.js deleted file mode 100644 index 94a8c5abe..000000000 --- a/games/whopixels/js/config/constants.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @typedef {Object} NPCCharacter - * @property {number} id - * @property {string} name - * @property {string} personality - * @property {string} hint - */ - -/** - * @typedef {Object} NPCState - * @property {boolean} isInConversation - * @property {boolean} isWaitingForResponse - * @property {boolean} identityRevealed - * @property {number[]} discoveredNPCs - * @property {number} currentNpcIndex - */ - -/** - * @typedef {Object} ConversationEntry - * @property {'user'|'npc'} type - * @property {string} message - */ - -/** - * @typedef {Object} MapConfig - * @property {number} widthInPixels - * @property {number} heightInPixels - * @property {number} tileWidth - * @property {number} tileHeight - */ - -/** @type {Readonly} */ -const GAME_CONFIG = { - // Spielfeld - GRID_SIZE: 11, - TILE_SIZE: 40, - get MAP_WIDTH() { - return this.GRID_SIZE * this.TILE_SIZE; - }, - get MAP_HEIGHT() { - return this.GRID_SIZE * this.TILE_SIZE; - }, - - // Spieler - PLAYER_SCALE: 2.4, - PLAYER_SPEED: 160, - - // NPC - NPC_SCALE: 2.4, - NPC_SPEED: 50, - NPC_MOVE_INTERVAL: 3000, - NPC_MOVE_CHANCE: 0.3, - NPC_INTERACTION_DISTANCE: 100, - NPC_WALK_DURATION: 2000, - - // Tiles - TILE_SCALE: 2.0, - - // Chat-UI - CHAT_HEIGHT: 250, - CHAT_PADDING: 20, - CHAT_INPUT_HEIGHT: 40, - CHAT_SEND_BUTTON_WIDTH: 85, - - // Farben - COLORS: { - CHAT_BG: 0x1a1a2a, - CHAT_BG_ALPHA: 0.9, - CHAT_BORDER: 0x4a6fa5, - INPUT_BG: 0x2a2a3a, - SEND_BUTTON: 0x4a6fa5, - SEND_BUTTON_HOVER: 0x5a7fb5, - CLOSE_BUTTON: 0x8a4a4a, - CLOSE_BUTTON_HOVER: 0x9a5a5a, - NPC_ANONYMOUS_TINT: 0x000000, - REVEAL_FLASH: 0xffff00, - TEXT_WHITE: '#ffffff', - TEXT_PLACEHOLDER: '#bbbbbb', - TEXT_NPC_RESPONSE: '#e0e0ff', - TEXT_REVEALED: '#ffff00', - BACK_BUTTON_BG: '#4a4a4a', - BACK_BUTTON_HOVER: '#ff0', - }, - - // Schriftgrößen - FONTS: { - CHAT_TITLE: '18px', - CHAT_INPUT: '16px', - CHAT_RESPONSE: '16px', - NPC_LABEL: '10px', - NPC_LABEL_REVEALED: '12px', - BACK_BUTTON: '18px', - INSTRUCTIONS: '16px', - REVEAL_TEXT: '24px', - NEW_NPC_TEXT: '20px', - }, - - // Animationen - ANIMATIONS: { - REVEAL_FLASH_DURATION: 300, - REVEAL_TEXT_FADE_DURATION: 2000, - REVEAL_TEXT_DELAY: 3000, - NEW_NPC_SPAWN_DELAY: 1000, - NEW_NPC_TEXT_FADE_DURATION: 1500, - NEW_NPC_TEXT_DELAY: 2500, - PARTICLE_LIFETIME: 1000, - PARTICLE_STOP_DELAY: 2000, - INTERACTION_PROMPT_DURATION: 2000, - }, - - // Terrain-Verteilung - TERRAIN: { - WALL_MOSS_CHANCE: 0.3, - GRASS_CHANCE: 0.4, - GRASS_FLOWER_CHANCE: 0.7, - DIRT_CHANCE: 0.9, - }, - - // API - API_URL: 'http://localhost:3000/api/chat', -}; diff --git a/games/whopixels/js/config/i18n.js b/games/whopixels/js/config/i18n.js deleted file mode 100644 index 608812240..000000000 --- a/games/whopixels/js/config/i18n.js +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Einfaches i18n-System für WhoPixels. - * Unterstützt Deutsch und Englisch. - */ -const I18N = { - _currentLang: 'de', - - translations: { - de: { - // Hauptmenü - title: 'WhoPixels', - subtitle: 'Ein Pixel-Abenteuer', - startGame: 'RPG Spiel starten', - pixelEditor: 'Pixel Editor', - resetProgress: 'Fortschritt zurücksetzen', - progressReset: 'Fortschritt zurückgesetzt!', - statsRevealed: 'Entlarvt', - statsAvgGuesses: 'Durchschn. Fragen', - statsBestStreak: 'Beste Serie', - - // RPG Scene - backToMenu: 'Zurück zum Menü', - arrowKeysToMove: 'Pfeiltasten zum Bewegen', - pressEToTalk: 'Drücke E zum Sprechen', - - // Chat - chatTitle: 'Gespräch mit NPC', - chatWithUnknown: 'Gespräch mit Unbekanntem', - chatWith: 'Gespräch mit', - typePlaceholder: 'Tippe deine Nachricht hier ein...', - sending: 'Nachricht wird gesendet...', - send: 'Senden', - talkToNpc: 'Sprich mit dem NPC...', - riddleIntro: 'Verhüllt von Zeit,\nwer könnt es sein?', - errorNoResponse: 'Entschuldigung, ich habe dich nicht verstanden.', - errorCantRespond: 'Entschuldigung, ich kann gerade nicht antworten.', - you: 'Du', - unknown: '???', - - // NPC - anonymous: 'Anonym', - revealed: 'entlarvt', - youRevealed: 'Du hast {name} entlarvt!', - newNpcAppeared: 'Ein neuer geheimnisvoller NPC ist erschienen!', - saveLoaded: 'Spielstand geladen: {count} NPCs bereits entdeckt', - - // Tutorial - tutorialWelcome: 'Willkommen bei WhoPixels!', - tutorialDesc: - 'Geheimnisvolle Figuren betreten die Arena.\nFinde durch Fragen heraus, wer sie sind!', - tutorialControlsDesktop: 'Pfeiltasten = Bewegen\nE = Mit NPC sprechen', - tutorialControlsMobile: 'Joystick links = Bewegen\nButton rechts = Interagieren', - tutorialStart: 'Tippe oder drücke eine Taste zum Starten', - - // Pixel Editor - editorTitle: 'Pixel Editor', - back: 'Zurück', - clear: 'Löschen', - saveAsAvatar: 'Als Avatar', - load: 'Laden', - colors: 'Farben', - gridCleared: 'Grid gelöscht', - avatarSaved: 'Avatar gespeichert! Wird im RPG-Spiel verwendet.', - avatarLoaded: 'Avatar geladen!', - noAvatarFound: 'Kein gespeicherter Avatar gefunden', - saveError: 'Fehler beim Speichern!', - loadError: 'Fehler beim Laden!', - }, - - en: { - // Main Menu - title: 'WhoPixels', - subtitle: 'A Pixel Adventure', - startGame: 'Start RPG Game', - pixelEditor: 'Pixel Editor', - resetProgress: 'Reset Progress', - progressReset: 'Progress reset!', - statsRevealed: 'Revealed', - statsAvgGuesses: 'Avg. Questions', - statsBestStreak: 'Best Streak', - - // RPG Scene - backToMenu: 'Back to Menu', - arrowKeysToMove: 'Arrow keys to move', - pressEToTalk: 'Press E to talk', - - // Chat - chatTitle: 'Chat with NPC', - chatWithUnknown: 'Chat with Unknown', - chatWith: 'Chat with', - typePlaceholder: 'Type your message here...', - sending: 'Sending message...', - send: 'Send', - talkToNpc: 'Talk to the NPC...', - riddleIntro: 'Veiled by time,\nwho could it be?', - errorNoResponse: "Sorry, I didn't understand you.", - errorCantRespond: "Sorry, I can't respond right now.", - you: 'You', - unknown: '???', - - // NPC - anonymous: 'Anonymous', - revealed: 'revealed', - youRevealed: 'You revealed {name}!', - newNpcAppeared: 'A new mysterious NPC has appeared!', - saveLoaded: 'Save loaded: {count} NPCs already discovered', - - // Tutorial - tutorialWelcome: 'Welcome to WhoPixels!', - tutorialDesc: 'Mysterious figures enter the arena.\nAsk questions to find out who they are!', - tutorialControlsDesktop: 'Arrow keys = Move\nE = Talk to NPC', - tutorialControlsMobile: 'Left joystick = Move\nRight button = Interact', - tutorialStart: 'Tap or press any key to start', - - // Pixel Editor - editorTitle: 'Pixel Editor', - back: 'Back', - clear: 'Clear', - saveAsAvatar: 'Save Avatar', - load: 'Load', - colors: 'Colors', - gridCleared: 'Grid cleared', - avatarSaved: 'Avatar saved! Will be used in RPG game.', - avatarLoaded: 'Avatar loaded!', - noAvatarFound: 'No saved avatar found', - saveError: 'Error saving!', - loadError: 'Error loading!', - }, - }, - - /** - * Sprache wechseln - * @param {'de'|'en'} lang - */ - setLanguage(lang) { - if (this.translations[lang]) { - this._currentLang = lang; - localStorage.setItem('whopixels_lang', lang); - } - }, - - /** @returns {'de'|'en'} */ - getLanguage() { - return this._currentLang; - }, - - /** Sprache aus LocalStorage laden */ - init() { - const saved = localStorage.getItem('whopixels_lang'); - if (saved && this.translations[saved]) { - this._currentLang = saved; - } - }, - - /** - * Übersetzung abrufen - * @param {string} key - * @param {Record} [params] - Platzhalter ersetzen, z.B. {name: 'Tesla'} - * @returns {string} - */ - t(key, params) { - const lang = this.translations[this._currentLang]; - let text = lang[key] || this.translations.de[key] || key; - - if (params) { - Object.entries(params).forEach(([k, v]) => { - text = text.replace(`{${k}}`, v); - }); - } - - return text; - }, - - /** Sprache umschalten */ - toggle() { - const next = this._currentLang === 'de' ? 'en' : 'de'; - this.setLanguage(next); - return next; - }, -}; - -// Beim Laden initialisieren -I18N.init(); diff --git a/games/whopixels/js/main.js b/games/whopixels/js/main.js deleted file mode 100644 index abe936cde..000000000 --- a/games/whopixels/js/main.js +++ /dev/null @@ -1,18 +0,0 @@ -// 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/managers/ChatUI.js b/games/whopixels/js/managers/ChatUI.js deleted file mode 100644 index 49a9b017a..000000000 --- a/games/whopixels/js/managers/ChatUI.js +++ /dev/null @@ -1,366 +0,0 @@ -class ChatUI { - /** @param {RPGScene} scene */ - constructor(scene) { - this.scene = scene; - /** @type {string} */ - this.userInput = ''; - /** @type {string} */ - this.lastNpcResponse = ''; - /** @type {ConversationEntry[]} */ - this.conversationHistory = []; - - /** @type {Record} */ - this.elements = {}; - } - - create() { - const { CHAT_HEIGHT, CHAT_PADDING, CHAT_SEND_BUTTON_WIDTH, CHAT_INPUT_HEIGHT, COLORS, FONTS } = - GAME_CONFIG; - const width = this.scene.cameras.main.width; - const height = this.scene.cameras.main.height; - const chatWidth = width - CHAT_PADDING * 2; - const chatTop = height - CHAT_HEIGHT - CHAT_PADDING; - - // Chat-Hintergrund - this.elements.background = this.scene.add.graphics(); - this.elements.background.fillStyle(COLORS.CHAT_BG, COLORS.CHAT_BG_ALPHA); - this.elements.background.fillRoundedRect(CHAT_PADDING, chatTop, chatWidth, CHAT_HEIGHT, 10); - this.elements.background.lineStyle(2, COLORS.CHAT_BORDER, 1); - this.elements.background.strokeRoundedRect(CHAT_PADDING, chatTop, chatWidth, CHAT_HEIGHT, 10); - this.elements.background.setScrollFactor(0); - this.elements.background.setVisible(false); - - // Titel - this.elements.title = this.scene.add.text(width / 2, chatTop + 20, I18N.t('chatTitle'), { - fontSize: FONTS.CHAT_TITLE, - fontFamily: 'Arial', - fontStyle: 'bold', - fill: COLORS.TEXT_WHITE, - align: 'center', - }); - this.elements.title.setOrigin(0.5, 0.5); - this.elements.title.setScrollFactor(0); - this.elements.title.setVisible(false); - - // NPC-Antwortbereich - this.elements.response = this.scene.add.text(CHAT_PADDING + 15, chatTop + 50, '', { - fontSize: FONTS.CHAT_RESPONSE, - fontFamily: 'Arial', - fill: COLORS.TEXT_NPC_RESPONSE, - padding: { x: 10, y: 10 }, - wordWrap: { width: chatWidth - 50 }, - lineSpacing: 6, - }); - this.elements.response.setScrollFactor(0); - this.elements.response.setVisible(false); - - // Trennlinie - this.elements.divider = this.scene.add.graphics(); - this.elements.divider.lineStyle(1, COLORS.CHAT_BORDER, 0.8); - this.elements.divider.lineBetween( - CHAT_PADDING + 15, - height - 90, - width - CHAT_PADDING - 15, - height - 90 - ); - this.elements.divider.setScrollFactor(0); - this.elements.divider.setVisible(false); - - // Eingabefeld-Hintergrund - this.elements.inputBg = this.scene.add.graphics(); - this.elements.inputBg.fillStyle(COLORS.INPUT_BG, 1); - this.elements.inputBg.fillRoundedRect( - CHAT_PADDING + 15, - height - 70, - chatWidth - 230, - CHAT_INPUT_HEIGHT, - 5 - ); - this.elements.inputBg.setScrollFactor(0); - this.elements.inputBg.setVisible(false); - - // Eingabefeld-Text - this.elements.input = this.scene.add.text( - CHAT_PADDING + 25, - height - 65, - I18N.t('typePlaceholder'), - { - fontSize: FONTS.CHAT_INPUT, - fontFamily: 'Arial', - fill: COLORS.TEXT_PLACEHOLDER, - padding: { x: 5, y: 5 }, - } - ); - this.elements.input.setScrollFactor(0); - this.elements.input.setVisible(false); - - // Senden-Button - this._createSendButton(width, height, chatWidth); - - // Schließen-Button - this._createCloseButton(width, height, chatTop); - - // Tastatureingabe - this.scene.input.keyboard.on('keydown', (event) => this._handleKeyInput(event), this); - } - - _createSendButton(width, height, chatWidth) { - const { CHAT_PADDING, CHAT_SEND_BUTTON_WIDTH, CHAT_INPUT_HEIGHT, COLORS, FONTS } = GAME_CONFIG; - const btnX = width - CHAT_PADDING - CHAT_SEND_BUTTON_WIDTH - 15; - - this.elements.sendBg = this.scene.add.graphics(); - this.elements.sendBg.fillStyle(COLORS.SEND_BUTTON, 1); - this.elements.sendBg.fillRoundedRect( - btnX, - height - 70, - CHAT_SEND_BUTTON_WIDTH, - CHAT_INPUT_HEIGHT, - 5 - ); - this.elements.sendBg.setScrollFactor(0); - this.elements.sendBg.setVisible(false); - - this.elements.sendBtn = this.scene.add.text( - btnX + CHAT_SEND_BUTTON_WIDTH / 2, - height - 50, - I18N.t('send'), - { - fontSize: FONTS.CHAT_INPUT, - fontFamily: 'Arial', - fontStyle: 'bold', - fill: COLORS.TEXT_WHITE, - } - ); - this.elements.sendBtn.setOrigin(0.5, 0.5); - this.elements.sendBtn.setScrollFactor(0); - this.elements.sendBtn.setVisible(false); - this.elements.sendBtn.setInteractive({ useHandCursor: true }); - this.elements.sendBtn.on('pointerdown', () => this.sendMessage()); - - this.elements.sendBtn.on('pointerover', () => { - this.elements.sendBg.clear(); - this.elements.sendBg.fillStyle(COLORS.SEND_BUTTON_HOVER, 1); - this.elements.sendBg.fillRoundedRect( - btnX, - height - 70, - CHAT_SEND_BUTTON_WIDTH, - CHAT_INPUT_HEIGHT, - 5 - ); - }); - this.elements.sendBtn.on('pointerout', () => { - this.elements.sendBg.clear(); - this.elements.sendBg.fillStyle(COLORS.SEND_BUTTON, 1); - this.elements.sendBg.fillRoundedRect( - btnX, - height - 70, - CHAT_SEND_BUTTON_WIDTH, - CHAT_INPUT_HEIGHT, - 5 - ); - }); - } - - _createCloseButton(width, height, chatTop) { - const { CHAT_PADDING, COLORS } = GAME_CONFIG; - const size = 24; - const pad = 10; - const cx = width - CHAT_PADDING - pad; - const cy = chatTop + pad + size / 2; - - this.elements.closeBg = this.scene.add.graphics(); - this.elements.closeBg.fillStyle(COLORS.CLOSE_BUTTON, 0.7); - this.elements.closeBg.fillCircle(cx, cy, size / 2); - this.elements.closeBg.setScrollFactor(0); - this.elements.closeBg.setVisible(false); - - this.elements.closeIcon = this.scene.add.graphics(); - this.elements.closeIcon.lineStyle(3, 0xffffff, 1); - this.elements.closeIcon.lineBetween(cx - size / 3, cy - size / 6, cx + size / 3, cy + size / 6); - this.elements.closeIcon.lineBetween(cx + size / 3, cy - size / 6, cx - size / 3, cy + size / 6); - this.elements.closeIcon.setScrollFactor(0); - this.elements.closeIcon.setVisible(false); - - this.elements.closeHit = this.scene.add.rectangle(cx, cy, size * 1.5, size * 1.5); - this.elements.closeHit.setScrollFactor(0); - this.elements.closeHit.setVisible(false); - this.elements.closeHit.setInteractive({ useHandCursor: true }); - this.elements.closeHit.on('pointerdown', () => this.close()); - - this.elements.closeHit.on('pointerover', () => { - this.elements.closeBg.clear(); - this.elements.closeBg.fillStyle(COLORS.CLOSE_BUTTON_HOVER, 0.9); - this.elements.closeBg.fillCircle(cx, cy, (size / 2) * 1.1); - }); - this.elements.closeHit.on('pointerout', () => { - this.elements.closeBg.clear(); - this.elements.closeBg.fillStyle(COLORS.CLOSE_BUTTON, 0.7); - this.elements.closeBg.fillCircle(cx, cy, size / 2); - }); - } - - open() { - Object.values(this.elements).forEach((el) => el.setVisible(true)); - - this.userInput = ''; - this.elements.input.setText(I18N.t('typePlaceholder')); - this.elements.input.setStyle({ fill: GAME_CONFIG.COLORS.TEXT_PLACEHOLDER }); - this.elements.response.setText(this.lastNpcResponse || I18N.t('talkToNpc')); - this.elements.title.setText(I18N.t('chatWithUnknown')); - } - - close() { - Object.values(this.elements).forEach((el) => el.setVisible(false)); - - const npcManager = this.scene.npcManager; - npcManager.state.isInConversation = false; - npcManager.npcDialog.setVisible(false); - } - - _handleKeyInput(event) { - if (!this.elements.input.visible) return; - - if (event.keyCode === 13) { - this.sendMessage(); - return; - } - - if (event.keyCode === 27) { - this.close(); - return; - } - - if (event.keyCode === 8 && this.userInput.length > 0) { - this.userInput = this.userInput.slice(0, -1); - } else if (event.keyCode >= 32 && event.keyCode <= 126) { - this.userInput += event.key; - } - - const { COLORS } = GAME_CONFIG; - - if (this.userInput.length === 0) { - this.elements.input.setText(I18N.t('typePlaceholder')); - this.elements.input.setStyle({ fill: COLORS.TEXT_PLACEHOLDER }); - } else { - this.elements.input.setText(this.userInput); - this.elements.input.setStyle({ fill: COLORS.TEXT_WHITE }); - - this.scene.tweens.add({ - targets: this.elements.inputBg, - alpha: 0.7, - duration: 50, - yoyo: true, - ease: 'Power1', - }); - } - } - - async sendMessage() { - const npcManager = this.scene.npcManager; - if (this.userInput.length === 0 || npcManager.state.isWaitingForResponse) return; - - const message = this.userInput; - this.userInput = ''; - - this.conversationHistory.push({ type: 'user', message }); - npcManager.currentGuessCount++; - this.elements.input.setText(''); - npcManager.state.isWaitingForResponse = true; - - if (this.scene.sound_mgr) this.scene.sound_mgr.playMessageSend(); - - // Typing-Indicator anzeigen - this._updateChatDisplay(`${I18N.t('you')}: ${message}\n\n...`); - this._startTypingAnimation(); - - try { - const npc = npcManager.currentNpc; - const response = await fetch(GAME_CONFIG.API_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - message, - conversationHistory: this.conversationHistory, - characterName: npc ? npc.characterName : null, - characterPersonality: npc ? npc.characterPersonality : null, - }), - }); - - const data = await response.json(); - let npcResponse = I18N.t('errorNoResponse'); - - if (data.response) { - npcResponse = data.response; - this.conversationHistory.push({ type: 'npc', message: npcResponse }); - - if (this.scene.sound_mgr) this.scene.sound_mgr.playMessageReceive(); - - if (data.identityRevealed) { - console.log('Identität aufgedeckt!'); - npcManager.revealIdentity(); - - if (this.elements.title && this.elements.title.visible) { - this.elements.title.setText(`${I18N.t('chatWith')} ${npc.characterName}`); - } - } - } - - this.lastNpcResponse = npcResponse; - this._stopTypingAnimation(); - this._updateChatDisplay(); - } catch (error) { - console.error('Fehler beim Senden der Nachricht:', error); - const errorMsg = I18N.t('errorCantRespond'); - this.lastNpcResponse = errorMsg; - this.conversationHistory.push({ type: 'npc', message: errorMsg }); - this._stopTypingAnimation(); - this._updateChatDisplay(); - } - - npcManager.state.isWaitingForResponse = false; - this.elements.input.setText(I18N.t('typePlaceholder')); - this.elements.input.setStyle({ fill: GAME_CONFIG.COLORS.TEXT_PLACEHOLDER }); - } - - /** Zeigt die letzten Nachrichten der Konversation im Chat-Bereich */ - _updateChatDisplay(customText) { - if (!this.elements.response || !this.elements.response.visible) return; - - if (customText) { - this.elements.response.setText(customText); - return; - } - - // Zeige die letzten 3 Nachrichten - const recent = this.conversationHistory.slice(-4); - const lines = recent.map((entry) => { - const prefix = entry.type === 'user' ? I18N.t('you') : I18N.t('unknown'); - return `${prefix}: ${entry.message}`; - }); - - this.elements.response.setText(lines.join('\n\n')); - } - - _startTypingAnimation() { - this._typingDots = 0; - this._typingTimer = this.scene.time.addEvent({ - delay: 400, - callback: () => { - this._typingDots = (this._typingDots + 1) % 4; - const dots = '.'.repeat(this._typingDots || 1); - const lastUserMsg = this.conversationHistory.filter((e) => e.type === 'user').pop(); - if (lastUserMsg && this.elements.response.visible) { - this.elements.response.setText(`${I18N.t('you')}: ${lastUserMsg.message}\n\n${dots}`); - } - }, - loop: true, - }); - } - - _stopTypingAnimation() { - if (this._typingTimer) { - this._typingTimer.destroy(); - this._typingTimer = null; - } - } -} diff --git a/games/whopixels/js/managers/NPCManager.js b/games/whopixels/js/managers/NPCManager.js deleted file mode 100644 index ef743db3a..000000000 --- a/games/whopixels/js/managers/NPCManager.js +++ /dev/null @@ -1,442 +0,0 @@ -class NPCManager { - /** @param {RPGScene} scene */ - constructor(scene) { - this.scene = scene; - /** @type {Phaser.Physics.Arcade.Sprite[]} */ - this.npcs = []; - /** @type {Phaser.Physics.Arcade.Sprite & {characterId: number, characterName: string, characterPersonality: string, debugText: Phaser.GameObjects.Text}} */ - this.currentNpc = null; - /** @type {NPCCharacter[]} */ - this.npcCharacters = []; - /** @type {NPCState} */ - this.state = { - isInConversation: false, - isWaitingForResponse: false, - identityRevealed: false, - discoveredNPCs: [], - currentNpcIndex: -1, - }; - /** @type {number} Anzahl gesendeter Nachrichten für den aktuellen NPC */ - this.currentGuessCount = 0; - this.npcDialog = null; - this.interactionPrompt = null; - - // Partikel-Emitter (wiederverwendbar) - /** @type {Phaser.GameObjects.Particles.ParticleEmitter|null} */ - this._emitter = null; - } - - /** - * @param {Phaser.Physics.Arcade.Sprite} player - * @param {Phaser.Physics.Arcade.StaticGroup} obstacles - */ - create(player, obstacles) { - this.player = player; - this.obstacles = obstacles; - - // Lade NPC-Charaktere - this.npcCharacters = window.npcCharacters || []; - if (!this.npcCharacters || this.npcCharacters.length === 0) { - console.error('Keine NPC-Charaktere gefunden!'); - 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); - - // Dialog-Box - this.npcDialog = this.scene.add.text(0, 0, I18N.t('pressEToTalk'), { - fontSize: '12px', - fill: GAME_CONFIG.COLORS.TEXT_WHITE, - backgroundColor: '#000', - padding: { x: 5, y: 5 }, - wordWrap: { width: 200 }, - }); - this.npcDialog.setVisible(false); - - // Interaktions-Prompt - this.interactionPrompt = this.scene.add.text(0, 0, I18N.t('pressEToTalk'), { - fontSize: GAME_CONFIG.FONTS.NPC_LABEL, - fill: GAME_CONFIG.COLORS.TEXT_WHITE, - backgroundColor: '#000', - padding: { x: 3, y: 3 }, - }); - this.interactionPrompt.setVisible(false); - - // Partikel-Pool erstellen (einmalig) - this._initParticlePool(); - - this.spawnNewNPC(); - } - - _initParticlePool() { - // Phaser 3.60+ API: add.particles() gibt direkt einen ParticleEmitter zurück - this._emitter = this.scene.add.particles(0, 0, 'particle', { - speed: { min: 50, max: 100 }, - angle: { min: 0, max: 360 }, - scale: { start: 0.5, end: 0 }, - blendMode: 'ADD', - lifespan: GAME_CONFIG.ANIMATIONS.PARTICLE_LIFETIME, - gravityY: 0, - emitting: false, - }); - } - - spawnNewNPC() { - const { NPC_SCALE, TILE_SIZE, COLORS, FONTS, ANIMATIONS } = GAME_CONFIG; - - // Verfügbare Charaktere filtern - let availableCharacters = this.npcCharacters.filter( - (char) => !this.state.discoveredNPCs.includes(char.id) - ); - - if (this.currentNpc && this.currentNpc.characterId) { - availableCharacters = availableCharacters.filter( - (char) => char.id !== this.currentNpc.characterId - ); - } - - // Fallback - if (availableCharacters.length === 0) { - availableCharacters = this.npcCharacters.filter((char) => { - return !(this.currentNpc && this.currentNpc.characterId === char.id); - }); - if (availableCharacters.length === 0) return null; - } - - const selectedCharacter = - availableCharacters[Math.floor(Math.random() * availableCharacters.length)]; - console.log('Ausgewählter Charakter:', selectedCharacter.name); - - const map = this.scene.worldManager.map; - const doorX = Math.floor(map.widthInPixels / 2); - const doorY = TILE_SIZE; - - // NPC erstellen - const newNpc = this.scene.physics.add.sprite(doorX, doorY, 'npc_down'); - newNpc.setScale(NPC_SCALE); - newNpc.setTint(COLORS.NPC_ANONYMOUS_TINT); - - newNpc.characterId = selectedCharacter.id; - newNpc.characterName = selectedCharacter.name; - newNpc.characterPersonality = selectedCharacter.personality; - - // Einlauf-Animation - this.scene.tweens.add({ - targets: newNpc, - y: map.heightInPixels / 2, - duration: GAME_CONFIG.NPC_WALK_DURATION, - ease: 'Linear', - onUpdate: () => { - if (newNpc.debugText) { - newNpc.debugText.x = newNpc.x; - newNpc.debugText.y = newNpc.y + 20; - } - if (Math.floor(Date.now() / 150) % 2 === 0) { - newNpc.setTexture('npc_down'); - } else if (this.scene.textures.exists('npc_down_walk')) { - newNpc.setTexture('npc_down_walk'); - } - }, - }); - - // Name-Label - const debugText = this.scene.add.text(doorX, doorY + 20, I18N.t('anonymous'), { - fontSize: FONTS.NPC_LABEL, - fontFamily: 'Arial', - fill: COLORS.TEXT_WHITE, - stroke: '#000000', - strokeThickness: 2, - align: 'center', - }); - debugText.setOrigin(0.5, 0); - newNpc.debugText = debugText; - - // Kollisionen - if (this.obstacles) { - this.scene.physics.add.collider(newNpc, this.obstacles); - } - if (this.player) { - this.scene.physics.add.collider( - newNpc, - this.player, - () => this.showInteractionPrompt(), - null, - this - ); - } - - this.npcs.push(newNpc); - this.currentNpc = newNpc; - this.state.currentNpcIndex = this.npcs.length - 1; - this.currentGuessCount = 0; - - return newNpc; - } - - showInteractionPrompt() { - if (!this.currentNpc || !this.player) return; - - this.interactionPrompt.setPosition( - this.currentNpc.x - this.interactionPrompt.width / 2, - this.currentNpc.y - 40 - ); - this.interactionPrompt.setVisible(true); - - this.scene.time.delayedCall(GAME_CONFIG.ANIMATIONS.INTERACTION_PROMPT_DURATION, () => { - this.interactionPrompt.setVisible(false); - }); - } - - startConversation() { - if (!this.currentNpc || !this.player || this.state.isInConversation) return; - - this.player.setVelocity(0); - this.currentNpc.setVelocity(0); - - if (this.player.x < this.currentNpc.x) { - this.currentNpc.setTexture('npc_up'); - } else { - this.currentNpc.setTexture('npc_down'); - } - - this.state.isInConversation = true; - return true; - } - - moveRandomly() { - if (!this.currentNpc) return; - - this.currentNpc.setVelocity(0); - - if (Math.random() < GAME_CONFIG.NPC_MOVE_CHANCE) { - const speed = GAME_CONFIG.NPC_SPEED; - const direction = Math.floor(Math.random() * 4); - - switch (direction) { - case 0: - this.currentNpc.setVelocityY(-speed); - this.currentNpc.setTexture('npc_up'); - break; - case 1: - this.currentNpc.setVelocityX(speed); - this.currentNpc.setTexture('npc_down'); - break; - case 2: - this.currentNpc.setVelocityY(speed); - this.currentNpc.setTexture('npc_down'); - break; - case 3: - this.currentNpc.setVelocityX(-speed); - this.currentNpc.setTexture('npc_up'); - break; - } - - this.scene.time.delayedCall(1000 + Math.random() * 1000, () => { - if (this.currentNpc) this.currentNpc.setVelocity(0); - }); - } - } - - revealIdentity() { - const { COLORS, FONTS, ANIMATIONS } = GAME_CONFIG; - - this.state.identityRevealed = true; - - if (this.currentNpc && this.currentNpc.characterId) { - if (!this.state.discoveredNPCs.includes(this.currentNpc.characterId)) { - this.state.discoveredNPCs.push(this.currentNpc.characterId); - console.log(`NPC ${this.currentNpc.characterName} wurde entdeckt!`); - } - - // Fortschritt speichern - if (this.scene.storage) { - this.scene.storage.recordDiscovery(this.currentNpc.characterId, this.currentGuessCount); - } - } - - this.currentNpc.clearTint(); - - // Reveal-Sound abspielen - if (this.scene.sound_mgr) this.scene.sound_mgr.playReveal(); - - // Name-Label aktualisieren - if (this.currentNpc.debugText) { - this.currentNpc.debugText.setText(this.currentNpc.characterName); - this.currentNpc.debugText.setStyle({ - fontSize: FONTS.NPC_LABEL_REVEALED, - fontFamily: 'Arial', - fontStyle: 'bold', - fill: COLORS.TEXT_REVEALED, - stroke: '#000000', - strokeThickness: 3, - align: 'center', - }); - } - - // Gelber Blitz - this.currentNpc.setTint(COLORS.REVEAL_FLASH); - this.scene.time.delayedCall(ANIMATIONS.REVEAL_FLASH_DURATION, () => { - if (this.state.identityRevealed) { - this.currentNpc.clearTint(); - } - }); - - // Partikeleffekt (wiederverwendbarer Pool) - if (this._emitter) { - this._emitter.setPosition(this.currentNpc.x, this.currentNpc.y); - this._emitter.start(); - this.scene.time.delayedCall(ANIMATIONS.PARTICLE_STOP_DELAY, () => { - if (this._emitter) this._emitter.stop(); - }); - } - - // Enthüllungs-Text - const revealText = this.scene.add.text( - this.scene.cameras.main.width / 2, - this.scene.cameras.main.height / 3, - I18N.t('youRevealed', { name: this.currentNpc.characterName }), - { - fontSize: FONTS.REVEAL_TEXT, - fontFamily: 'Arial', - fontStyle: 'bold', - fill: COLORS.TEXT_REVEALED, - stroke: '#000000', - strokeThickness: 4, - align: 'center', - } - ); - revealText.setOrigin(0.5); - revealText.setScrollFactor(0); - - this.scene.tweens.add({ - targets: revealText, - alpha: 0, - duration: ANIMATIONS.REVEAL_TEXT_FADE_DURATION, - delay: ANIMATIONS.REVEAL_TEXT_DELAY, - onComplete: () => { - revealText.destroy(); - - this.scene.time.delayedCall(ANIMATIONS.NEW_NPC_SPAWN_DELAY, () => { - const newNpc = this.spawnNewNPC(); - if (newNpc) { - const newNpcText = this.scene.add.text( - this.scene.cameras.main.width / 2, - this.scene.cameras.main.height / 3, - I18N.t('newNpcAppeared'), - { - fontSize: FONTS.NEW_NPC_TEXT, - fontFamily: 'Arial', - fontStyle: 'bold', - fill: COLORS.TEXT_WHITE, - stroke: '#000000', - strokeThickness: 3, - align: 'center', - } - ); - newNpcText.setOrigin(0.5); - newNpcText.setScrollFactor(0); - - this.scene.tweens.add({ - targets: newNpcText, - alpha: 0, - duration: ANIMATIONS.NEW_NPC_TEXT_FADE_DURATION, - delay: ANIMATIONS.NEW_NPC_TEXT_DELAY, - onComplete: () => newNpcText.destroy(), - }); - } - }); - }, - }); - } - - /** - * @param {Phaser.Input.Keyboard.Key} interactKey - * @param {boolean} [touchInteract=false] - */ - checkInteraction(interactKey, touchInteract = false) { - const keyPressed = interactKey && Phaser.Input.Keyboard.JustDown(interactKey); - if ((keyPressed || touchInteract) && this.npcs.length > 0) { - let closestNPC = null; - let closestDistance = GAME_CONFIG.NPC_INTERACTION_DISTANCE; - - 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); - - if (distance < closestDistance) { - closestDistance = distance; - closestNPC = npc; - this.state.currentNpcIndex = i; - } - } - - if (closestNPC) { - this.currentNpc = closestNPC; - return this.startConversation(); - } - } - return false; - } - - update() { - if (this.npcDialog && this.npcDialog.visible && this.currentNpc) { - this.npcDialog.setPosition(this.currentNpc.x - 100, this.currentNpc.y - 50); - } - - if (this.interactionPrompt && this.interactionPrompt.visible && this.currentNpc) { - this.interactionPrompt.setPosition(this.currentNpc.x - 50, this.currentNpc.y - 30); - } - - // Fragezeichen-Icon über NPCs in Reichweite - this.npcs.forEach((npc) => { - if (npc.debugText) { - npc.debugText.setPosition(npc.x, npc.y + 20); - } - - if (this.player && !this.state.isInConversation) { - const distance = Phaser.Math.Distance.Between(this.player.x, this.player.y, npc.x, npc.y); - - if (distance < GAME_CONFIG.NPC_INTERACTION_DISTANCE) { - if (!npc.questionMark) { - npc.questionMark = this.scene.add.text(npc.x, npc.y - 35, '?', { - fontSize: '24px', - fontFamily: 'Arial', - fontStyle: 'bold', - fill: GAME_CONFIG.COLORS.TEXT_REVEALED, - stroke: '#000000', - strokeThickness: 3, - }); - npc.questionMark.setOrigin(0.5); - - // Schwebe-Animation - this.scene.tweens.add({ - targets: npc.questionMark, - y: npc.y - 45, - duration: 800, - yoyo: true, - repeat: -1, - ease: 'Sine.easeInOut', - }); - } - npc.questionMark.setPosition(npc.x, npc.questionMark.y); - } else if (npc.questionMark) { - npc.questionMark.destroy(); - npc.questionMark = null; - } - } - }); - } -} diff --git a/games/whopixels/js/managers/PlayerManager.js b/games/whopixels/js/managers/PlayerManager.js deleted file mode 100644 index 40e5ad7e7..000000000 --- a/games/whopixels/js/managers/PlayerManager.js +++ /dev/null @@ -1,82 +0,0 @@ -class PlayerManager { - /** @param {Phaser.Scene} scene */ - constructor(scene) { - this.scene = scene; - /** @type {Phaser.Physics.Arcade.Sprite} */ - this.player = null; - /** @type {Phaser.Types.Input.Keyboard.CursorKeys} */ - this.cursors = null; - } - - /** - * @param {MapConfig} map - * @param {Phaser.Physics.Arcade.StaticGroup} obstacles - */ - create(map, obstacles) { - const { PLAYER_SCALE } = GAME_CONFIG; - - // Custom-Avatar verwenden, falls vorhanden - this.useCustomAvatar = this.scene.textures.exists('custom_avatar_down'); - const initialTexture = this.useCustomAvatar ? 'custom_avatar_down' : 'player_down'; - - this.player = this.scene.physics.add.sprite( - map.widthInPixels / 2, - map.heightInPixels / 2, - initialTexture - ); - this.player.setScale(PLAYER_SCALE); - this.scene.physics.add.collider(this.player, obstacles); - this.player.setCollideWorldBounds(true); - - // Kamera einrichten - this.scene.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels); - this.scene.cameras.main.startFollow(this.player, true); - - // Steuerung - this.cursors = this.scene.input.keyboard.createCursorKeys(); - } - - /** @param {TouchControls} [touchControls] */ - handleMovement(touchControls) { - if (!this.player) return; - - const { PLAYER_SPEED } = GAME_CONFIG; - - // Touch-Input hat Priorität, dann Keyboard - const touchActive = touchControls && touchControls.isActive; - const touchDir = touchActive ? touchControls.direction : { x: 0, y: 0 }; - - const moveLeft = this.cursors.left.isDown || touchDir.x < -0.3; - const moveRight = this.cursors.right.isDown || touchDir.x > 0.3; - const moveUp = this.cursors.up.isDown || touchDir.y < -0.3; - const moveDown = this.cursors.down.isDown || touchDir.y > 0.3; - - const prefix = this.useCustomAvatar ? 'custom_avatar' : 'player'; - - // Horizontal - if (moveLeft) { - this.player.setVelocityX(-PLAYER_SPEED); - this.player.setTexture(`${prefix}_left`); - } else if (moveRight) { - this.player.setVelocityX(PLAYER_SPEED); - this.player.setTexture(`${prefix}_right`); - } else { - this.player.setVelocityX(0); - } - - // Vertikal - if (moveUp) { - this.player.setVelocityY(-PLAYER_SPEED); - if (!moveLeft && !moveRight) { - this.player.setTexture(`${prefix}_up`); - } - } else if (moveDown) { - this.player.setVelocityY(PLAYER_SPEED); - if (!moveLeft && !moveRight) { - this.player.setTexture(`${prefix}_down`); - } - } else { - this.player.setVelocityY(0); - } - } -} diff --git a/games/whopixels/js/managers/SoundManager.js b/games/whopixels/js/managers/SoundManager.js deleted file mode 100644 index 1e8ff8001..000000000 --- a/games/whopixels/js/managers/SoundManager.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Sound-Manager mit programmatisch generierten Sounds (keine externen Dateien nötig). - * Verwendet die Web Audio API für einfache Synthesizer-Sounds. - */ -class SoundManager { - constructor() { - /** @type {AudioContext|null} */ - this.ctx = null; - this.enabled = true; - this.volume = 0.3; - } - - /** AudioContext erst bei erster Nutzer-Interaktion erstellen (Browser-Policy) */ - _ensureContext() { - if (!this.ctx) { - this.ctx = new (window.AudioContext || window.webkitAudioContext)(); - } - if (this.ctx.state === 'suspended') { - this.ctx.resume(); - } - } - - /** - * Spielt einen Ton mit gegebener Frequenz und Dauer - * @param {number} frequency - Hz - * @param {number} duration - Sekunden - * @param {'sine'|'square'|'triangle'|'sawtooth'} type - * @param {number} [vol] - Lautstärke 0-1 - */ - _playTone(frequency, duration, type = 'sine', vol = this.volume) { - if (!this.enabled) return; - this._ensureContext(); - - const osc = this.ctx.createOscillator(); - const gain = this.ctx.createGain(); - - osc.type = type; - osc.frequency.setValueAtTime(frequency, this.ctx.currentTime); - - gain.gain.setValueAtTime(vol, this.ctx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); - - osc.connect(gain); - gain.connect(this.ctx.destination); - - osc.start(this.ctx.currentTime); - osc.stop(this.ctx.currentTime + duration); - } - - /** Gespräch starten */ - playConversationStart() { - this._playTone(440, 0.15, 'triangle', 0.2); - setTimeout(() => this._playTone(554, 0.15, 'triangle', 0.2), 100); - } - - /** Nachricht gesendet */ - playMessageSend() { - this._playTone(600, 0.08, 'sine', 0.15); - } - - /** NPC antwortet */ - playMessageReceive() { - this._playTone(400, 0.1, 'triangle', 0.15); - setTimeout(() => this._playTone(500, 0.1, 'triangle', 0.15), 80); - } - - /** Identität aufgedeckt — Fanfare */ - playReveal() { - const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6 - notes.forEach((freq, i) => { - setTimeout(() => this._playTone(freq, 0.3, 'triangle', 0.25), i * 150); - }); - } - - /** Neuer NPC erscheint */ - playNewNPC() { - this._playTone(330, 0.2, 'sine', 0.15); - setTimeout(() => this._playTone(392, 0.3, 'sine', 0.15), 150); - } - - /** Spieler bewegt sich (dezent) */ - playStep() { - this._playTone(100 + Math.random() * 50, 0.05, 'square', 0.05); - } - - /** Fehler/kann nicht interagieren */ - playError() { - this._playTone(200, 0.15, 'sawtooth', 0.1); - setTimeout(() => this._playTone(150, 0.2, 'sawtooth', 0.1), 100); - } - - toggle() { - this.enabled = !this.enabled; - return this.enabled; - } - - /** @param {number} vol 0-1 */ - setVolume(vol) { - this.volume = Math.max(0, Math.min(1, vol)); - } -} diff --git a/games/whopixels/js/managers/StorageManager.js b/games/whopixels/js/managers/StorageManager.js deleted file mode 100644 index 4353f9c41..000000000 --- a/games/whopixels/js/managers/StorageManager.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @typedef {Object} GameSaveData - * @property {number[]} discoveredNPCs - IDs der entdeckten NPCs - * @property {number} totalGuesses - Gesamtanzahl der Rateversuche - * @property {number} totalRevealed - Gesamtanzahl aufgedeckter NPCs - * @property {number} bestStreak - Beste Serie korrekt erratener NPCs - * @property {number} currentStreak - Aktuelle Serie - * @property {Record} guessesPerNPC - Anzahl Versuche pro NPC (ID -> Anzahl) - * @property {number} lastPlayed - Timestamp des letzten Spiels - */ - -class StorageManager { - constructor() { - this.STORAGE_KEY = 'whopixels_save'; - } - - /** @returns {GameSaveData} */ - load() { - try { - const saved = localStorage.getItem(this.STORAGE_KEY); - if (saved) { - return JSON.parse(saved); - } - } catch (error) { - console.error('Fehler beim Laden des Spielstands:', error); - } - return this._createDefault(); - } - - /** @param {GameSaveData} data */ - save(data) { - try { - data.lastPlayed = Date.now(); - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); - } catch (error) { - console.error('Fehler beim Speichern:', error); - } - } - - /** - * NPC als entdeckt markieren und Statistiken aktualisieren - * @param {number} npcId - * @param {number} guessCount - Anzahl der Fragen bis zur Enthüllung - */ - recordDiscovery(npcId, guessCount) { - const data = this.load(); - - if (!data.discoveredNPCs.includes(npcId)) { - data.discoveredNPCs.push(npcId); - } - - data.totalRevealed++; - data.totalGuesses += guessCount; - data.currentStreak++; - data.guessesPerNPC[npcId] = guessCount; - - if (data.currentStreak > data.bestStreak) { - data.bestStreak = data.currentStreak; - } - - this.save(data); - return data; - } - - /** @returns {GameSaveData} */ - _createDefault() { - return { - discoveredNPCs: [], - totalGuesses: 0, - totalRevealed: 0, - bestStreak: 0, - currentStreak: 0, - guessesPerNPC: {}, - lastPlayed: 0, - }; - } - - reset() { - localStorage.removeItem(this.STORAGE_KEY); - } - - /** @returns {{averageGuesses: number, totalRevealed: number, bestStreak: number}} */ - getStats() { - const data = this.load(); - return { - averageGuesses: - data.totalRevealed > 0 ? Math.round((data.totalGuesses / data.totalRevealed) * 10) / 10 : 0, - totalRevealed: data.totalRevealed, - bestStreak: data.bestStreak, - }; - } -} diff --git a/games/whopixels/js/managers/TouchControls.js b/games/whopixels/js/managers/TouchControls.js deleted file mode 100644 index a496ff268..000000000 --- a/games/whopixels/js/managers/TouchControls.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Touch-Controls für Mobile-Unterstützung. - * Zeigt einen virtuellen Joystick und einen Interaktions-Button. - */ -class TouchControls { - /** @param {Phaser.Scene} scene */ - constructor(scene) { - this.scene = scene; - this.isActive = false; - this.direction = { x: 0, y: 0 }; - this.interactPressed = false; - - // Joystick-Elemente - this.joystickBase = null; - this.joystickThumb = null; - this.interactButton = null; - - // Joystick-State - this.joystickPointer = null; - this.joystickCenter = { x: 0, y: 0 }; - } - - create() { - // Nur auf Touch-Geräten aktivieren - if (!this.scene.sys.game.device.input.touch) return; - - this.isActive = true; - const width = this.scene.cameras.main.width; - const height = this.scene.cameras.main.height; - - // Joystick-Base (links unten) - const joyX = 100; - const joyY = height - 100; - this.joystickCenter = { x: joyX, y: joyY }; - - this.joystickBase = this.scene.add.graphics(); - this.joystickBase.fillStyle(0xffffff, 0.2); - this.joystickBase.fillCircle(joyX, joyY, 60); - this.joystickBase.lineStyle(2, 0xffffff, 0.4); - this.joystickBase.strokeCircle(joyX, joyY, 60); - this.joystickBase.setScrollFactor(0); - this.joystickBase.setDepth(1000); - - this.joystickThumb = this.scene.add.graphics(); - this._drawThumb(joyX, joyY); - this.joystickThumb.setScrollFactor(0); - this.joystickThumb.setDepth(1001); - - // Interaktions-Button (rechts unten) - const btnX = width - 80; - const btnY = height - 100; - - this.interactButton = this.scene.add.graphics(); - this.interactButton.fillStyle(GAME_CONFIG.COLORS.CHAT_BORDER, 0.6); - this.interactButton.fillCircle(btnX, btnY, 35); - this.interactButton.setScrollFactor(0); - this.interactButton.setDepth(1000); - - const btnLabel = this.scene.add.text(btnX, btnY, 'E', { - fontSize: '28px', - fontFamily: 'Arial', - fontStyle: 'bold', - fill: GAME_CONFIG.COLORS.TEXT_WHITE, - }); - btnLabel.setOrigin(0.5); - btnLabel.setScrollFactor(0); - btnLabel.setDepth(1001); - - // Interaktiver Bereich für den Button - const btnHit = this.scene.add.circle(btnX, btnY, 40); - btnHit.setScrollFactor(0); - btnHit.setInteractive(); - btnHit.setAlpha(0.001); - btnHit.setDepth(1002); - - btnHit.on('pointerdown', () => { - this.interactPressed = true; - this.interactButton.clear(); - this.interactButton.fillStyle(GAME_CONFIG.COLORS.SEND_BUTTON_HOVER, 0.8); - this.interactButton.fillCircle(btnX, btnY, 35); - }); - - btnHit.on('pointerup', () => { - this.interactButton.clear(); - this.interactButton.fillStyle(GAME_CONFIG.COLORS.CHAT_BORDER, 0.6); - this.interactButton.fillCircle(btnX, btnY, 35); - }); - - // Joystick Touch-Handling - this.scene.input.on('pointerdown', (pointer) => this._onPointerDown(pointer)); - this.scene.input.on('pointermove', (pointer) => this._onPointerMove(pointer)); - this.scene.input.on('pointerup', (pointer) => this._onPointerUp(pointer)); - } - - /** @param {number} x @param {number} y */ - _drawThumb(x, y) { - this.joystickThumb.clear(); - this.joystickThumb.fillStyle(0xffffff, 0.5); - this.joystickThumb.fillCircle(x, y, 25); - } - - /** @param {Phaser.Input.Pointer} pointer */ - _onPointerDown(pointer) { - if (!this.isActive) return; - - // Nur linke Hälfte des Bildschirms für Joystick - if (pointer.x < this.scene.cameras.main.width / 2) { - const dist = Phaser.Math.Distance.Between( - pointer.x, - pointer.y, - this.joystickCenter.x, - this.joystickCenter.y - ); - if (dist < 80) { - this.joystickPointer = pointer; - } - } - } - - /** @param {Phaser.Input.Pointer} pointer */ - _onPointerMove(pointer) { - if (!this.isActive || !this.joystickPointer || pointer.id !== this.joystickPointer.id) return; - - const maxDist = 50; - const dx = pointer.x - this.joystickCenter.x; - const dy = pointer.y - this.joystickCenter.y; - const dist = Math.sqrt(dx * dx + dy * dy); - - let thumbX, thumbY; - if (dist > maxDist) { - thumbX = this.joystickCenter.x + (dx / dist) * maxDist; - thumbY = this.joystickCenter.y + (dy / dist) * maxDist; - } else { - thumbX = pointer.x; - thumbY = pointer.y; - } - - this._drawThumb(thumbX, thumbY); - - // Richtung normalisieren - const normDist = Math.min(dist, maxDist) / maxDist; - this.direction.x = (dx / (dist || 1)) * normDist; - this.direction.y = (dy / (dist || 1)) * normDist; - } - - /** @param {Phaser.Input.Pointer} pointer */ - _onPointerUp(pointer) { - if (!this.isActive) return; - - if (this.joystickPointer && pointer.id === this.joystickPointer.id) { - this.joystickPointer = null; - this.direction = { x: 0, y: 0 }; - this._drawThumb(this.joystickCenter.x, this.joystickCenter.y); - } - } - - /** @returns {boolean} Ob der Interact-Button gedrückt wurde (einmalig) */ - consumeInteract() { - if (this.interactPressed) { - this.interactPressed = false; - return true; - } - return false; - } -} diff --git a/games/whopixels/js/managers/WorldManager.js b/games/whopixels/js/managers/WorldManager.js deleted file mode 100644 index 34cf642cb..000000000 --- a/games/whopixels/js/managers/WorldManager.js +++ /dev/null @@ -1,81 +0,0 @@ -class WorldManager { - /** @param {Phaser.Scene} scene */ - constructor(scene) { - this.scene = scene; - /** @type {MapConfig} */ - this.map = null; - /** @type {Phaser.Physics.Arcade.StaticGroup} */ - this.obstacles = null; - } - - create() { - const { GRID_SIZE, TILE_SIZE, MAP_WIDTH, MAP_HEIGHT, TILE_SCALE, TERRAIN } = GAME_CONFIG; - - this.map = { - widthInPixels: MAP_WIDTH, - heightInPixels: MAP_HEIGHT, - tileWidth: TILE_SIZE, - tileHeight: TILE_SIZE, - }; - - // Hintergrund - this.scene.add - .tileSprite(0, 0, MAP_WIDTH, MAP_HEIGHT, 'background') - .setOrigin(0, 0) - .setScale(1.0); - - // Hindernisse - this.obstacles = this.scene.physics.add.staticGroup(); - - 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 }, - ]; - - for (let y = 0; y < GRID_SIZE; y++) { - for (let x = 0; x < GRID_SIZE; x++) { - let tileType; - - if (x === 0 || y === 0 || x === GRID_SIZE - 1 || y === GRID_SIZE - 1) { - if (y === 0 && x === Math.floor(GRID_SIZE / 2)) { - tileType = tileTypes[2]; // Tür - } else { - tileType = Math.random() < TERRAIN.WALL_MOSS_CHANCE ? tileTypes[5] : tileTypes[4]; - } - } else { - const rand = Math.random(); - if (rand < TERRAIN.GRASS_CHANCE) { - tileType = tileTypes[0]; - } else if (rand < TERRAIN.GRASS_FLOWER_CHANCE) { - tileType = tileTypes[1]; - } else if (rand < TERRAIN.DIRT_CHANCE) { - tileType = tileTypes[2]; - } else { - tileType = tileTypes[3]; - } - } - - const tile = this.scene.add.image( - x * TILE_SIZE + TILE_SIZE / 2, - y * TILE_SIZE + TILE_SIZE / 2, - tileType.key - ); - tile.setScale(TILE_SCALE); - - if (tileType.isObstacle) { - const obstacle = this.scene.add.rectangle( - x * TILE_SIZE + TILE_SIZE / 2, - y * TILE_SIZE + TILE_SIZE / 2, - TILE_SIZE * TILE_SCALE, - TILE_SIZE * TILE_SCALE - ); - this.obstacles.add(obstacle); - } - } - } - } -} diff --git a/games/whopixels/js/scenes/BootScene.js b/games/whopixels/js/scenes/BootScene.js deleted file mode 100644 index 7c22d2885..000000000 --- a/games/whopixels/js/scenes/BootScene.js +++ /dev/null @@ -1,481 +0,0 @@ -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(); - - // Lade gespeicherten Custom-Avatar, falls vorhanden - this.loadCustomAvatar(); - - this.scene.start('MainMenuScene'); - } - - loadCustomAvatar() { - try { - const saved = localStorage.getItem('whopixels_avatar'); - if (!saved) return; - - const avatarData = JSON.parse(saved); - const frameSize = 32; - const pixelSize = frameSize / avatarData.width; - - const graphics = this.make.graphics({ x: 0, y: 0 }); - - for (let y = 0; y < avatarData.height; y++) { - for (let x = 0; x < avatarData.width; x++) { - const color = avatarData.pixels[y][x]; - if (color !== 0xffffff) { - graphics.fillStyle(color); - graphics.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); - } - } - } - - graphics.generateTexture('custom_avatar_down', frameSize, frameSize); - graphics.generateTexture('custom_avatar_up', frameSize, frameSize); - graphics.generateTexture('custom_avatar_left', frameSize, frameSize); - graphics.generateTexture('custom_avatar_right', frameSize, frameSize); - graphics.destroy(); - - console.log('Custom-Avatar geladen'); - } catch (error) { - console.error('Fehler beim Laden des Custom-Avatars:', error); - } - } - - 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 deleted file mode 100644 index c0149cbd6..000000000 --- a/games/whopixels/js/scenes/GameScene.js +++ /dev/null @@ -1,241 +0,0 @@ -class GameScene extends Phaser.Scene { - constructor() { - super({ key: 'GameScene' }); - } - - create() { - const { COLORS } = GAME_CONFIG; - - this.add.image(400, 300, 'background'); - - // Grid erstellen (16x16 Grid mit 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; - - // Grid-Farben als 2D-Array speichern - this.gridColors = []; - - for (let y = 0; y < this.gridHeight; y++) { - this.grid[y] = []; - this.gridColors[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); - tile.setInteractive(); - tile.on('pointerdown', () => this.paintTile(x, y)); - tile.on('pointerover', (pointer) => { - if (pointer.isDown) this.paintTile(x, y); - }); - this.grid[y][x] = tile; - this.gridColors[y][x] = 0xffffff; - } - } - - this.currentColor = 0x000000; - - // Farbpalette - this.createColorPalette(); - - // Titel - this.add - .text(400, 30, I18N.t('editorTitle'), { fontSize: '32px', fill: COLORS.TEXT_WHITE }) - .setOrigin(0.5); - - // Buttons - this._createButton(80, 560, I18N.t('back'), () => this.scene.start('MainMenuScene')); - this._createButton(250, 560, I18N.t('clear'), () => this.clearGrid()); - this._createButton(420, 560, I18N.t('saveAsAvatar'), () => this.saveAsAvatar()); - this._createButton(600, 560, I18N.t('load'), () => this.loadAvatar()); - - // Status-Text - this.statusText = this.add - .text(400, 30 + 30, '', { - fontSize: '14px', - fontFamily: 'Arial', - fill: COLORS.TEXT_REVEALED, - align: 'center', - }) - .setOrigin(0.5); - } - - createColorPalette() { - const colors = [ - 0x000000, 0xffffff, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, 0xff8800, - 0x8800ff, 0x88ff00, 0x0088ff, 0xff4444, 0x44ff44, 0x4444ff, 0x888888, 0xffcc99, 0x663300, - 0x339933, 0x333366, - ]; - - const paletteX = 720; - const paletteStartY = 120; - const size = 25; - const gap = 5; - const cols = 2; - - this.add - .text(paletteX, paletteStartY - 25, 'Farben', { - fontSize: '14px', - fill: GAME_CONFIG.COLORS.TEXT_WHITE, - }) - .setOrigin(0.5); - - colors.forEach((color, index) => { - const col = index % cols; - const row = Math.floor(index / cols); - const x = paletteX - (cols * (size + gap)) / 2 + col * (size + gap) + size / 2; - const y = paletteStartY + row * (size + gap); - - const btn = this.add.rectangle(x, y, size, size, color); - btn.setInteractive({ useHandCursor: true }); - btn.setStrokeStyle(2, 0xffffff); - btn.on('pointerdown', () => { - this.currentColor = color; - // Highlight aktive Farbe - colors.forEach((_, i) => { - const el = this.children.list.find( - (c) => - c.type === 'Rectangle' && - c.x === paletteX - (cols * (size + gap)) / 2 + (i % cols) * (size + gap) + size / 2 && - c.fillColor === colors[i] - ); - if (el) el.setStrokeStyle(2, 0xffffff); - }); - btn.setStrokeStyle(3, GAME_CONFIG.COLORS.REVEAL_FLASH); - }); - }); - } - - paintTile(x, y) { - this.grid[y][x].setTint(this.currentColor); - this.gridColors[y][x] = this.currentColor; - } - - clearGrid() { - for (let y = 0; y < this.gridHeight; y++) { - for (let x = 0; x < this.gridWidth; x++) { - this.grid[y][x].setTint(0xffffff); - this.gridColors[y][x] = 0xffffff; - } - } - this._showStatus(I18N.t('gridCleared')); - } - - /** Speichert das aktuelle Pixel-Art als Avatar-Textur */ - saveAsAvatar() { - // Pixel-Daten speichern - const avatarData = { - width: this.gridWidth, - height: this.gridHeight, - pixels: this.gridColors, - }; - - try { - localStorage.setItem('whopixels_avatar', JSON.stringify(avatarData)); - - // Textur generieren (32x32 skaliert auf Spieler-Größe) - this._generateAvatarTexture(avatarData); - - this._showStatus(I18N.t('avatarSaved')); - } catch (error) { - console.error('Fehler beim Speichern des Avatars:', error); - this._showStatus(I18N.t('saveError')); - } - } - - /** Lädt einen gespeicherten Avatar in den Editor */ - loadAvatar() { - try { - const saved = localStorage.getItem('whopixels_avatar'); - if (!saved) { - this._showStatus(I18N.t('noAvatarFound')); - return; - } - - const avatarData = JSON.parse(saved); - for (let y = 0; y < Math.min(avatarData.height, this.gridHeight); y++) { - for (let x = 0; x < Math.min(avatarData.width, this.gridWidth); x++) { - const color = avatarData.pixels[y][x]; - this.grid[y][x].setTint(color); - this.gridColors[y][x] = color; - } - } - this._showStatus(I18N.t('avatarLoaded')); - } catch (error) { - console.error('Fehler beim Laden:', error); - this._showStatus(I18N.t('loadError')); - } - } - - _generateAvatarTexture(avatarData) { - const frameSize = 32; - const pixelSize = frameSize / avatarData.width; // 2px pro Pixel bei 16x16 - - const graphics = this.make.graphics({ x: 0, y: 0 }); - - for (let y = 0; y < avatarData.height; y++) { - for (let x = 0; x < avatarData.width; x++) { - const color = avatarData.pixels[y][x]; - if (color !== 0xffffff) { - // Weiß = transparent - graphics.fillStyle(color); - graphics.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); - } - } - } - - // Generiere Texturen für alle Richtungen - // (einfache Version: gleiche Textur für alle Richtungen) - if (this.textures.exists('custom_avatar_down')) { - this.textures.remove('custom_avatar_down'); - this.textures.remove('custom_avatar_up'); - this.textures.remove('custom_avatar_left'); - this.textures.remove('custom_avatar_right'); - } - - graphics.generateTexture('custom_avatar_down', frameSize, frameSize); - graphics.generateTexture('custom_avatar_up', frameSize, frameSize); - graphics.generateTexture('custom_avatar_left', frameSize, frameSize); - graphics.generateTexture('custom_avatar_right', frameSize, frameSize); - graphics.destroy(); - } - - /** @param {string} msg */ - _showStatus(msg) { - this.statusText.setText(msg); - this.tweens.add({ - targets: this.statusText, - alpha: 0, - duration: 1000, - delay: 2000, - onComplete: () => { - this.statusText.setAlpha(1); - this.statusText.setText(''); - }, - }); - } - - _createButton(x, y, label, onClick) { - const { COLORS } = GAME_CONFIG; - const btn = this.add - .text(x, y, label, { - fontSize: '20px', - fill: COLORS.TEXT_WHITE, - backgroundColor: COLORS.BACK_BUTTON_BG, - padding: { x: 12, y: 6 }, - }) - .setOrigin(0.5) - .setInteractive({ useHandCursor: true }); - - btn.on('pointerover', () => btn.setStyle({ fill: COLORS.BACK_BUTTON_HOVER })); - btn.on('pointerout', () => btn.setStyle({ fill: COLORS.TEXT_WHITE })); - btn.on('pointerdown', onClick); - return btn; - } -} diff --git a/games/whopixels/js/scenes/MainMenuScene.js b/games/whopixels/js/scenes/MainMenuScene.js deleted file mode 100644 index 6fb888366..000000000 --- a/games/whopixels/js/scenes/MainMenuScene.js +++ /dev/null @@ -1,130 +0,0 @@ -class MainMenuScene extends Phaser.Scene { - constructor() { - super({ key: 'MainMenuScene' }); - } - - create() { - const { COLORS } = GAME_CONFIG; - const centerX = this.cameras.main.width / 2; - - this.add.image(centerX, 300, 'background'); - - // Titel - this.add - .text(centerX, 100, I18N.t('title'), { - fontSize: '64px', - fill: COLORS.TEXT_WHITE, - fontStyle: 'bold', - }) - .setOrigin(0.5); - - this.add - .text(centerX, 160, I18N.t('subtitle'), { - fontSize: '32px', - fill: COLORS.TEXT_WHITE, - }) - .setOrigin(0.5); - - // Statistiken - this._showStats(centerX); - - // Buttons - this._createButton(centerX, 300, I18N.t('startGame'), () => { - this.scene.start('RPGScene'); - }); - - this._createButton(centerX, 370, I18N.t('pixelEditor'), () => { - this.scene.start('GameScene'); - }); - - this._createButton(centerX, 440, I18N.t('resetProgress'), () => { - new StorageManager().reset(); - this._showStats(centerX); - - const confirmText = this.add - .text(centerX, 500, I18N.t('progressReset'), { - fontSize: '18px', - fill: COLORS.TEXT_REVEALED, - fontFamily: 'Arial', - }) - .setOrigin(0.5); - - this.tweens.add({ - targets: confirmText, - alpha: 0, - duration: 1500, - delay: 1500, - onComplete: () => confirmText.destroy(), - }); - }); - - // Sprach-Umschalter (rechts oben) - const langBtn = this.add - .text(this.cameras.main.width - 20, 20, I18N.getLanguage().toUpperCase(), { - fontSize: '20px', - fontFamily: 'Arial', - fontStyle: 'bold', - fill: COLORS.TEXT_WHITE, - backgroundColor: COLORS.BACK_BUTTON_BG, - padding: { x: 10, y: 5 }, - }) - .setOrigin(1, 0) - .setInteractive({ useHandCursor: true }); - - langBtn.on('pointerover', () => langBtn.setStyle({ fill: COLORS.BACK_BUTTON_HOVER })); - langBtn.on('pointerout', () => langBtn.setStyle({ fill: COLORS.TEXT_WHITE })); - langBtn.on('pointerdown', () => { - const newLang = I18N.toggle(); - // Scene neu laden, um Texte zu aktualisieren - this.scene.restart(); - }); - } - - _showStats(centerX) { - const { COLORS } = GAME_CONFIG; - const storage = new StorageManager(); - const stats = storage.getStats(); - - if (this.statsText) this.statsText.destroy(); - - if (stats.totalRevealed > 0) { - this.statsText = this.add - .text( - centerX, - 220, - [ - `${I18N.t('statsRevealed')}: ${stats.totalRevealed}`, - `${I18N.t('statsAvgGuesses')}: ${stats.averageGuesses}`, - `${I18N.t('statsBestStreak')}: ${stats.bestStreak}`, - ].join(' | '), - { - fontSize: '16px', - fontFamily: 'Arial', - fill: COLORS.TEXT_NPC_RESPONSE, - align: 'center', - } - ) - .setOrigin(0.5); - } - } - - _createButton(x, y, label, onClick) { - const { COLORS } = GAME_CONFIG; - - const button = this.add - .text(x, y, label, { - fontSize: '28px', - fill: COLORS.TEXT_WHITE, - backgroundColor: COLORS.BACK_BUTTON_BG, - padding: { x: 20, y: 10 }, - }) - .setOrigin(0.5) - .setInteractive({ useHandCursor: true }); - - button.on('pointerover', () => button.setStyle({ fill: COLORS.BACK_BUTTON_HOVER })); - button.on('pointerout', () => button.setStyle({ fill: COLORS.TEXT_WHITE })); - button.on('pointerdown', onClick); - - return button; - } -} diff --git a/games/whopixels/js/scenes/RPGScene.js b/games/whopixels/js/scenes/RPGScene.js deleted file mode 100644 index fd4b0ca7c..000000000 --- a/games/whopixels/js/scenes/RPGScene.js +++ /dev/null @@ -1,149 +0,0 @@ -class RPGScene extends Phaser.Scene { - constructor() { - super({ key: 'RPGScene' }); - } - - preload() { - // Wir verwenden die in BootScene erstellten Texturen - } - - create() { - // Manager erstellen - this.storage = new StorageManager(); - this.sound_mgr = new SoundManager(); - this.worldManager = new WorldManager(this); - this.playerManager = new PlayerManager(this); - this.npcManager = new NPCManager(this); - this.chatUI = new ChatUI(this); - - // Gespeicherten Fortschritt laden - const saveData = this.storage.load(); - if (saveData.discoveredNPCs.length > 0) { - console.log(I18N.t('saveLoaded', { count: String(saveData.discoveredNPCs.length) })); - } - - // Spielwelt erstellen - this.worldManager.create(); - - // Spieler erstellen - this.playerManager.create(this.worldManager.map, this.worldManager.obstacles); - - // NPCs erstellen (mit gespeichertem Fortschritt) - this.npcManager.create(this.playerManager.player, this.worldManager.obstacles); - this.npcManager.state.discoveredNPCs = saveData.discoveredNPCs; - - // Touch-Controls (Mobile) - this.touchControls = new TouchControls(this); - this.touchControls.create(); - - // UI erstellen - this._createSceneUI(); - this.chatUI.create(); - - // Interaktions-Taste (E) - this.interactKey = this.input.keyboard.addKey('E'); - - // NPC-Bewegung - this.time.addEvent({ - delay: GAME_CONFIG.NPC_MOVE_INTERVAL, - callback: () => this.npcManager.moveRandomly(), - callbackScope: this, - loop: true, - }); - - // Tutorial beim ersten Spielstart - if (saveData.totalRevealed === 0) { - this._showTutorial(); - } - } - - _showTutorial() { - const { COLORS } = GAME_CONFIG; - const w = this.cameras.main.width; - const h = this.cameras.main.height; - - const overlay = this.add.graphics(); - overlay.fillStyle(0x000000, 0.7); - overlay.fillRect(0, 0, w, h); - overlay.setScrollFactor(0); - overlay.setDepth(2000); - - const isMobile = this.sys.game.device.input.touch; - const controlsText = isMobile - ? I18N.t('tutorialControlsMobile') - : I18N.t('tutorialControlsDesktop'); - - const tutorialText = this.add.text( - w / 2, - h / 2 - 40, - [ - I18N.t('tutorialWelcome'), - '', - I18N.t('tutorialDesc'), - '', - controlsText, - '', - I18N.t('tutorialStart'), - ].join('\n'), - { - fontSize: '18px', - fontFamily: 'Arial', - fill: COLORS.TEXT_WHITE, - align: 'center', - lineSpacing: 6, - } - ); - tutorialText.setOrigin(0.5); - tutorialText.setScrollFactor(0); - tutorialText.setDepth(2001); - - const closeTutorial = () => { - overlay.destroy(); - tutorialText.destroy(); - }; - - this.input.once('pointerdown', closeTutorial); - this.input.keyboard.once('keydown', closeTutorial); - } - - _createSceneUI() { - const { COLORS, FONTS } = GAME_CONFIG; - - const backButton = this.add - .text(10, 10, I18N.t('backToMenu'), { - fontSize: FONTS.BACK_BUTTON, - fill: COLORS.TEXT_WHITE, - backgroundColor: COLORS.BACK_BUTTON_BG, - padding: { x: 10, y: 5 }, - }) - .setScrollFactor(0) - .setInteractive(); - - backButton.on('pointerover', () => backButton.setStyle({ fill: COLORS.BACK_BUTTON_HOVER })); - backButton.on('pointerout', () => backButton.setStyle({ fill: COLORS.TEXT_WHITE })); - backButton.on('pointerdown', () => this.scene.start('MainMenuScene')); - - this.add - .text(10, 50, I18N.t('arrowKeysToMove'), { - fontSize: FONTS.INSTRUCTIONS, - fill: COLORS.TEXT_WHITE, - backgroundColor: 'rgba(0,0,0,0.5)', - padding: { x: 5, y: 2 }, - }) - .setScrollFactor(0); - } - - update() { - this.playerManager.handleMovement(this.touchControls); - - const touchInteract = this.touchControls.isActive && this.touchControls.consumeInteract(); - if (this.npcManager.checkInteraction(this.interactKey, touchInteract)) { - this.chatUI.lastNpcResponse = I18N.t('riddleIntro'); - this.chatUI.conversationHistory = []; - this.chatUI.open(); - this.sound_mgr.playConversationStart(); - } - - this.npcManager.update(); - } -} diff --git a/games/whopixels/jsconfig.json b/games/whopixels/jsconfig.json deleted file mode 100644 index 573be10fb..000000000 --- a/games/whopixels/jsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "checkJs": true, - "strict": false, - "target": "ES2020", - "module": "ES2020", - "moduleResolution": "node", - "lib": ["ES2020", "DOM"], - "typeRoots": ["./node_modules/@types"] - }, - "include": ["js/**/*.js", "data/**/*.js"], - "exclude": ["node_modules"] -} diff --git a/games/whopixels/package.json b/games/whopixels/package.json deleted file mode 100644 index 2a673f1b1..000000000 --- a/games/whopixels/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "dotenv": "^16.4.7", - "node-fetch": "^2.7.0" - } -} diff --git a/games/whopixels/server.js b/games/whopixels/server.js deleted file mode 100644 index 4239138a2..000000000 --- a/games/whopixels/server.js +++ /dev/null @@ -1,305 +0,0 @@ -const http = require('http'); -const fs = require('fs'); -const path = require('path'); - -require('dotenv').config(); - -// Konfiguration -const PORT = process.env.PORT || 3000; -const MAX_BODY_SIZE = 50 * 1024; // 50KB max request body -const MAX_CONVERSATION_HISTORY = 20; // Max Einträge in der Konversationshistorie -const RATE_LIMIT_WINDOW_MS = 60000; // 1 Minute -const RATE_LIMIT_MAX_REQUESTS = 30; // Max 30 Anfragen pro Minute - -// CORS — in Produktion einschränken -const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS - ? process.env.ALLOWED_ORIGINS.split(',') - : ['http://localhost:3000', 'http://localhost:5100']; - -// Azure OpenAI API Konfiguration -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', -}; - -// === Rate Limiting === -const rateLimitMap = new Map(); - -function isRateLimited(ip) { - const now = Date.now(); - const entry = rateLimitMap.get(ip); - - if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) { - rateLimitMap.set(ip, { windowStart: now, count: 1 }); - return false; - } - - entry.count++; - if (entry.count > RATE_LIMIT_MAX_REQUESTS) { - return true; - } - return false; -} - -// Cleanup alte Einträge alle 5 Minuten -setInterval(() => { - const now = Date.now(); - for (const [ip, entry] of rateLimitMap) { - if (now - entry.windowStart > RATE_LIMIT_WINDOW_MS * 2) { - rateLimitMap.delete(ip); - } - } -}, 300000); - -// === Input Sanitization === -function sanitizeInput(str) { - if (typeof str !== 'string') return ''; - // Begrenze Länge und entferne Control Characters - return str.slice(0, 2000).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); -} - -// === Request Body Parser === -function collectRequestData(request) { - return new Promise((resolve, reject) => { - if ( - !request.headers['content-type'] || - !request.headers['content-type'].includes('application/json') - ) { - resolve({}); - return; - } - - let body = ''; - let size = 0; - - request.on('data', (chunk) => { - size += chunk.length; - if (size > MAX_BODY_SIZE) { - request.destroy(); - reject(new Error('Request body too large')); - return; - } - body += chunk.toString(); - }); - - request.on('end', () => { - try { - resolve(JSON.parse(body)); - } catch { - reject(new Error('Invalid JSON')); - } - }); - - request.on('error', reject); - }); -} - -// === Azure OpenAI API === -async function callOpenAI( - message, - conversationHistory = [], - characterName = null, - characterPersonality = null -) { - 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}`; - - const npcName = characterName || 'Leonardo da Vinci'; - const npcPersonality = characterPersonality || 'ein berühmter Künstler und Erfinder'; - - 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.`, - }, - ]; - - // Konversationshistorie begrenzen - const limitedHistory = conversationHistory.slice(-MAX_CONVERSATION_HISTORY); - - if (limitedHistory.length > 0) { - limitedHistory.forEach((entry) => { - if (entry.type === 'user') { - messages.push({ role: 'user', content: sanitizeInput(entry.message) }); - } else if (entry.type === 'npc') { - messages.push({ role: 'assistant', content: entry.message }); - } - }); - } else { - messages.push({ role: 'user', content: sanitizeInput(message) }); - } - - if (messages.length === 1 || messages[messages.length - 1].role !== 'user') { - messages.push({ role: 'user', content: sanitizeInput(message) }); - } - - // Timeout für API-Call - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 15000); // 15s Timeout - - try { - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'api-key': AZURE_OPENAI_API_KEY, - }, - body: JSON.stringify({ messages, max_tokens: 150 }), - signal: controller.signal, - }); - - clearTimeout(timeout); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`HTTP Fehler: ${response.status}`, errorText); - return { text: 'Entschuldigung, ich kann gerade nicht antworten.', identityRevealed: false }; - } - - const data = await response.json(); - - if (data.error) { - console.error('Azure OpenAI API Fehler:', data.error); - return { text: 'Entschuldigung, ich kann gerade nicht antworten.', identityRevealed: false }; - } - - const responseText = data.choices[0].message.content; - const identityRevealed = responseText.includes('[IDENTITY_REVEALED]'); - const cleanedResponse = responseText.replace('[IDENTITY_REVEALED]', '').trim(); - - return { text: cleanedResponse, identityRevealed }; - } catch (error) { - clearTimeout(timeout); - if (error.name === 'AbortError') { - console.error('API-Timeout nach 15 Sekunden'); - return { - text: 'Entschuldigung, die Antwort hat zu lange gedauert.', - identityRevealed: false, - }; - } - console.error('Fehler beim Aufrufen der Azure OpenAI API:', error.message); - return { text: 'Entschuldigung, ich kann gerade nicht antworten.', identityRevealed: false }; - } -} - -// === HTTP Server === -const server = http.createServer(async (req, res) => { - const clientIP = req.socket.remoteAddress; - - // CORS-Header - const origin = req.headers.origin; - if (origin && ALLOWED_ORIGINS.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - } else if (!origin) { - // Same-origin Requests haben keinen Origin-Header - res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGINS[0]); - } - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(204); - res.end(); - return; - } - - // API-Endpunkt - if (req.method === 'POST' && req.url === '/api/chat') { - // Rate Limiting - if (isRateLimited(clientIP)) { - res.writeHead(429, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Zu viele Anfragen. Bitte warte einen Moment.' })); - return; - } - - try { - const data = await collectRequestData(req); - - if (!data.message || typeof data.message !== 'string') { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Nachricht fehlt oder ungültig' })); - return; - } - - const conversationHistory = Array.isArray(data.conversationHistory) - ? data.conversationHistory - : []; - - const response = await callOpenAI( - data.message, - conversationHistory, - typeof data.characterName === 'string' ? data.characterName : null, - typeof data.characterPersonality === 'string' ? data.characterPersonality : null - ); - - 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:', error.message); - const statusCode = error.message === 'Request body too large' ? 413 : 400; - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: error.message })); - } - return; - } - - // Statische Dateien - let filePath = '.' + req.url; - if (filePath === './') filePath = './index.html'; - - // Path Traversal verhindern - const resolvedPath = path.resolve(filePath); - if (!resolvedPath.startsWith(path.resolve('.'))) { - res.writeHead(403); - res.end('Forbidden'); - return; - } - - const extname = path.extname(filePath); - const contentType = MIME_TYPES[extname] || 'application/octet-stream'; - - fs.readFile(filePath, (error, content) => { - if (error) { - if (error.code === 'ENOENT') { - fs.readFile('./index.html', (err, fallback) => { - if (err) { - res.writeHead(500); - res.end('Error loading index.html'); - } else { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(fallback, 'utf-8'); - } - }); - } else { - res.writeHead(500); - res.end(`Server Error: ${error.code}`); - } - } else { - res.writeHead(200, { 'Content-Type': contentType }); - res.end(content, 'utf-8'); - } - }); -}); - -server.listen(PORT, () => { - console.log(`Server running at http://localhost:${PORT}/`); - console.log( - `Rate Limit: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 1000}s` - ); - console.log(`CORS: ${ALLOWED_ORIGINS.join(', ')}`); -});