slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
/**
* Zugriff auf die JSON-API des Flask-Backends (ohne Phaser).
*
* Fehler werden nie geworfen, sondern als Ergebnisobjekt zurückgegeben: Ein
* ausgefallener Highscore-Server darf das Spiel nicht unterbrechen.
* @module systems/ApiClient
*/
import { HIGHSCORE } from '../config.js';
/**
* @typedef {Object} HighscoreEntry
* @property {number} rank
* @property {string} name
* @property {number} score
* @property {number} wave
* @property {string} created_at
*/
/**
* @typedef {Object} SubmitResult
* @property {boolean} ok
* @property {number} [rank]
* @property {HighscoreEntry[]} [highscores]
* @property {string} [error] Fehlercode der API oder „network“.
* @property {string} [message] Lesbare Meldung.
* @property {Record<string, string>} [fields] Feldfehler bei Validierung.
*/
export class ApiClient {
/**
* @param {{highscores: string, runs: string}} endpoints
* @param {typeof fetch} [fetchImpl] Für Tests austauschbar.
* @param {number} [timeoutMs]
*/
constructor(endpoints, fetchImpl = globalThis.fetch?.bind(globalThis), timeoutMs = HIGHSCORE.requestTimeoutMs) {
this.endpoints = endpoints;
this.fetchImpl = fetchImpl;
this.timeoutMs = timeoutMs;
}
/**
* @param {string} url
* @param {RequestInit} [init]
* @returns {Promise<{status: number, body: any}>}
*/
async request(url, init = {}) {
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timer = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
try {
const response = await this.fetchImpl(url, {
...init,
signal: controller?.signal,
credentials: 'same-origin',
headers: { Accept: 'application/json', ...(init.headers ?? {}) },
});
let body = null;
try {
body = await response.json();
} catch {
body = null;
}
return { status: response.status, body };
} finally {
if (timer) clearTimeout(timer);
}
}
/**
* Meldet eine neue Spielrunde an.
* @returns {Promise<string|null>} Run-Token oder null, wenn der Server nicht erreichbar ist.
*/
async startRun() {
try {
const { status, body } = await this.request(this.endpoints.runs, { method: 'POST' });
return status === 201 && typeof body?.run_token === 'string' ? body.run_token : null;
} catch {
return null;
}
}
/**
* @returns {Promise<{ok: boolean, highscores: HighscoreEntry[]}>}
*/
async fetchHighscores() {
try {
const { status, body } = await this.request(this.endpoints.highscores);
if (status === 200 && Array.isArray(body?.highscores)) {
return { ok: true, highscores: body.highscores };
}
} catch {
// Netzwerkfehler oder Timeout – unten als Fehlschlag gemeldet.
}
return { ok: false, highscores: [] };
}
/**
* Reicht einen Highscore ein.
* @param {{name: string, score: number, wave: number, durationS: number, runToken: string}} entry
* @returns {Promise<SubmitResult>}
*/
async submitHighscore(entry) {
const payload = {
name: entry.name,
score: Math.max(0, Math.floor(entry.score)),
wave: Math.max(1, Math.floor(entry.wave)),
duration_s: Math.max(0, Math.floor(entry.durationS)),
run_token: entry.runToken,
};
try {
const { status, body } = await this.request(this.endpoints.highscores, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (status === 201) {
return { ok: true, rank: body?.rank, highscores: body?.highscores ?? [] };
}
return {
ok: false,
error: body?.error ?? `http_${status}`,
message: body?.message ?? 'Unbekannter Fehler.',
fields: body?.fields,
};
} catch {
return { ok: false, error: 'network', message: 'Keine Verbindung zum Server.' };
}
}
}