112 lines
3.6 KiB
JavaScript
112 lines
3.6 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|
||
}
|