/** * 1.13c Numerical & Runtime Verification Suite — Skill #028: Dopplezon (AMA) * Gitea Tracking Issue: #182 */ import { describe, expect, it } from 'vitest' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { UnitStatList } from '../../../src/server/engine/stat-list.ts' import { WorldArena } from '../../../src/server/engine/world-arena.ts' import { skillModule } from '../../../src/server/skills/impl/ama/skill-028-dopplezon.ts' import { calculateDecoyStats } from '../../../src/common/skills/amazon-passive.ts' describe('[Skill #028] Dopplezon (AMA) — 1.13c Parity (Issue #182)', () => { it('evaluates exact 1.13c Skills.txt / Missiles.txt formulas at slvl 1, 10, and 20', async () => { const registry = await getSharedDataRegistry() const skillRec = registry.getSkillById(28)! expect(skillRec).toBeDefined() expect(skillModule.skillId).toBe(28) expect(skillModule.name).toBe("Dopplezon") expect(skillModule.charClass).toBe("ama") const stats1 = new UnitStatList() stats1.setBaseSkillLevel(28, 1) const r1 = skillModule.evaluate!({ registry, skill: skillRec, slvl: 1, blvl: 1, statList: stats1 }) expect(r1.manaCost).toBe(19) expect(r1.manaCost256).toBe(4864) expect(r1.minPhysDmg).toBe(0) expect(r1.maxPhysDmg).toBe(0) expect(r1.minElemDmg256).toBe(0) expect(r1.maxElemDmg256).toBe(0) const stats10 = new UnitStatList() stats10.setBaseSkillLevel(28, 10) const r10 = skillModule.evaluate!({ registry, skill: skillRec, slvl: 10, blvl: 10, statList: stats10 }) expect(r10.manaCost).toBe(12) expect(r10.manaCost256).toBe(3136) expect(r10.minPhysDmg).toBe(0) expect(r10.maxPhysDmg).toBe(0) expect(r10.minElemDmg256).toBe(0) expect(r10.maxElemDmg256).toBe(0) const stats20 = new UnitStatList() stats20.setBaseSkillLevel(28, 20) const r20 = skillModule.evaluate!({ registry, skill: skillRec, slvl: 20, blvl: 20, statList: stats20 }) expect(r20.manaCost).toBe(4) expect(r20.manaCost256).toBe(1216) expect(r20.minPhysDmg).toBe(0) expect(r20.maxPhysDmg).toBe(0) expect(r20.minElemDmg256).toBe(0) expect(r20.maxElemDmg256).toBe(0) expect(r20.durationFrames).toBe(0) expect(r20.radiusSubtiles).toBe(2625) expect(r20.petMax).toBe(1) expect(r20.cooldownFrames).toBe(0) }) it('enforces 1.13c blvl synergy rule (hard points boost synergy; +skills and item charges do NOT)', async () => { const registry = await getSharedDataRegistry() const skillRec = registry.getSkillById(28)! const synId = null const statsBase = new UnitStatList() statsBase.setBaseSkillLevel(28, 20) if (synId !== null) { statsBase.setBaseSkillLevel(synId, 20) } const withHardPoints = skillModule.evaluate!({ registry, skill: skillRec, slvl: 20, blvl: 20, statList: statsBase, }) expect(withHardPoints.synergyBonusPct).toBe(0) // +20 allskills and +33 Marrowwalk charges without hard points must yield 0 synergy const statsSoftOnly = new UnitStatList() statsSoftOnly.setBaseSkillLevel(28, 20) statsSoftOnly.addStat('item_allskills', 20) if (synId !== null) { statsSoftOnly.setChargedSkillLevel(synId, 33) } const withSoftOnly = skillModule.evaluate!({ registry, skill: skillRec, slvl: 20, blvl: 20, statList: statsSoftOnly, }) expect(withSoftOnly.synergyBonusPct).toBe(0) }) it('executes runtime skill effect in WorldArena (castCount, action frame, and combat/state/missile/summon outcome)', async () => { const registry = await getSharedDataRegistry() const arena = new WorldArena({ registry, classCode: "ama", skillId: 28, slvl: 20, }) const outcome = arena.triggerCast() arena.stepTicks(15) const dbg = arena.getDebugState() expect(dbg.ready).toBe(true) expect(dbg.skillId).toBe(28) expect(dbg.castCount).toBeGreaterThanOrEqual(1) expect(dbg.actionFrameTriggered).toBe(true) expect(outcome?.executed).toBe(true) }) it('validates 1.13c calculateDecoyStats pure calculations and boundary invariants', () => { // slvl 1: 250 frames (10.0s), 60% of Amazon life, 4% res, defense = amazonDef, stationary, draws threat, 19 mana (4864) const stats1 = calculateDecoyStats(1, 500, 150, { avoidLvl: 5, evadeLvl: 3 }) expect(stats1.durationFrames).toBe(250) expect(stats1.durationSeconds).toBe(10) expect(stats1.hp).toBe(300) // 500 * 0.6 expect(stats1.hpPctOfAmazon).toBe(60) expect(stats1.allResistancesPct).toBe(4) expect(stats1.defense).toBe(150) expect(stats1.isStationary).toBe(true) expect(stats1.drawsThreat).toBe(true) expect(stats1.inheritedAvoidLvl).toBe(5) expect(stats1.inheritedEvadeLvl).toBe(3) expect(stats1.manaCost).toBe(19) expect(stats1.manaCost256).toBe(4864) // slvl 10: 250 + 125*9 = 1375 frames (55.0s), 150% Amazon life, 40% res, 10 mana (2560) or 12 int mana const stats10 = calculateDecoyStats(10, 500, 200) expect(stats10.durationFrames).toBe(1375) expect(stats10.durationSeconds).toBe(55) expect(stats10.hp).toBe(750) // 500 * 1.5 expect(stats10.allResistancesPct).toBe(40) // slvl 20: 250 + 125*19 = 2625 frames (105.0s), 250% Amazon life, capped at 80% res (min(85, 4*20)=80), min mana 1 const stats20 = calculateDecoyStats(20, 500, 300) expect(stats20.durationFrames).toBe(2625) expect(stats20.durationSeconds).toBe(105) expect(stats20.hp).toBe(1250) // 500 * 2.5 expect(stats20.allResistancesPct).toBe(80) // slvl 25: 4*25 = 100% -> capped at 85% const stats25 = calculateDecoyStats(25, 500, 300) expect(stats25.allResistancesPct).toBe(85) }) })