/** * Boss System Tests — Phase 8: First Archont (Ouroboros) * * Tests cover: * - Boss state creation and initialization * - Phase cycling (Coil → Spray → Lash → Digest → repeat) * - Phase speedup per cycle * - Damage mechanics (vulnerability windows, armor) * - Chemical damage (NaOH during Spray) * - Catalyst poison (Hg stacks, regen/armor reduction) * - Victory detection and method determination * - Arena generation (circular layout, features) * - Reward calculation (spores, lore) * - Boss entity factory (ECS) */ import { describe, it, expect, beforeEach } from 'vitest'; import { createWorld, addEntity, addComponent, query } from 'bitecs'; import type { World } from '../src/ecs/world'; import { Position, Health, SpriteRef, Boss } from '../src/ecs/components'; // Boss system imports import { createBossState, updateBossPhase, getEffectiveArmor, getEffectiveRegen, isVulnerable, getEffectivePhaseDuration, } from '../src/boss/ai'; import { applyBossDamage, isBossDefeated, } from '../src/boss/victory'; import { generateArena, buildArenaWalkableSet, } from '../src/boss/arena'; import { calculateBossReward, applyBossReward, } from '../src/boss/reward'; import { createBossEntity } from '../src/boss/factory'; // Types import { BossPhase, VictoryMethod, type BossData, type BossState, } from '../src/boss/types'; import type { BiomeData } from '../src/world/types'; import { createMetaState } from '../src/run/meta'; // Load boss data import bossDataArray from '../src/data/bosses.json'; import biomeDataArray from '../src/data/biomes.json'; const ouroboros = bossDataArray[0] as BossData; const biome = biomeDataArray[0] as BiomeData; // ─── Boss State Creation ───────────────────────────────────────── describe('Boss State Creation', () => { it('creates initial boss state from data', () => { const state = createBossState(ouroboros); expect(state.bossId).toBe('ouroboros'); expect(state.health).toBe(300); expect(state.maxHealth).toBe(300); expect(state.currentPhase).toBe(BossPhase.Coil); expect(state.cycleCount).toBe(0); expect(state.catalystStacks).toBe(0); expect(state.defeated).toBe(false); expect(state.victoryMethod).toBeNull(); }); it('starts with phase timer set to first phase duration', () => { const state = createBossState(ouroboros); expect(state.phaseTimer).toBe(ouroboros.phaseDurations[0]); // 5000ms }); it('starts with zero damage counters', () => { const state = createBossState(ouroboros); expect(state.totalDamageDealt).toBe(0); expect(state.chemicalDamageDealt).toBe(0); expect(state.directDamageDealt).toBe(0); expect(state.catalystDamageDealt).toBe(0); }); }); // ─── Phase Cycling ─────────────────────────────────────────────── describe('Boss Phase Cycling', () => { let state: BossState; beforeEach(() => { state = createBossState(ouroboros); }); it('stays in current phase while timer has not expired', () => { updateBossPhase(state, ouroboros, 1000); expect(state.currentPhase).toBe(BossPhase.Coil); expect(state.phaseTimer).toBeGreaterThan(0); }); it('transitions from Coil to Spray when timer expires', () => { updateBossPhase(state, ouroboros, 5000); // Full Coil duration expect(state.currentPhase).toBe(BossPhase.Spray); }); it('transitions through full cycle: Coil → Spray → Lash → Digest', () => { // Coil (5s) → Spray updateBossPhase(state, ouroboros, 5000); expect(state.currentPhase).toBe(BossPhase.Spray); // Spray (8s) → Lash updateBossPhase(state, ouroboros, 8000); expect(state.currentPhase).toBe(BossPhase.Lash); // Lash (6s) → Digest updateBossPhase(state, ouroboros, 6000); expect(state.currentPhase).toBe(BossPhase.Digest); }); it('completes a full cycle and increments cycleCount', () => { // Full cycle: 5000 + 8000 + 6000 + 4000 = 23000ms const events = []; events.push(...updateBossPhase(state, ouroboros, 5000)); // Coil → Spray events.push(...updateBossPhase(state, ouroboros, 8000)); // Spray → Lash events.push(...updateBossPhase(state, ouroboros, 6000)); // Lash → Digest events.push(...updateBossPhase(state, ouroboros, 4000)); // Digest → Coil (cycle 1) expect(state.currentPhase).toBe(BossPhase.Coil); expect(state.cycleCount).toBe(1); const cycleEvent = events.find(e => e.type === 'cycle_complete'); expect(cycleEvent).toBeDefined(); expect(cycleEvent?.cycleCount).toBe(1); }); it('emits phase_change events', () => { const events = updateBossPhase(state, ouroboros, 5000); // Coil → Spray const phaseChange = events.find(e => e.type === 'phase_change'); expect(phaseChange).toBeDefined(); expect(phaseChange?.phase).toBe(BossPhase.Spray); }); it('emits boss_attack during Spray phase', () => { updateBossPhase(state, ouroboros, 5000); // → Spray const events = updateBossPhase(state, ouroboros, 100); // Tick in Spray const attack = events.find(e => e.type === 'boss_attack'); expect(attack).toBeDefined(); expect(attack?.phase).toBe(BossPhase.Spray); }); it('emits boss_attack during Lash phase', () => { updateBossPhase(state, ouroboros, 5000 + 8000); // → Lash const events = updateBossPhase(state, ouroboros, 100); // Tick in Lash const attack = events.find(e => e.type === 'boss_attack'); expect(attack).toBeDefined(); expect(attack?.phase).toBe(BossPhase.Lash); }); it('does not emit boss_attack during Coil or Digest', () => { // During Coil const coilEvents = updateBossPhase(state, ouroboros, 100); expect(coilEvents.filter(e => e.type === 'boss_attack')).toHaveLength(0); // Advance to Digest updateBossPhase(state, ouroboros, 4900 + 8000 + 6000); // Coil → Spray → Lash → Digest const digestEvents = updateBossPhase(state, ouroboros, 100); expect(digestEvents.filter(e => e.type === 'boss_attack')).toHaveLength(0); }); it('does not update when defeated', () => { state.defeated = true; const events = updateBossPhase(state, ouroboros, 10000); expect(events).toHaveLength(0); }); }); // ─── Phase Speedup ─────────────────────────────────────────────── describe('Boss Phase Speedup', () => { it('returns base duration for cycle 0', () => { const duration = getEffectivePhaseDuration( { cycleCount: 0 } as BossState, ouroboros, BossPhase.Coil, ); expect(duration).toBe(5000); }); it('reduces duration by speedup factor per cycle', () => { // Cycle 1: 5000 * 0.9 = 4500 const duration = getEffectivePhaseDuration( { cycleCount: 1 } as BossState, ouroboros, BossPhase.Coil, ); expect(duration).toBeCloseTo(4500); }); it('caps speedup at maxCycles', () => { // Cycle 5 (maxCycles): 5000 * 0.9^5 = 2952.45 const duration5 = getEffectivePhaseDuration( { cycleCount: 5 } as BossState, ouroboros, BossPhase.Coil, ); // Cycle 10 (should cap at 5): same const duration10 = getEffectivePhaseDuration( { cycleCount: 10 } as BossState, ouroboros, BossPhase.Coil, ); expect(duration10).toBeCloseTo(duration5); }); it('applies speedup to all phases', () => { const state = { cycleCount: 2 } as BossState; const coil = getEffectivePhaseDuration(state, ouroboros, BossPhase.Coil); const spray = getEffectivePhaseDuration(state, ouroboros, BossPhase.Spray); const lash = getEffectivePhaseDuration(state, ouroboros, BossPhase.Lash); const digest = getEffectivePhaseDuration(state, ouroboros, BossPhase.Digest); expect(coil).toBeCloseTo(5000 * 0.81); // 0.9^2 expect(spray).toBeCloseTo(8000 * 0.81); expect(lash).toBeCloseTo(6000 * 0.81); expect(digest).toBeCloseTo(4000 * 0.81); }); }); // ─── Armor & Regeneration ──────────────────────────────────────── describe('Boss Armor and Regeneration', () => { let state: BossState; beforeEach(() => { state = createBossState(ouroboros); }); it('has full armor during non-vulnerable phases', () => { state.currentPhase = BossPhase.Coil; expect(getEffectiveArmor(state, ouroboros)).toBe(0.5); }); it('has reduced armor during vulnerable phases', () => { state.currentPhase = BossPhase.Digest; expect(getEffectiveArmor(state, ouroboros)).toBe(0.1); }); it('has full regen rate with 0 catalyst stacks', () => { expect(getEffectiveRegen(state, ouroboros)).toBe(5); }); it('reduces regen per catalyst stack', () => { state.catalystStacks = 1; expect(getEffectiveRegen(state, ouroboros)).toBe(3); // 5 - 1*2 = 3 }); it('regen does not go below 0', () => { state.catalystStacks = 3; expect(getEffectiveRegen(state, ouroboros)).toBe(0); // 5 - 3*2 = -1 → 0 }); it('reduces armor per catalyst stack', () => { state.catalystStacks = 2; state.currentPhase = BossPhase.Coil; expect(getEffectiveArmor(state, ouroboros)).toBe(0.3); // 0.5 - 2*0.1 }); it('armor does not go below 0', () => { state.catalystStacks = 10; // Way over max stacks state.currentPhase = BossPhase.Digest; expect(getEffectiveArmor(state, ouroboros)).toBe(0); // 0.1 - 10*0.1 → 0 }); it('identifies vulnerable phases correctly', () => { state.currentPhase = BossPhase.Digest; expect(isVulnerable(state, ouroboros)).toBe(true); state.currentPhase = BossPhase.Coil; expect(isVulnerable(state, ouroboros)).toBe(false); state.currentPhase = BossPhase.Spray; expect(isVulnerable(state, ouroboros)).toBe(false); state.currentPhase = BossPhase.Lash; expect(isVulnerable(state, ouroboros)).toBe(false); }); it('applies regeneration over time', () => { state.health = 250; // Damaged updateBossPhase(state, ouroboros, 2000); // 2 seconds → 10 HP regen expect(state.health).toBeCloseTo(260); }); it('does not regenerate past max health', () => { state.health = 298; updateBossPhase(state, ouroboros, 2000); // Would regen 10, capped at 300 expect(state.health).toBe(300); }); it('regeneration is reduced by catalyst stacks', () => { state.health = 250; state.catalystStacks = 2; // regen = 5 - 2*2 = 1 HP/s updateBossPhase(state, ouroboros, 2000); // 2 seconds → 2 HP regen expect(state.health).toBeCloseTo(252); }); }); // ─── Damage: Direct (Victory Path 2) ──────────────────────────── describe('Boss Damage — Direct', () => { let state: BossState; beforeEach(() => { state = createBossState(ouroboros); }); it('deals full damage during Digest (vulnerable) phase', () => { state.currentPhase = BossPhase.Digest; const result = applyBossDamage(state, ouroboros, 'Fe'); // 15 * (1 - 0.1) = 13.5 → 14 rounded expect(result.damageDealt).toBe(14); expect(result.damageType).toBe(VictoryMethod.Direct); }); it('deals reduced damage during non-vulnerable phases', () => { state.currentPhase = BossPhase.Coil; const result = applyBossDamage(state, ouroboros, 'Fe'); // 15 * (1 - 0.5) = 7.5 → 8 rounded expect(result.damageDealt).toBe(8); }); it('tracks direct damage dealt', () => { state.currentPhase = BossPhase.Digest; applyBossDamage(state, ouroboros, 'Fe'); expect(state.directDamageDealt).toBeGreaterThan(0); expect(state.totalDamageDealt).toBeGreaterThan(0); }); it('does not deal damage to defeated boss', () => { state.defeated = true; const result = applyBossDamage(state, ouroboros, 'Fe'); expect(result.damageDealt).toBe(0); }); }); // ─── Damage: Chemical (Victory Path 1) ────────────────────────── describe('Boss Damage — Chemical (NaOH)', () => { let state: BossState; beforeEach(() => { state = createBossState(ouroboros); }); it('deals multiplied damage with NaOH during Spray phase', () => { state.currentPhase = BossPhase.Spray; const result = applyBossDamage(state, ouroboros, 'NaOH'); // 15 * 3.0 * (1 - 0.5) = 22.5 → 23 rounded expect(result.damageDealt).toBe(23); expect(result.damageType).toBe(VictoryMethod.Chemical); }); it('treats NaOH as normal projectile outside effective phases', () => { state.currentPhase = BossPhase.Coil; // Not a chemical-effective phase const result = applyBossDamage(state, ouroboros, 'NaOH'); // Normal reduced damage: 15 * (1 - 0.5) = 8 expect(result.damageDealt).toBe(8); expect(result.damageType).toBe(VictoryMethod.Direct); }); it('tracks chemical damage separately', () => { state.currentPhase = BossPhase.Spray; applyBossDamage(state, ouroboros, 'NaOH'); expect(state.chemicalDamageDealt).toBeGreaterThan(0); expect(state.directDamageDealt).toBe(0); }); }); // ─── Damage: Catalytic Poison (Victory Path 3) ────────────────── describe('Boss Damage — Catalytic Poison (Hg)', () => { let state: BossState; beforeEach(() => { state = createBossState(ouroboros); }); it('applies catalyst stack on first Hg hit', () => { const result = applyBossDamage(state, ouroboros, 'Hg'); expect(result.catalystApplied).toBe(true); expect(state.catalystStacks).toBe(1); expect(result.damageType).toBe(VictoryMethod.Catalytic); }); it('caps at maxCatalystStacks', () => { state.catalystStacks = 3; // Already at max const result = applyBossDamage(state, ouroboros, 'Hg'); expect(result.catalystApplied).toBe(false); expect(state.catalystStacks).toBe(3); }); it('deals damage on every Hg application', () => { const result = applyBossDamage(state, ouroboros, 'Hg'); expect(result.damageDealt).toBeGreaterThan(0); }); it('tracks catalyst damage separately', () => { applyBossDamage(state, ouroboros, 'Hg'); expect(state.catalystDamageDealt).toBeGreaterThan(0); expect(state.chemicalDamageDealt).toBe(0); expect(state.directDamageDealt).toBe(0); }); it('Hg works in any phase (not phase-dependent)', () => { for (const phase of [BossPhase.Coil, BossPhase.Spray, BossPhase.Lash, BossPhase.Digest]) { const s = createBossState(ouroboros); s.currentPhase = phase; const result = applyBossDamage(s, ouroboros, 'Hg'); expect(result.catalystApplied).toBe(true); expect(result.damageType).toBe(VictoryMethod.Catalytic); } }); }); // ─── Victory Detection ─────────────────────────────────────────── describe('Boss Victory Detection', () => { let state: BossState; beforeEach(() => { state = createBossState(ouroboros); }); it('detects killing blow', () => { state.health = 5; state.currentPhase = BossPhase.Digest; const result = applyBossDamage(state, ouroboros, 'Fe'); expect(result.killingBlow).toBe(true); expect(state.defeated).toBe(true); }); it('sets victoryMethod based on killing blow type', () => { state.health = 5; state.currentPhase = BossPhase.Spray; applyBossDamage(state, ouroboros, 'NaOH'); expect(state.victoryMethod).toBe(VictoryMethod.Chemical); }); it('sets Catalytic victory on Hg killing blow', () => { state.health = 3; applyBossDamage(state, ouroboros, 'Hg'); expect(state.victoryMethod).toBe(VictoryMethod.Catalytic); }); it('isBossDefeated returns true when defeated', () => { expect(isBossDefeated(state)).toBe(false); state.health = 1; state.currentPhase = BossPhase.Digest; applyBossDamage(state, ouroboros, 'Fe'); expect(isBossDefeated(state)).toBe(true); }); it('health does not go below 0', () => { state.health = 1; state.currentPhase = BossPhase.Digest; applyBossDamage(state, ouroboros, 'Fe'); expect(state.health).toBe(0); }); }); // ─── Arena Generation ──────────────────────────────────────────── describe('Arena Generation', () => { it('generates a grid of correct size', () => { const arena = generateArena(ouroboros, biome); const diameter = ouroboros.arenaRadius * 2 + 1; expect(arena.width).toBe(diameter); expect(arena.height).toBe(diameter); expect(arena.grid.length).toBe(diameter); expect(arena.grid[0].length).toBe(diameter); }); it('has walkable ground in the center', () => { const arena = generateArena(ouroboros, biome); const center = ouroboros.arenaRadius; // Center tile should be walkable ground (0) expect(arena.grid[center][center]).toBe(0); }); it('has crystal wall border', () => { const arena = generateArena(ouroboros, biome); const r = ouroboros.arenaRadius; // Top edge should be bedrock (7) or crystal (4) // Very top-left corner is outside circle → bedrock expect(arena.grid[0][0]).toBe(7); }); it('has boss spawn at center', () => { const arena = generateArena(ouroboros, biome); const center = ouroboros.arenaRadius; const tileSize = biome.tileSize; expect(arena.bossSpawnX).toBe(center * tileSize + tileSize / 2); expect(arena.bossSpawnY).toBe(center * tileSize + tileSize / 2); }); it('has player spawn south of center', () => { const arena = generateArena(ouroboros, biome); expect(arena.playerSpawnY).toBeGreaterThan(arena.bossSpawnY); }); it('places 4 resource deposits', () => { const arena = generateArena(ouroboros, biome); expect(arena.resourcePositions).toHaveLength(4); }); it('resource positions are within arena bounds', () => { const arena = generateArena(ouroboros, biome); const maxPixel = arena.width * biome.tileSize; for (const pos of arena.resourcePositions) { expect(pos.x).toBeGreaterThan(0); expect(pos.x).toBeLessThan(maxPixel); expect(pos.y).toBeGreaterThan(0); expect(pos.y).toBeLessThan(maxPixel); } }); it('has acid tiles in the arena', () => { const arena = generateArena(ouroboros, biome); let acidCount = 0; for (const row of arena.grid) { for (const tile of row) { if (tile === 2) acidCount++; // ACID_SHALLOW } } expect(acidCount).toBeGreaterThan(0); }); it('buildArenaWalkableSet includes expected tiles', () => { const walkable = buildArenaWalkableSet(); expect(walkable.has(0)).toBe(true); // GROUND expect(walkable.has(1)).toBe(true); // SCORCHED_EARTH expect(walkable.has(6)).toBe(true); // MINERAL_VEIN expect(walkable.has(7)).toBe(false); // BEDROCK (not walkable) expect(walkable.has(4)).toBe(false); // CRYSTAL (not walkable) }); }); // ─── Reward System ─────────────────────────────────────────────── describe('Boss Reward System', () => { it('calculates base reward for direct victory', () => { const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Direct; const reward = calculateBossReward(state, ouroboros); expect(reward.spores).toBe(100); // Base expect(reward.loreId).toBe('ouroboros'); expect(reward.victoryMethod).toBe(VictoryMethod.Direct); }); it('gives 50% bonus for chemical victory', () => { const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Chemical; const reward = calculateBossReward(state, ouroboros); expect(reward.spores).toBe(150); // 100 * 1.5 }); it('gives 100% bonus for catalytic victory', () => { const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Catalytic; const reward = calculateBossReward(state, ouroboros); expect(reward.spores).toBe(200); // 100 * 2.0 }); it('returns no spores if boss not defeated', () => { const state = createBossState(ouroboros); const reward = calculateBossReward(state, ouroboros); expect(reward.spores).toBe(0); }); it('includes lore text in reward', () => { const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Direct; const reward = calculateBossReward(state, ouroboros); expect(reward.loreText.length).toBeGreaterThan(0); expect(reward.loreTextRu.length).toBeGreaterThan(0); }); it('applyBossReward adds spores to meta', () => { const meta = createMetaState(); const initialSpores = meta.spores; const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Direct; const reward = calculateBossReward(state, ouroboros); applyBossReward(meta, reward, 1); expect(meta.spores).toBe(initialSpores + reward.spores); }); it('applyBossReward adds lore to codex', () => { const meta = createMetaState(); const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Direct; const reward = calculateBossReward(state, ouroboros); applyBossReward(meta, reward, 1); const bossEntry = meta.codex.find(e => e.id === 'ouroboros'); expect(bossEntry).toBeDefined(); expect(bossEntry?.type).toBe('boss'); expect(bossEntry?.discoveredOnRun).toBe(1); }); it('does not add duplicate codex entries', () => { const meta = createMetaState(); const state = createBossState(ouroboros); state.defeated = true; state.victoryMethod = VictoryMethod.Direct; const reward = calculateBossReward(state, ouroboros); applyBossReward(meta, reward, 1); applyBossReward(meta, reward, 2); const bossEntries = meta.codex.filter(e => e.id === 'ouroboros'); expect(bossEntries).toHaveLength(1); }); }); // ─── Boss Entity Factory ───────────────────────────────────────── describe('Boss Entity Factory', () => { let world: World; beforeEach(() => { world = createWorld(); }); it('creates an entity with Position component', () => { const eid = createBossEntity(world, ouroboros, 400, 300); expect(Position.x[eid]).toBe(400); expect(Position.y[eid]).toBe(300); }); it('creates an entity with Health component', () => { const eid = createBossEntity(world, ouroboros, 400, 300); expect(Health.current[eid]).toBe(300); expect(Health.max[eid]).toBe(300); }); it('creates an entity with SpriteRef component', () => { const eid = createBossEntity(world, ouroboros, 400, 300); expect(SpriteRef.color[eid]).toBe(0xcc44ff); expect(SpriteRef.radius[eid]).toBe(20); }); it('creates an entity with Boss component', () => { const eid = createBossEntity(world, ouroboros, 400, 300); expect(Boss.dataIndex[eid]).toBe(0); expect(Boss.phase[eid]).toBe(0); // BossPhase.Coil expect(Boss.cycleCount[eid]).toBe(0); }); it('entity is queryable by Boss component', () => { const eid = createBossEntity(world, ouroboros, 400, 300); const bosses = query(world, [Boss]); expect(bosses).toContain(eid); }); }); // ─── Integration: Full Boss Fight ──────────────────────────────── describe('Boss Fight Integration', () => { it('can defeat boss with direct damage during Digest windows', () => { const state = createBossState(ouroboros); let totalCycles = 0; // Simulate multiple cycles of hitting during Digest // In a 4s Digest window, player can throw ~10 projectiles while (!state.defeated && totalCycles < 30) { // Advance through Coil → Spray → Lash → Digest updateBossPhase(state, ouroboros, 5000); // Coil (+ regen) updateBossPhase(state, ouroboros, 8000); // Spray (+ regen) updateBossPhase(state, ouroboros, 6000); // Lash (+ regen) // Now in Digest — rapid-fire attacks (10 per 4s window is realistic) for (let i = 0; i < 10 && !state.defeated; i++) { state.currentPhase = BossPhase.Digest; // Ensure in Digest applyBossDamage(state, ouroboros, 'Fe'); } updateBossPhase(state, ouroboros, 4000); // Complete Digest totalCycles++; } expect(state.defeated).toBe(true); expect(state.victoryMethod).toBe(VictoryMethod.Direct); // Direct path is slow but viable (boss regens 5 HP/s between windows) expect(totalCycles).toBeLessThan(30); }); it('can defeat boss faster with NaOH during Spray', () => { const state = createBossState(ouroboros); let hits = 0; // Advance to Spray phase and keep hitting with NaOH state.currentPhase = BossPhase.Spray; while (!state.defeated && hits < 50) { applyBossDamage(state, ouroboros, 'NaOH'); hits++; } expect(state.defeated).toBe(true); expect(state.victoryMethod).toBe(VictoryMethod.Chemical); expect(hits).toBeLessThan(20); // NaOH should be efficient }); it('Hg makes boss progressively weaker', () => { const state = createBossState(ouroboros); // Apply 3 Hg stacks for (let i = 0; i < 3; i++) { applyBossDamage(state, ouroboros, 'Hg'); } expect(state.catalystStacks).toBe(3); // Regen should be 0 (5 - 3*2 = -1 → 0) expect(getEffectiveRegen(state, ouroboros)).toBe(0); // Armor should be reduced: 0.5 - 3*0.1 = 0.2 expect(getEffectiveArmor(state, ouroboros)).toBeCloseTo(0.2); }); it('catalyst poison + direct damage is a viable strategy', () => { const state = createBossState(ouroboros); // Apply 3 Hg stacks to maximize weakness for (let i = 0; i < 3; i++) { applyBossDamage(state, ouroboros, 'Hg'); } // Now kill with direct damage during Digest state.currentPhase = BossPhase.Digest; let hits = 0; while (!state.defeated && hits < 50) { applyBossDamage(state, ouroboros, 'Fe'); hits++; } expect(state.defeated).toBe(true); // Victory method should be Direct (killing blow was Fe) expect(state.victoryMethod).toBe(VictoryMethod.Direct); }); });