/** * 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; } }