/** * Einfache Informationsseiten im Canvas: Ehrentafel, Steuerung, Credits. * Alle kehren mit Esc, Enter oder Klick auf „Zurück“ ins Hauptmenü zurück. * @module scenes/InfoScenes */ import { ORBITAL } from '../config.js'; import { CONTROLS, CREDITS, HIGHSCORES } from '../content/texts.js'; import { formatScore, formatSequence } from '../logic/format.js'; import { oncePerEvent } from '../systems/keyboard.js'; import { textStyle, UI, uiStyle } from '../ui/theme.js'; /** * Gemeinsamer Rahmen: Hintergrund, Titel und Zurück-Schaltfläche. * @param {Phaser.Scene} scene * @param {string} title * @param {string} backLabel */ function frame(scene, title, backLabel) { const { width, height } = scene.scale; scene.input.setDefaultCursor('default'); const bg = scene.add.graphics(); bg.fillGradientStyle(0x0d1321, 0x0d1321, 0x1b2438, 0x1b2438, 1); bg.fillRect(0, 0, width, height); bg.lineStyle(3, 0xf2c14e, 1); bg.strokeRect(40, 30, width - 80, height - 60); scene.add.text(width / 2, 70, title, textStyle({ fontSize: '38px', color: UI.colors.gold })).setOrigin(0.5); const back = scene.add .text(width / 2, height - 70, backLabel, textStyle({ fontSize: '22px' })) .setOrigin(0.5) .setInteractive({ useHandCursor: true }); back.on('pointerover', () => back.setColor(UI.colors.gold)); back.on('pointerout', () => back.setColor(UI.colors.cream)); const leave = () => scene.scene.start('Menu'); back.on('pointerup', leave); const leaveByKey = oncePerEvent((event) => { if (!event.repeat) leave(); }); scene.input.keyboard.on('keydown-ESC', leaveByKey); scene.input.keyboard.on('keydown-ENTER', leaveByKey); } export class HighscoresScene extends Phaser.Scene { constructor() { super('Highscores'); } create() { frame(this, HIGHSCORES.title, HIGHSCORES.back); const { width } = this.scale; this.statusText = this.add.text(width / 2, 300, HIGHSCORES.loading, uiStyle({ fontSize: '22px', color: UI.colors.muted })).setOrigin(0.5); this.registry .get('services') .api.fetchHighscores() .then((result) => { if (!this.sys.isActive()) return; if (!result.ok) this.statusText.setText(HIGHSCORES.error); else if (result.highscores.length === 0) this.statusText.setText(HIGHSCORES.empty); else { this.statusText.destroy(); this.renderTable(result.highscores); } }); } /** @param {import('../systems/ApiClient.js').HighscoreEntry[]} entries */ renderTable(entries) { const columns = { rank: 280, name: 360, score: 900, wave: 1000 }; const top = 130; const rowHeight = 40; const header = uiStyle({ fontSize: '17px', color: UI.colors.muted }); this.add.text(columns.rank, top, HIGHSCORES.columns.rank, header); this.add.text(columns.name, top, HIGHSCORES.columns.name, header); this.add.text(columns.score, top, HIGHSCORES.columns.score, header).setOrigin(1, 0); this.add.text(columns.wave, top, HIGHSCORES.columns.wave, header).setOrigin(1, 0); entries.forEach((entry, i) => { const y = top + 40 + i * rowHeight; const color = i === 0 ? UI.colors.gold : UI.colors.cream; const style = uiStyle({ fontSize: '24px', color }); this.add.text(columns.rank, y, `${entry.rank}.`, style); // Canvas-Text interpretiert kein HTML – Namen werden hier sicher als Text gezeichnet. this.add.text(columns.name, y, entry.name, style); this.add.text(columns.score, y, formatScore(entry.score), style).setOrigin(1, 0); this.add.text(columns.wave, y, String(entry.wave), style).setOrigin(1, 0); }); } } export class ControlsScene extends Phaser.Scene { constructor() { super('Controls'); } create() { frame(this, CONTROLS.title, CONTROLS.back); const keyStyle = uiStyle({ fontSize: '20px', color: UI.colors.gold }); const valueStyle = uiStyle({ fontSize: '20px' }); CONTROLS.rows.forEach(([key, action], i) => { const y = 130 + i * 38; this.add.text(560, y, key, keyStyle).setOrigin(1, 0); this.add.text(590, y, action, valueStyle); }); const top = 130 + CONTROLS.rows.length * 38 + 30; this.add.text(this.scale.width / 2, top, CONTROLS.orbitalTitle, textStyle({ fontSize: '22px', color: UI.colors.gold })).setOrigin(0.5); ORBITAL.commands.forEach((command, i) => { const y = top + 40 + i * 36; this.add.text(560, y, command.name, valueStyle).setOrigin(1, 0); this.add.text(590, y, formatSequence(command.sequence, ' '), { ...keyStyle, fontFamily: UI.symbolFont }); }); } } export class CreditsScene extends Phaser.Scene { constructor() { super('Credits'); } create() { frame(this, CREDITS.title, CREDITS.back); const { width } = this.scale; let y = 118; for (const section of CREDITS.sections) { const heading = this.add.text(width / 2, y, section.heading, textStyle({ fontSize: '21px', color: UI.colors.gold })).setOrigin(0.5, 0); y += heading.height + 4; for (const line of section.lines) { const text = this.add .text(width / 2, y, line, { ...uiStyle({ fontSize: '17px', align: 'center' }), wordWrap: { width: width - 200 } }) .setOrigin(0.5, 0); // Tatsächliche Höhe verwenden: umgebrochene Zeilen sind höher als eine Zeile. y += text.height + 2; } y += 12; } } }