slop game oder so
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Game Over: Statistik, Namenseingabe für die Ehrentafel und Top 10.
|
||||
* Formular als HTML-Overlay (Texteingabe im Canvas wäre unzugänglich).
|
||||
* @module scenes/GameOverScene
|
||||
*/
|
||||
|
||||
import { HIGHSCORE } from '../config.js';
|
||||
import { GAME_OVER, HIGHSCORES } from '../content/texts.js';
|
||||
import { formatDuration, formatScore, validateName } from '../logic/format.js';
|
||||
import { el, Overlay } from '../systems/Overlay.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} RunStats
|
||||
* @property {number} score
|
||||
* @property {number} wave
|
||||
* @property {number} kills
|
||||
* @property {number} bestStreak
|
||||
* @property {number} durationMs
|
||||
* @property {string|null} runToken
|
||||
*/
|
||||
|
||||
export class GameOverScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super('GameOver');
|
||||
}
|
||||
|
||||
/** @param {RunStats} stats */
|
||||
create(stats) {
|
||||
this.stats = stats;
|
||||
this.services = this.registry.get('services');
|
||||
const { width, height } = this.scale;
|
||||
this.input.setDefaultCursor('default');
|
||||
// Sonst würde Phaser Tasten wie Leertaste oder WASD im Namensfeld abfangen.
|
||||
this.input.keyboard.clearCaptures();
|
||||
this.input.keyboard.disableGlobalCapture();
|
||||
this.events.once('shutdown', () => {
|
||||
this.overlay?.close();
|
||||
this.input.keyboard?.enableGlobalCapture();
|
||||
});
|
||||
|
||||
this.services.audio.stopMusic();
|
||||
this.services.audio.play('game-over');
|
||||
|
||||
const bg = this.add.graphics();
|
||||
bg.fillGradientStyle(0x2a0a10, 0x2a0a10, 0x0d1321, 0x0d1321, 1);
|
||||
bg.fillRect(0, 0, width, height);
|
||||
|
||||
this.showForm();
|
||||
}
|
||||
|
||||
/** @returns {HTMLElement[]} Untertitel und Statistik. */
|
||||
summary() {
|
||||
const s = this.stats;
|
||||
const subtitle = GAME_OVER.subtitles[Math.floor(Math.random() * GAME_OVER.subtitles.length)];
|
||||
const list = el('dl', 'stats');
|
||||
const rows = [
|
||||
[GAME_OVER.stats.score, formatScore(s.score)],
|
||||
[GAME_OVER.stats.wave, String(s.wave)],
|
||||
[GAME_OVER.stats.kills, String(s.kills)],
|
||||
[GAME_OVER.stats.bestStreak, String(s.bestStreak)],
|
||||
[GAME_OVER.stats.duration, formatDuration(s.durationMs)],
|
||||
];
|
||||
for (const [label, value] of rows) {
|
||||
const row = el('div', 'stats__row');
|
||||
row.append(el('dt', '', label), el('dd', '', value));
|
||||
list.append(row);
|
||||
}
|
||||
this.subtitle = this.subtitle ?? subtitle;
|
||||
return [el('p', 'disclaimer__lead', this.subtitle), list];
|
||||
}
|
||||
|
||||
endButtons() {
|
||||
return [
|
||||
{ label: GAME_OVER.retry, onClick: () => this.scene.start('Game'), testId: 'gameover-retry' },
|
||||
{ label: GAME_OVER.menu, onClick: () => this.scene.start('Menu'), secondary: true, testId: 'gameover-menu' },
|
||||
];
|
||||
}
|
||||
|
||||
showForm() {
|
||||
const content = this.summary();
|
||||
if (!this.stats.runToken || this.stats.score <= 0) {
|
||||
if (!this.stats.runToken) content.push(el('p', 'field__error', GAME_OVER.noToken));
|
||||
this.openOverlay(content, this.endButtons());
|
||||
return;
|
||||
}
|
||||
|
||||
const form = /** @type {HTMLFormElement} */ (el('form', 'highscore-form'));
|
||||
const field = el('label', 'field');
|
||||
const input = /** @type {HTMLInputElement} */ (el('input', 'field__input'));
|
||||
input.type = 'text';
|
||||
input.name = 'name';
|
||||
input.maxLength = HIGHSCORE.nameMaxLength;
|
||||
input.autocomplete = 'nickname';
|
||||
input.spellcheck = false;
|
||||
input.required = true;
|
||||
input.value = this.services.settings.getLastName();
|
||||
input.dataset.testid = 'highscore-name';
|
||||
const error = el('p', 'field__error');
|
||||
error.setAttribute('role', 'alert');
|
||||
field.append(el('span', 'field__label', GAME_OVER.nameLabel), input, el('span', 'field__hint', GAME_OVER.nameHint));
|
||||
form.append(field, error);
|
||||
content.push(form);
|
||||
|
||||
const submit = async () => {
|
||||
const check = validateName(input.value);
|
||||
if (!check.ok) {
|
||||
error.textContent = check.error;
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
submitButton.disabled = true;
|
||||
error.textContent = GAME_OVER.submitting;
|
||||
const result = await this.services.api.submitHighscore({
|
||||
name: check.name,
|
||||
score: this.stats.score,
|
||||
wave: this.stats.wave,
|
||||
durationS: Math.floor(this.stats.durationMs / 1000),
|
||||
runToken: /** @type {string} */ (this.stats.runToken),
|
||||
});
|
||||
if (!this.sys.isActive()) return;
|
||||
if (result.ok) {
|
||||
this.services.settings.setLastName(check.name);
|
||||
this.showResults(GAME_OVER.saved(result.rank), result.highscores ?? [], result.rank);
|
||||
return;
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
if (result.error === 'network') error.textContent = GAME_OVER.offline;
|
||||
else error.textContent = result.fields?.name ?? result.message ?? GAME_OVER.offline;
|
||||
};
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
});
|
||||
|
||||
this.openOverlay(content, [
|
||||
{ label: GAME_OVER.submit, onClick: () => submit(), testId: 'highscore-submit' },
|
||||
{ label: GAME_OVER.skip, onClick: () => this.showResults('', null, null), secondary: true, testId: 'highscore-skip' },
|
||||
]);
|
||||
const submitButton = /** @type {HTMLButtonElement} */ (this.overlay.actions.firstElementChild);
|
||||
input.select();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {import('../systems/ApiClient.js').HighscoreEntry[]|null} highscores null = neu laden
|
||||
* @param {number|null} ownRank
|
||||
*/
|
||||
async showResults(message, highscores, ownRank) {
|
||||
const content = this.summary();
|
||||
if (message) content.push(el('p', 'disclaimer__lead', message));
|
||||
const table = el('ol', 'highscore-list');
|
||||
content.push(table);
|
||||
this.openOverlay(content, this.endButtons());
|
||||
|
||||
let entries = highscores;
|
||||
if (!entries) {
|
||||
table.append(el('li', '', HIGHSCORES.loading));
|
||||
const result = await this.services.api.fetchHighscores();
|
||||
if (!this.sys.isActive()) return;
|
||||
entries = result.ok ? result.highscores : [];
|
||||
table.textContent = '';
|
||||
if (!result.ok) table.append(el('li', '', HIGHSCORES.error));
|
||||
}
|
||||
if (entries.length === 0 && table.childElementCount === 0) table.append(el('li', '', HIGHSCORES.empty));
|
||||
for (const entry of entries) {
|
||||
const item = el('li', entry.rank === ownRank ? 'highscore-list__own' : '');
|
||||
item.append(
|
||||
el('span', 'highscore-list__rank', `${entry.rank}.`),
|
||||
el('span', 'highscore-list__name', entry.name),
|
||||
el('span', 'highscore-list__score', formatScore(entry.score)),
|
||||
el('span', 'highscore-list__wave', `W${entry.wave}`),
|
||||
);
|
||||
table.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement[]} content
|
||||
* @param {import('../systems/Overlay.js').OverlayButton[]} buttons
|
||||
*/
|
||||
openOverlay(content, buttons) {
|
||||
this.overlay?.close();
|
||||
this.overlay = new Overlay({ id: 'gameover-overlay', title: GAME_OVER.title, content, buttons });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user