import { IBossConfig } from '../data/Interfaces'; /** * Boss controller — 双幻坊 (req 9.1-9.6 + 8.6-8.7). * * Key beats: * 1. A lone butterfly orbits the boss. Until the butterfly is hit, the boss * is invulnerable (`butterflyRevealed = false`). * 2. First hit on the butterfly → `revealedAt` timestamp stamped, boss * becomes vulnerable. * 3. While revealed, ANY single clean hit kills the boss (req 9.3). We still * honour `phase` transitions at 2/3 and 1/3 HP for visual variety. * 4. When HP ≤ `princessCutsceneAtHpRatio`, we emit a one-shot `princess_taken` * event (≤ 3s, battle keeps running — req 8.6 / 14.1). * 5. On death we emit `boss_killed` + `chapter_end_cutscene` (≤ 2s). Neither * code path produces a "rope-severing rescue" event — decision D-5 / req * 14.5. */ export type BossOutcomeEvent = | { kind: 'phase_changed'; phase: string; actionIntervalSec: number } | { kind: 'butterfly_revealed' } | { kind: 'princess_taken_cutscene' } | { kind: 'boss_killed' }; export class BossController { private hp: number; private butterflyRevealed = false; private phaseIndex = 0; private princessCutscenePlayed = false; private killed = false; public readonly cfg: IBossConfig; constructor(cfg: IBossConfig) { this.cfg = cfg; this.hp = cfg.hp; } public get currentHp(): number { return this.hp; } public get currentPhase() { return this.cfg.phases[this.phaseIndex]; } public get isButterflyRevealed(): boolean { return this.butterflyRevealed; } public get isDead(): boolean { return this.killed; } /** Called when a player's attack hits the butterfly (req 9.2). */ public onButterflyHit(): BossOutcomeEvent[] { if (this.butterflyRevealed) return []; this.butterflyRevealed = true; return [{ kind: 'butterfly_revealed' }]; } /** * Called when a player's attack hits the boss body. Before the butterfly * is revealed this call is a no-op; after reveal, the boss dies in one * hit (req 9.3). */ public onBodyHit(): BossOutcomeEvent[] { if (!this.butterflyRevealed) return []; if (this.killed) return []; const out: BossOutcomeEvent[] = []; this.hp = Math.max(0, this.hp - 1); // Check phase transition AFTER decrementing HP so the ratio // reflects the new HP (e.g. 3→2 = 2/3). const hpRatio = this.hp / this.cfg.hp; out.push(...this.checkPhaseTransitionAtRatio(hpRatio), ...this.checkPrincessCutscene()); if (this.hp === 0) { this.killed = true; out.push({ kind: 'boss_killed' }); } return out; } // -------------------------------------------------------------------- private checkPhaseTransitionAtRatio(hpRatio: number): BossOutcomeEvent[] { for (let i = this.phaseIndex + 1; i < this.cfg.phases.length; i++) { if (hpRatio <= this.cfg.phases[i].hpThreshold) { this.phaseIndex = i; return [ { kind: 'phase_changed', phase: this.cfg.phases[i].mode, actionIntervalSec: this.cfg.phases[i].actionIntervalSec, }, ]; } } return []; } private checkPrincessCutscene(): BossOutcomeEvent[] { if (this.princessCutscenePlayed) return []; const threshold = this.cfg.princessCutsceneAtHpRatio; if (threshold === undefined) return []; const hpRatio = this.hp / this.cfg.hp; if (hpRatio <= threshold) { this.princessCutscenePlayed = true; return [{ kind: 'princess_taken_cutscene' }]; } return []; } }