managarten/games/whopixels/js/scenes/GameScene.js
Till JS c0c11c325a feat(whopixels): major refactor with 20 improvements across architecture, gameplay, UX, security, and i18n
Split monolithic RPGScene.js (1210 lines) into modular manager classes:
- WorldManager, PlayerManager, NPCManager, ChatUI, StorageManager,
  SoundManager, TouchControls

Key improvements:
- Constants config (GAME_CONFIG) replacing all magic numbers
- JSDoc types + jsconfig.json for IDE type-safety
- LocalStorage persistence for progress, stats, and custom avatars
- Synthesized sound effects via Web Audio API
- 26 NPCs (up from 10) in 3 categories
- Stats/leaderboard in main menu
- Pixel editor avatar integration with RPG game
- Mobile touch controls (virtual joystick + interact button)
- Chat UI with typing indicator and conversation history
- Interactive tutorial overlay for first-time players
- Floating question mark over NPCs in range
- Server hardened: rate limiting, input sanitization, CORS restrictions,
  API timeouts, conversation history cap
- Particle effect object pooling
- i18n framework with DE/EN and language switcher

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:26:40 +01:00

241 lines
6.7 KiB
JavaScript

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;
}
}