/** * Formatierung und Eingabeprüfung für Anzeige und Highscore-Formular (ohne Phaser). * @module logic/format */ import { DIRECTION_SYMBOLS, HIGHSCORE } from '../config.js'; const scoreFormatter = new Intl.NumberFormat('de-DE'); /** * Punktestand mit Tausendertrennzeichen („12.345“). * @param {number} score * @returns {string} */ export function formatScore(score) { return scoreFormatter.format(Math.max(0, Math.floor(score))); } /** * Dauer als „m:ss“. * @param {number} ms * @returns {string} */ export function formatDuration(ms) { const totalSeconds = Math.max(0, Math.floor(ms / 1000)); const minutes = Math.floor(totalSeconds / 60); const seconds = String(totalSeconds % 60).padStart(2, '0'); return `${minutes}:${seconds}`; } /** * Multiplikator mit deutschem Dezimalkomma („×1,5“). * @param {number} multiplier * @returns {string} */ export function formatMultiplier(multiplier) { return `×${String(Math.round(multiplier * 10) / 10).replace('.', ',')}`; } /** * Abklingzeit in Sekunden für das HUD („12“, „0,8“). * @param {number} ms * @returns {string} */ export function formatCooldown(ms) { if (ms <= 0) return ''; const seconds = ms / 1000; return seconds >= 1 ? String(Math.ceil(seconds)) : seconds.toFixed(1).replace('.', ','); } /** * Richtungsfolge als Pfeile („↑ → ↓ ↓“). * @param {readonly string[]} sequence * @param {string} [separator] * @returns {string} */ export function formatSequence(sequence, separator = ' ') { return sequence.map((dir) => DIRECTION_SYMBOLS[dir] ?? '?').join(separator); } /** Wie auf dem Server (app/validation.py): alles außer Buchstaben, Ziffern und wenigen Zeichen. */ const DISALLOWED_NAME_CHARS = /[^\p{L}\p{N}_ .\-'!?]/gu; /** Steuer-, Format-, Surrogat- und private Zeichen (Unicode-Kategorie C). */ const CONTROL_CHARS = /\p{C}/gu; /** * Bereinigt einen Namen genauso wie das Backend, damit der Spieler vor dem Absenden * sieht, was gespeichert wird. Der Server prüft trotzdem selbst. * @param {string} raw * @returns {string} */ export function sanitizeName(raw) { return String(raw) .normalize('NFKC') .replace(CONTROL_CHARS, '') .replace(DISALLOWED_NAME_CHARS, '') .split(/\s+/u) .filter(Boolean) .join(' '); } /** * Prüft einen Namen für das Highscore-Formular. * @param {string} raw * @returns {{ok: true, name: string} | {ok: false, error: string}} */ export function validateName(raw) { const name = sanitizeName(raw); if (!name) return { ok: false, error: 'Bitte einen Namen mit Buchstaben oder Ziffern eingeben.' }; if ([...name].length > HIGHSCORE.nameMaxLength) { return { ok: false, error: `Höchstens ${HIGHSCORE.nameMaxLength} Zeichen.` }; } return { ok: true, name }; }