slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
/**
* 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 = [];
}
}