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)
-
-
-
-
-
-`;
-
-// 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(', ')}`);
-});