Files
slop-game-opus5.5/app/static/js/logic/comboInput.js
T
2026-09-24 17:57:54 +02:00

133 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Eingabe der Orbital-Befehle als Richtungsfolge (ohne Phaser).
*
* Solange die Halte-Taste gedrückt ist, sammelt `ComboInput` Richtungen. Nach jeder
* Eingabe wird geprüft, ob der Puffer noch Anfang mindestens eines Befehls ist:
* - passt nichts mehr → „fail“, Puffer wird geleert (man kann direkt neu ansetzen)
* - Folge vollständig → „match“ mit Befehls-ID
* - sonst → „progress“
* Ob ein Befehl gerade abklingt, entscheidet der Aufrufer – hier geht es nur um Folgen.
* @module logic/comboInput
*/
/** @typedef {'up'|'down'|'left'|'right'} Direction */
/** @typedef {{id: string, sequence: readonly Direction[]}} ComboCommand */
/** @type {readonly Direction[]} */
export const DIRECTIONS = Object.freeze(['up', 'down', 'left', 'right']);
/**
* Physische Tastencodes (KeyboardEvent.code) → Richtung. `code` statt `key`, damit
* WASD auch auf AZERTY- oder QWERTZ-Tastaturen an derselben Stelle liegt.
*/
const CODE_TO_DIRECTION = Object.freeze({
ArrowUp: 'up',
KeyW: 'up',
ArrowDown: 'down',
KeyS: 'down',
ArrowLeft: 'left',
KeyA: 'left',
ArrowRight: 'right',
KeyD: 'right',
});
/**
* @param {string} code KeyboardEvent.code
* @returns {Direction|null}
*/
export function codeToDirection(code) {
return CODE_TO_DIRECTION[code] ?? null;
}
/**
* @param {readonly string[]} prefix
* @param {readonly string[]} sequence
* @returns {boolean}
*/
export function isPrefix(prefix, sequence) {
if (prefix.length > sequence.length) return false;
return prefix.every((dir, i) => sequence[i] === dir);
}
/**
* Prüft die Befehlsliste: gültige Richtungen, eindeutige IDs und keine Folge, die
* Anfang einer anderen ist (sonst wäre die längere Folge nie erreichbar).
* @param {readonly ComboCommand[]} commands
* @throws {Error} bei ungültiger Konfiguration.
*/
export function validateCommands(commands) {
const ids = new Set();
for (const command of commands) {
if (ids.has(command.id)) throw new Error(`Doppelte Befehls-ID: ${command.id}`);
ids.add(command.id);
if (command.sequence.length === 0) throw new Error(`Leere Folge bei ${command.id}`);
for (const dir of command.sequence) {
if (!DIRECTIONS.includes(dir)) throw new Error(`Ungültige Richtung „${dir}“ bei ${command.id}`);
}
}
for (const a of commands) {
for (const b of commands) {
if (a !== b && isPrefix(a.sequence, b.sequence)) {
throw new Error(`Folge von ${a.id} ist Anfang der Folge von ${b.id}`);
}
}
}
}
export class ComboInput {
/** @param {readonly ComboCommand[]} commands */
constructor(commands) {
validateCommands(commands);
this.commands = commands;
/** @type {Direction[]} */
this.buffer = [];
this.active = false;
}
/** Halte-Taste gedrückt: Eingabe beginnt mit leerem Puffer. */
begin() {
this.active = true;
this.buffer = [];
}
/** Halte-Taste losgelassen: halbe Eingaben verfallen. */
end() {
this.active = false;
this.buffer = [];
}
/**
* Verarbeitet eine Richtung.
* @param {string} direction
* @returns {{status: 'inactive'|'ignored'|'fail'|'progress'|'match', commandId?: string, candidates?: string[]}}
*/
push(direction) {
if (!this.active) return { status: 'inactive' };
if (!DIRECTIONS.includes(/** @type {Direction} */ (direction))) return { status: 'ignored' };
this.buffer.push(/** @type {Direction} */ (direction));
const candidates = this.commands.filter((c) => isPrefix(this.buffer, c.sequence));
if (candidates.length === 0) {
this.buffer = [];
return { status: 'fail' };
}
const exact = candidates.find((c) => c.sequence.length === this.buffer.length);
if (exact) {
this.buffer = [];
return { status: 'match', commandId: exact.id };
}
return { status: 'progress', candidates: candidates.map((c) => c.id) };
}
/**
* Wie viele Schritte eines Befehls bereits korrekt eingegeben sind (für die HUD-Anzeige).
* @param {string} commandId
* @returns {number}
*/
progressFor(commandId) {
const command = this.commands.find((c) => c.id === commandId);
if (!command || !isPrefix(this.buffer, command.sequence)) return 0;
return this.buffer.length;
}
}