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; remainingSec: number; } export class LevelMgr { private elapsedSec = 0; private kills = new Map(); 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 { const out: Record = {}; for (const [k, v] of this.kills.entries()) out[k] = v; return out; } }