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
+111
View File
@@ -0,0 +1,111 @@
/**
* 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;
}
}