137 lines
5.3 KiB
JavaScript
137 lines
5.3 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|
||
}
|
||
}
|