108 lines
3.0 KiB
TypeScript
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;
|
|
}
|
|
}
|