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

180 lines
5.8 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.
/**
* Orbital-Befehle: Eingabe (Halte-Taste + Richtungsfolge), Abklingzeiten,
* Bodenmarkierung und verzögerter Einschlag.
*
* Die Eingabelogik steckt in `logic/comboInput.js`, die Abklingzeiten in
* `logic/cooldowns.js`; hier wird beides mit Phaser-Tastatur und Grafik verbunden.
* @module systems/OrbitalSystem
*/
import { COLORS, GAME, ORBITAL } from '../config.js';
import { codeToDirection, ComboInput } from '../logic/comboInput.js';
import { CooldownTracker } from '../logic/cooldowns.js';
import { clamp, distance } from '../logic/math.js';
import { GroundMarker } from './GroundMarker.js';
import { oncePerEvent } from './keyboard.js';
/** Farbe der Markierung je Befehl. */
const MARKER_COLORS = Object.freeze({
strike: COLORS.strike,
supply: COLORS.supply,
turret: COLORS.turret,
});
/**
* @typedef {typeof ORBITAL.commands[number]} OrbitalCommand
*/
/**
* @typedef {Object} OrbitalCallbacks
* @property {() => {x: number, y: number}} getPlayer
* @property {() => {x: number, y: number}} getAimPoint Mausposition in Weltkoordinaten.
* @property {(command: OrbitalCommand, x: number, y: number) => void} onImpact
* @property {(kind: 'step'|'fail'|'deploy'|'notReady', command?: OrbitalCommand, remainingMs?: number) => void} onFeedback
*/
export class OrbitalSystem {
/**
* @param {Phaser.Scene} scene
* @param {OrbitalCallbacks} callbacks
*/
constructor(scene, callbacks) {
this.scene = scene;
this.callbacks = callbacks;
this.commands = ORBITAL.commands;
this.combo = new ComboInput(this.commands);
this.cooldowns = new CooldownTracker();
/** @type {{command: OrbitalCommand, marker: GroundMarker}[]} */
this.pending = [];
this.now = 0;
this.holdKey = scene.input.keyboard.addKey(ORBITAL.holdKey);
// Pfeiltasten abfangen, sonst scrollt der Browser die Seite.
scene.input.keyboard.addCapture('UP,DOWN,LEFT,RIGHT');
this.onKeyDown = oncePerEvent((event) => this.handleKeyDown(event));
scene.input.keyboard.on('keydown', this.onKeyDown);
}
/** @returns {boolean} true, solange ein Befehl eingegeben wird (Bewegung pausiert). */
get isInputActive() {
return this.combo.active;
}
/**
* @param {number} now Spielzeit in ms.
*/
update(now) {
this.now = now;
if (this.holdKey.isDown && !this.combo.active) this.combo.begin();
else if (!this.holdKey.isDown && this.combo.active) this.combo.end();
for (let i = this.pending.length - 1; i >= 0; i -= 1) {
const entry = this.pending[i];
if (entry.marker.isDue(now)) {
entry.marker.destroy();
this.pending.splice(i, 1);
this.callbacks.onImpact(entry.command, entry.marker.x, entry.marker.y);
} else {
entry.marker.update(now);
}
}
}
/**
* @param {KeyboardEvent} event
* @private
*/
handleKeyDown(event) {
// Gedrückt gehaltene Tasten erzeugen Wiederholungen – die zählen nicht als Eingabe.
if (event.repeat) return;
// Q und erste Richtung können im selben Frame ankommen, bevor update() die
// Eingabe gestartet hat – dann hier starten, sonst ginge die erste Richtung verloren.
if (!this.combo.active && this.holdKey.isDown) this.combo.begin();
if (!this.combo.active) return;
const direction = codeToDirection(event.code);
if (!direction) return;
const result = this.combo.push(direction);
if (result.status === 'progress') this.callbacks.onFeedback('step');
else if (result.status === 'fail') this.callbacks.onFeedback('fail');
else if (result.status === 'match') this.request(/** @type {string} */ (result.commandId));
}
/**
* Fordert einen Befehl an (sofern bereit) und setzt die Markierung ans Ziel.
* @param {string} commandId
* @returns {boolean}
*/
request(commandId) {
const command = this.commands.find((c) => c.id === commandId);
if (!command) return false;
const now = this.now;
if (!this.cooldowns.isReady(command.id, now)) {
this.callbacks.onFeedback('notReady', command, this.cooldowns.remaining(command.id, now));
return false;
}
const target = this.clampTarget(this.callbacks.getAimPoint());
this.cooldowns.trigger(command.id, command.cooldownMs, now);
const marker = new GroundMarker(
this.scene,
target.x,
target.y,
command.radius,
MARKER_COLORS[command.id] ?? COLORS.gold,
now,
now + command.delayMs,
{ label: command.shortName, beam: true },
);
marker.update(now);
this.pending.push({ command, marker });
this.callbacks.onFeedback('deploy', command);
return true;
}
/**
* Begrenzt das Ziel auf die maximale Reichweite und die Arena.
* @param {{x: number, y: number}} aim
* @returns {{x: number, y: number}}
*/
clampTarget(aim) {
const player = this.callbacks.getPlayer();
const d = distance(player, aim);
let { x, y } = aim;
if (d > ORBITAL.maxTargetRange) {
const f = ORBITAL.maxTargetRange / d;
x = player.x + (aim.x - player.x) * f;
y = player.y + (aim.y - player.y) * f;
}
const margin = GAME.spawnMargin;
return {
x: clamp(x, margin, GAME.world.width - margin),
y: clamp(y, margin, GAME.world.height - margin),
};
}
/**
* Daten für die HUD-Anzeige.
* @param {number} now
*/
hudState(now) {
return {
active: this.combo.active,
commands: this.commands.map((c) => ({
id: c.id,
name: c.shortName,
sequence: c.sequence,
progress: this.combo.progressFor(c.id),
ready: this.cooldowns.isReady(c.id, now),
remainingMs: this.cooldowns.remaining(c.id, now),
cooldownProgress: this.cooldowns.progress(c.id, now),
})),
};
}
/** Aufräumen beim Beenden der Szene. */
destroy() {
this.scene.input.keyboard?.off('keydown', this.onKeyDown);
for (const entry of this.pending) entry.marker.destroy();
this.pending = [];
}
}