180 lines
5.8 KiB
JavaScript
180 lines
5.8 KiB
JavaScript
/**
|
||
* 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 = [];
|
||
}
|
||
}
|