/** * Issue #436: Hydra (#62) Summon Cap & Entity Visuals * * 1.13c Ground Truth Invariants Verification Suite: * 1. Summon Cap: Maximum 6 active Hydra groups (18 heads total) concurrently active. * 2. FIFO Eviction: When 7th group is cast, the oldest 1st group is automatically evicted (pruned to 0 ttl). * 3. Exact 3-Head Offsets: Offsets [(-6, -11), (0, -7), (4, -15)] correctly assigned per group. * 4. Slvl Scaling: Hydra damage properly respects caster skill level instead of hardcoded slvl 1. */ import { describe, expect, it } from 'vitest' import { GameEngine } from '../../../src/server/engine/engine.ts' import { castSkill } from '../../../src/server/scene/skill-caster.ts' describe('Issue #436: Hydra (#62) Summon Cap & Entity Visuals', () => { it('enforces maximum 6 groups (18 heads) and FIFO eviction on successive casts', () => { const engine = new GameEngine( { widthPx: 1000, heightPx: 1000, overlap: () => 0 }, { spawn: { x: 200, y: 200 }, stats: [], questDefs: [], npcDefs: [] } as any, ) engine.world.player.x = 200 engine.world.player.y = 200 engine.world.player.mana = 1000 const statusEl = { textContent: '' } as HTMLElement const dummyRuntime = { walkable: { isWalkable: () => true }, collision: { isWalkable: () => true }, } as any // Cast Hydra 6 times (filling cap to 6 groups = 18 heads) for (let c = 1; c <= 6; c++) { engine.world.player.cooldown = 0 const ok = castSkill(62, 300 + c * 20, 300, { engine, runtime: dummyRuntime, hudManager: null, status: statusEl, }) expect(ok).toBe(true) expect(engine.projectiles.length).toBe(c * 3) } expect(engine.projectiles.length).toBe(18) const firstGroupHeadX = engine.projectiles[0]!.x // Cast 7th time -> triggers FIFO eviction of group 1 engine.world.player.cooldown = 0 const ok7 = castSkill(62, 500, 300, { engine, runtime: dummyRuntime, hudManager: null, status: statusEl, }) expect(ok7).toBe(true) // Total active heads must be capped at 18 (6 groups * 3 heads) expect(engine.projectiles.length).toBe(18) // First group must have been evicted and no longer present in engine.projectiles const containsOldHead = engine.projectiles.some(p => p.x === firstGroupHeadX) expect(containsOldHead).toBe(false) }) it('correctly reads slvl from hudManager for damage calculation', () => { const engine = new GameEngine( { widthPx: 1000, heightPx: 1000, overlap: () => 0 }, { spawn: { x: 200, y: 200 }, stats: [], questDefs: [], npcDefs: [] } as any, ) engine.world.player.x = 200 engine.world.player.y = 200 engine.world.player.mana = 1000 engine.world.player.cooldown = 0 const mockHudManager = { getSkillLevel: (skillId: number) => (skillId === 62 ? 20 : 0), syncPublishedState: () => {}, } const statusEl = { textContent: '' } as HTMLElement const dummyRuntime = { walkable: { isWalkable: () => true }, collision: { isWalkable: () => true }, } as any castSkill(62, 350, 250, { engine, runtime: dummyRuntime, hudManager: mockHudManager as any, status: statusEl, }) expect(engine.projectiles.length).toBe(3) // At slvl 20, damage is ~162 (150-175 range), much higher than slvl 1 (14-19) const head = engine.projectiles[0]! expect(head.damage).toBeGreaterThan(100) }) })