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

112 lines
3.6 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.
/**
* Ablaufsteuerung der Wellen (ohne Phaser): Pause → Welle → Spawns → Welle geschafft.
*
* Der Director kennt keine Sprites. Er ruft `spawnGroup` auf, wenn eine Gruppe
* erscheinen soll, und fragt `aliveCount` ab, um zu erkennen, wann eine Welle
* geschafft ist. So lässt sich der gesamte Ablauf ohne Browser testen.
* @module logic/waveDirector
*/
import { WAVES } from '../config.js';
import { generateWave } from './waves.js';
/** @typedef {import('./waves.js').WavePlan} WavePlan */
/** @typedef {import('./waves.js').SpawnGroup} SpawnGroup */
/**
* @typedef {Object} WaveDirectorOptions
* @property {import('./rng.js').Rng} rng
* @property {(group: SpawnGroup, plan: WavePlan) => void} spawnGroup
* @property {() => number} aliveCount
* @property {(plan: WavePlan) => void} [onWaveStart]
* @property {(waveNumber: number) => void} [onWaveCleared]
* @property {typeof WAVES} [cfg]
*/
export class WaveDirector {
/** @param {WaveDirectorOptions} options */
constructor(options) {
this.rng = options.rng;
this.spawnGroup = options.spawnGroup;
this.aliveCount = options.aliveCount;
this.onWaveStart = options.onWaveStart ?? (() => {});
this.onWaveCleared = options.onWaveCleared ?? (() => {});
this.cfg = options.cfg ?? WAVES;
/** @type {'idle'|'intermission'|'active'} */
this.state = 'idle';
this.waveNumber = 0;
/** @type {WavePlan|null} */
this.plan = null;
this.nextWaveAt = 0;
this.nextGroupIndex = 0;
/**
* Uhr der laufenden Welle. Sie steht still, solange zu viele Gegner leben –
* so stauen sich Spawns nicht auf, wenn der Spieler in Bedrängnis ist.
*/
this.waveClock = 0;
}
/**
* Startet den Ablauf; die erste Welle folgt nach `firstWaveDelayMs`.
* @param {number} now Spielzeit in ms.
*/
start(now) {
this.state = 'intermission';
this.waveNumber = 0;
this.nextWaveAt = now + this.cfg.firstWaveDelayMs;
}
/**
* Muss jeden Frame aufgerufen werden.
* @param {number} now Spielzeit in ms.
* @param {number} deltaMs Seit dem letzten Aufruf vergangene Spielzeit.
*/
update(now, deltaMs) {
if (this.state === 'intermission' && now >= this.nextWaveAt) {
this.beginWave();
return;
}
if (this.state !== 'active' || !this.plan) return;
if (this.aliveCount() < this.cfg.maxAliveEnemies) {
this.waveClock += deltaMs;
}
const { groups } = this.plan;
while (this.nextGroupIndex < groups.length && groups[this.nextGroupIndex].atMs <= this.waveClock) {
this.spawnGroup(groups[this.nextGroupIndex], this.plan);
this.nextGroupIndex += 1;
}
if (this.nextGroupIndex >= groups.length && this.aliveCount() === 0) {
const cleared = this.waveNumber;
this.state = 'intermission';
this.nextWaveAt = now + this.cfg.intermissionMs;
this.onWaveCleared(cleared);
}
}
/** @private */
beginWave() {
this.waveNumber += 1;
this.plan = generateWave(this.waveNumber, this.rng, this.cfg);
this.nextGroupIndex = 0;
this.waveClock = 0;
this.state = 'active';
this.onWaveStart(this.plan);
}
/** @returns {number} Gegner, die in dieser Welle noch erscheinen werden. */
get pendingEnemies() {
if (this.state !== 'active' || !this.plan) return 0;
return this.plan.groups.slice(this.nextGroupIndex).reduce((sum, g) => sum + g.count, 0);
}
/**
* @param {number} now
* @returns {number} Restzeit der Pause in ms (0 während einer Welle).
*/
intermissionRemaining(now) {
return this.state === 'intermission' ? Math.max(0, this.nextWaveAt - now) : 0;
}
}