slop game oder so
This commit is contained in:
@@ -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.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user