phase 8: Ouroboros boss fight — pattern puzzle with 3 victory paths

First Archon encounter: a cyclical pattern-recognition puzzle.

Boss AI: 4-phase cycle (Coil → Spray → Lash → Digest) with
escalating difficulty (10% faster per cycle, caps at 5 cycles).

Victory paths (all based on real chemistry):
- Chemical: NaOH during Spray phase (acid-base neutralization, 3x dmg)
- Direct: any projectile during Digest vulnerability window
- Catalytic: Hg poison stacks (mercury poisons catalytic sites, reduces
  regen+armor permanently)

New files: src/boss/ (types, ai, victory, arena, factory, reward),
src/data/bosses.json, src/scenes/BossArenaScene.ts, tests/boss.test.ts

Extended: ECS Boss component, CodexEntry 'boss' type, GameScene
triggers arena on Resolution phase completion.

70 new tests (455 total), all passing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Денис Шкабатур
2026-02-12 16:12:34 +03:00
parent 0d35cdcc73
commit 7d52d749a3
14 changed files with 2429 additions and 4 deletions

758
tests/boss.test.ts Normal file
View File

@@ -0,0 +1,758 @@
/**
* 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);
});
});