first commmit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
out.push(...this.checkPhaseTransition(), ...this.checkPrincessCutscene());
|
||||
if (this.hp === 0) {
|
||||
this.killed = true;
|
||||
out.push({ kind: 'boss_killed' });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
private checkPhaseTransition(): BossOutcomeEvent[] {
|
||||
const hpRatio = this.hp / this.cfg.hp;
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user