mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
chore: remove whopixels — superseded by the who module
Standalone games/whopixels has been replaced by the who module that
landed in the previous four commits. The whopixels Phaser RPG world
wrapper around the chat (~80% of the source) was deliberately
dropped during the port; the chat loop, the 26 historical-figure
personalities, and the [IDENTITY_REVEALED] sentinel trick all live
on inside apps/api/src/modules/who/.
What's gone in this commit:
games/whopixels/ — 33 source files, ~3.6k LOC
Phaser scenes (Boot, MainMenu, Game, RPG)
Managers (Player, NPC, World, Touch, Sound, Storage, ChatUI)
Vanilla http server with hand-rolled rate limit + Azure OpenAI
Static assets, css, jsconfig
docker-compose.macmini.yml — `whopixels` service block
Build context, Azure OpenAI env wiring, healthcheck. Port 5100
is now free. Comment left in place explaining the migration so
a future reader doesn't wonder why this gap exists.
What still has to happen outside this PR (Mac Mini side):
- docker rm -f mana-game-whopixels
- cloudflared route for whopixels.mana.how needs a redirect or
archive (sub-domain stops resolving once the container is gone
unless DNS / tunnel routes are touched separately)
The migration is non-destructive in terms of data: whopixels stored
no per-user state — sessions were in-memory, conversation history
lived only in the browser tab. There's nothing to migrate.
Net delta of the entire who module migration (5 commits combined):
+1880 LOC (RFC + backend + module + UI + branding)
-3666 LOC (whopixels)
───────
-1786 LOC
Closes Phase A.6 of docs/WHO_MODULE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
65c4d935d5
commit
ef780cf069
34 changed files with 4 additions and 3666 deletions
|
|
@ -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:
|
||||
|
|
|
|||
27
games/whopixels/.gitignore
vendored
27
games/whopixels/.gitignore
vendored
|
|
@ -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/
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Background Image</title>
|
||||
<style>
|
||||
body { margin: 0; background: #222233; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" width="800" height="600"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Fill background
|
||||
ctx.fillStyle = '#222233';
|
||||
ctx.fillRect(0, 0, 800, 600);
|
||||
|
||||
// Add some pattern
|
||||
ctx.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;
|
||||
ctx.fillRect(x, y, size, size);
|
||||
}
|
||||
|
||||
// Instructions
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '20px Arial';
|
||||
ctx.fillText('Right-click and save this image as background.png', 200, 300);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Create Placeholder Images</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Creating placeholder images...</h1>
|
||||
<canvas id="backgroundCanvas" width="800" height="600" style="display: none;"></canvas>
|
||||
<canvas id="playerCanvas" width="32" height="32" style="display: none;"></canvas>
|
||||
<canvas id="tileCanvas" width="32" height="32" style="display: none;"></canvas>
|
||||
|
||||
<div id="downloadLinks"></div>
|
||||
|
||||
<script>
|
||||
// Create background image
|
||||
const bgCanvas = document.getElementById('backgroundCanvas');
|
||||
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
|
||||
const playerCanvas = document.getElementById('playerCanvas');
|
||||
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
|
||||
const tileCanvas = document.getElementById('tileCanvas');
|
||||
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);
|
||||
|
||||
// Create download links
|
||||
const downloadDiv = document.getElementById('downloadLinks');
|
||||
|
||||
function createDownloadLink(canvas, filename) {
|
||||
const link = document.createElement('a');
|
||||
link.download = filename;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.textContent = `Download ${filename}`;
|
||||
link.style.display = 'block';
|
||||
link.style.margin = '10px';
|
||||
downloadDiv.appendChild(link);
|
||||
|
||||
// Auto-click to download
|
||||
setTimeout(() => link.click(), 500);
|
||||
}
|
||||
|
||||
createDownloadLink(bgCanvas, 'background.png');
|
||||
createDownloadLink(playerCanvas, 'player.png');
|
||||
createDownloadLink(tileCanvas, 'tile.png');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Player Image</title>
|
||||
<style>
|
||||
body { margin: 0; background: #333; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
canvas { display: block; border: 1px solid #fff; background: #000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" width="32" height="32"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Draw player
|
||||
ctx.fillStyle = '#ff0000';
|
||||
ctx.fillRect(0, 0, 32, 32);
|
||||
ctx.fillStyle = '#ff5555';
|
||||
ctx.fillRect(8, 8, 16, 16);
|
||||
|
||||
// Instructions (shown in console)
|
||||
console.log('Right-click and save this image as player.png');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -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 = `
|
||||
<h1>Placeholder Images for WhoPixels</h1>
|
||||
<div>
|
||||
<h2>Background (800x600)</h2>
|
||||
<canvas id="bg" width="800" height="600" style="border:1px solid #000; max-width: 100%;"></canvas>
|
||||
<p id="bgData"></p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Player (32x32)</h2>
|
||||
<canvas id="player" width="32" height="32" style="border:1px solid #000;"></canvas>
|
||||
<p id="playerData"></p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Tile (32x32)</h2>
|
||||
<canvas id="tile" width="32" height="32" style="border:1px solid #000;"></canvas>
|
||||
<p id="tileData"></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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';
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tile Image</title>
|
||||
<style>
|
||||
body { margin: 0; background: #333; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
canvas { display: block; border: 1px solid #fff; background: #000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" width="32" height="32"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Draw tile
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, 32, 32);
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0.5, 0.5, 31, 31);
|
||||
|
||||
// Instructions (shown in console)
|
||||
console.log('Right-click and save this image as tile.png');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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!');
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WhoPixels - Pixel Game</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="game-container"></div>
|
||||
|
||||
<!-- Phaser Library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
|
||||
|
||||
<!-- Game Data -->
|
||||
<script src="data/npc_characters.js"></script>
|
||||
|
||||
<!-- Config -->
|
||||
<script src="js/config/constants.js"></script>
|
||||
<script src="js/config/i18n.js"></script>
|
||||
|
||||
<!-- Manager Classes -->
|
||||
<script src="js/managers/StorageManager.js"></script>
|
||||
<script src="js/managers/SoundManager.js"></script>
|
||||
<script src="js/managers/WorldManager.js"></script>
|
||||
<script src="js/managers/PlayerManager.js"></script>
|
||||
<script src="js/managers/NPCManager.js"></script>
|
||||
<script src="js/managers/ChatUI.js"></script>
|
||||
<script src="js/managers/TouchControls.js"></script>
|
||||
|
||||
<!-- Game Scenes -->
|
||||
<script src="js/scenes/BootScene.js"></script>
|
||||
<script src="js/scenes/MainMenuScene.js"></script>
|
||||
<script src="js/scenes/GameScene.js"></script>
|
||||
<script src="js/scenes/RPGScene.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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<typeof GAME_CONFIG>} */
|
||||
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',
|
||||
};
|
||||
|
|
@ -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<string, string>} [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();
|
||||
|
|
@ -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);
|
||||
|
|
@ -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<string, Phaser.GameObjects.GameObject>} */
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<number, number>} 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -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(', ')}`);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue