Files
2026-05-06 08:17:32 +08:00

108 lines
3.0 KiB
TypeScript

import { EnemyType, ILevelConfig, ILevelObjective } from '../data/Interfaces';
/**
* Level lifecycle manager (task 7.1 + 7.2).
*
* Given an `ILevelConfig`, this class:
* - ticks the time-limit countdown (req 8.1-8.5)
* - tracks kill counters by enemy type
* - evaluates the level objective each tick
* - emits a structured `LevelResult` when the level ends
*
* Scene + parallax rendering live in the Cocos view layer; this module is
* intentionally engine-agnostic so the whole progression logic is Jest-
* testable.
*/
export type LevelStatus = 'running' | 'victory' | 'timeout' | 'player_dead';
export interface ILevelResult {
status: LevelStatus;
elapsedSec: number;
kills: Record<string, number>;
remainingSec: number;
}
export class LevelMgr {
private elapsedSec = 0;
private kills = new Map<EnemyType, number>();
private totalKills = 0;
private bossKilled = false;
private reachedTop = false;
private playerDead = false;
public readonly level: ILevelConfig;
constructor(level: ILevelConfig) {
this.level = level;
}
public tick(dtSec: number): LevelStatus {
if (this.isTerminal()) return this.currentStatus();
this.elapsedSec += dtSec;
return this.currentStatus();
}
public onEnemyKilled(enemy: EnemyType): void {
this.kills.set(enemy, (this.kills.get(enemy) ?? 0) + 1);
this.totalKills++;
}
public onBossKilled(): void {
this.bossKilled = true;
}
public onReachedTop(): void {
this.reachedTop = true;
}
public onPlayerDied(): void {
this.playerDead = true;
}
public result(): ILevelResult {
return {
status: this.currentStatus(),
elapsedSec: this.elapsedSec,
kills: this.killsAsObject(),
remainingSec: Math.max(0, this.level.timeLimitSec - this.elapsedSec),
};
}
public get totalKillsCount(): number {
return this.totalKills;
}
// ------------------------------------------------------------------
private currentStatus(): LevelStatus {
if (this.playerDead) return 'player_dead';
if (this.evaluateObjective(this.level.objective)) return 'victory';
if (this.elapsedSec >= this.level.timeLimitSec) return 'timeout';
return 'running';
}
private isTerminal(): boolean {
const s = this.currentStatus();
return s !== 'running';
}
private evaluateObjective(o: ILevelObjective): boolean {
if (o.kind === 'kill_count' && o.enemy && o.count) {
return (this.kills.get(o.enemy) ?? 0) >= o.count;
}
if (o.kind === 'reach_top') {
return this.reachedTop;
}
if (o.kind === 'defeat_boss') {
return this.bossKilled;
}
return false;
}
private killsAsObject(): Record<string, number> {
const out: Record<string, number> = {};
for (const [k, v] of this.kills.entries()) out[k] = v;
return out;
}
}