import { describe, expect, it } from 'vitest' import { calculateFirestormStats, calculateMoltenBoulderStats, calculateArcticBlastStats, calculateFissureStats, calculateCycloneArmorStats, calculateTwisterStats, calculateVolcanoStats, calculateTornadoStats, calculateArmageddonStats, calculateHurricaneStats, simulateCycloneArmorAbsorption, calculateFirestormPathPoint, simulateTornadoWanderStep, calculateArmageddonImpactLocation, } from '../../../src/server/skills/druid-elemental.ts' import { BATCH1_SKILLS } from '../../../src/server/skills/skills.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { UnitStatList } from '../../../src/server/engine/stat-list.ts' import { evaluateSkill113c, getRegisteredSkillModule } from '../../../src/server/skills/registry.ts' describe('Milestone M22 — Druid Elemental Skills Parity & Adversarial Stress Suite', () => { // ========================================================================= // Dimension 1: Exact 1.13c Numerical Scaling & Level Progressions // ========================================================================= describe('Dimension 1: Exact 1.13c Numerical Scaling & Formulas', () => { it('1.1 Firestorm (225): Evaluates exact fire damage, 3-snake count, and +23%/blvl synergy', () => { // slvl 1 const fs1 = calculateFirestormStats(1, 0, 0) expect(fs1.minFireDamage).toBe(3) expect(fs1.maxFireDamage).toBe(6) expect(fs1.snakesCount).toBe(3) expect(fs1.manaCost).toBe(4) expect(fs1.manaCost256).toBe(1024) expect(fs1.cooldownFrames).toBe(15) expect(fs1.cooldownSeconds).toBe(0.6) expect(fs1.castOverlay).toBe('druid_fire_cast_1') // slvl 10: 3 + 7*3 + 2*5 = 34 min, 6 + 7*3 + 2*6 = 39 max const fs10 = calculateFirestormStats(10, 0, 0) expect(fs10.minFireDamage).toBe(34) expect(fs10.maxFireDamage).toBe(39) // slvl 20: 3 + 7*3 + 8*5 + 4*7 = 92 min, 6 + 7*3 + 8*6 + 4*8 = 107 max const fs20 = calculateFirestormStats(20, 0, 0) expect(fs20.minFireDamage).toBe(92) expect(fs20.maxFireDamage).toBe(107) // Synergy: 10 in Molten Boulder + 10 in Fissure = +460% fire damage const fs20Syn = calculateFirestormStats(20, 10, 10) expect(fs20Syn.synergyBonusPct).toBe(460) expect(fs20Syn.minFireDamage).toBe(Math.trunc((92 * 560) / 100)) // 515 expect(fs20Syn.maxFireDamage).toBe(Math.trunc((107 * 560) / 100)) // 599 }) it('1.2 Molten Boulder (229): Evaluates dual physical/fire damage and fixed-point mana scaling', () => { // slvl 1 const mb1 = calculateMoltenBoulderStats(1, 0, 0) expect(mb1.minPhysicalDamage).toBe(6) expect(mb1.maxPhysicalDamage).toBe(12) expect(mb1.minFireDamage).toBe(6) expect(mb1.maxFireDamage).toBe(12) expect(mb1.manaCost).toBe(10) expect(mb1.manaCost256).toBe(2560) expect(mb1.cooldownFrames).toBe(50) expect(mb1.knockbackRadius).toBe(7) // slvl 10: base: 6 + 7*4 + 2*7 = 48 min, 12 + 7*5 + 2*8 = 63 max // mana: (20 + 9) << 7 = 3712 -> manaCost = 14 const mb10 = calculateMoltenBoulderStats(10, 0, 0) expect(mb10.minPhysicalDamage).toBe(48) expect(mb10.maxPhysicalDamage).toBe(63) expect(mb10.minFireDamage).toBe(48) expect(mb10.maxFireDamage).toBe(63) expect(mb10.manaCost).toBe(14) expect(mb10.manaCost256).toBe(3712) // slvl 20: base: 48 + 6*7 + 4*10 = 130 min, 63 + 6*8 + 4*11 = 155 max // mana: (20 + 19) << 7 = 4992 -> manaCost = 19 const mb20 = calculateMoltenBoulderStats(20, 0, 0) expect(mb20.minPhysicalDamage).toBe(130) expect(mb20.maxPhysicalDamage).toBe(155) expect(mb20.manaCost).toBe(19) expect(mb20.manaCost256).toBe(4992) // Synergies: Volcano (+10%/lvl phys), Firestorm (+8%/lvl fire) const mb20Syn = calculateMoltenBoulderStats(20, 20, 20) expect(mb20Syn.physSynergyBonusPct).toBe(200) expect(mb20Syn.fireSynergyBonusPct).toBe(160) expect(mb20Syn.minPhysicalDamage).toBe(130 * 3) // 390 expect(mb20Syn.maxPhysicalDamage).toBe(155 * 3) // 465 expect(mb20Syn.minFireDamage).toBe(Math.trunc((130 * 260) / 100)) // 338 expect(mb20Syn.maxFireDamage).toBe(Math.trunc((155 * 260) / 100)) // 403 }) it('1.3 Arctic Blast (230): Evaluates continuous channeled cold damage, expanding arc, and chill scaling', () => { // slvl 1 const ab1 = calculateArcticBlastStats(1, 0, 0) expect(ab1.minColdDamagePerSecond).toBe(21) expect(ab1.maxColdDamagePerSecond).toBe(40) expect(ab1.chillDurationFrames).toBe(100) // 4s expect(ab1.chillDurationSeconds).toBe(4.0) expect(ab1.rangeYards).toBe(6.75) // (35/4) - 2 = 8.75 - 2 = 6.75 expect(ab1.manaCostPerTick256).toBe(96) // 24 << 2 expect(ab1.isChanneled).toBe(true) // slvl 10: 21 + 7*16 + 2*18 = 169 min, 40 + 7*16 + 2*19 = 190 max const ab10 = calculateArcticBlastStats(10, 0, 0) expect(ab10.minColdDamagePerSecond).toBe(169) expect(ab10.maxColdDamagePerSecond).toBe(190) expect(ab10.chillDurationFrames).toBe(235) // 100 + 9*15 = 235 (9.4s) expect(ab10.manaCostPerTick256).toBe(132) // (24 + 9) << 2 = 132 // slvl 20: 169 + 6*18 + 4*20 = 357 min, 190 + 6*19 + 4*21 = 388 max const ab20 = calculateArcticBlastStats(20, 0, 0) expect(ab20.minColdDamagePerSecond).toBe(357) expect(ab20.maxColdDamagePerSecond).toBe(388) expect(ab20.chillDurationFrames).toBe(385) // 100 + 19*15 = 385 (15.4s) expect(ab20.manaCostPerTick256).toBe(172) // (24 + 19) << 2 = 172 // Synergy: Cyclone Armor + Hurricane (+15% / blvl each) const ab20Syn = calculateArcticBlastStats(20, 20, 20) expect(ab20Syn.synergyBonusPct).toBe(600) expect(ab20Syn.minColdDamagePerSecond).toBe(357 * 7) // 2499 expect(ab20Syn.maxColdDamagePerSecond).toBe(388 * 7) // 2716 }) it('1.4 Eruption / Fissure (234): Evaluates geological fire damage and duration', () => { // slvl 1 const f1 = calculateFissureStats(1, 0, 0) expect(f1.minFireDamage).toBe(15) expect(f1.maxFireDamage).toBe(25) expect(f1.durationFrames).toBe(80) // 3.2s expect(f1.cooldownFrames).toBe(50) // 2.0s expect(f1.manaCost).toBe(15) expect(f1.manaCost256).toBe(3840) // slvl 10: 15 + 7*6 + 2*12 = 81 min, 25 + 7*6 + 2*12 = 91 max const f10 = calculateFissureStats(10, 0, 0) expect(f10.minFireDamage).toBe(81) expect(f10.maxFireDamage).toBe(91) // slvl 20: 81 + 6*12 + 4*16 = 217 min, 91 + 6*12 + 4*16 = 227 max const f20 = calculateFissureStats(20, 0, 0) expect(f20.minFireDamage).toBe(217) expect(f20.maxFireDamage).toBe(227) // Synergies: Firestorm + Volcano (+12%/lvl) const f20Syn = calculateFissureStats(20, 20, 20) expect(f20Syn.synergyBonusPct).toBe(480) expect(f20Syn.minFireDamage).toBe(Math.trunc((217 * 580) / 100)) // 1258 expect(f20Syn.maxFireDamage).toBe(Math.trunc((227 * 580) / 100)) // 1316 }) it('1.5 Cyclone Armor (235): Evaluates base absorption, synergy scaling, and mana progression', () => { // slvl 1 const ca1 = calculateCycloneArmorStats(1, 0, 0, 0) expect(ca1.baseAbsorption).toBe(40) expect(ca1.totalAbsorption).toBe(40) expect(ca1.totalAbsorption256).toBe(40 << 8) expect(ca1.manaCost).toBe(5) expect(ca1.manaCost256).toBe(1280) expect(ca1.absorbedElements).toEqual(['fire', 'cold', 'lightning']) expect(ca1.unabsorbedElements).toEqual(['physical', 'magic', 'poison']) // slvl 10: 40 + 9*12 = 148, mana: 5 + 9 = 14 const ca10 = calculateCycloneArmorStats(10, 0, 0, 0) expect(ca10.baseAbsorption).toBe(148) expect(ca10.totalAbsorption).toBe(148) expect(ca10.manaCost).toBe(14) expect(ca10.manaCost256).toBe(3584) // slvl 20: 40 + 19*12 = 268, mana: 5 + 19 = 24 const ca20 = calculateCycloneArmorStats(20, 0, 0, 0) expect(ca20.baseAbsorption).toBe(268) expect(ca20.totalAbsorption).toBe(268) expect(ca20.manaCost).toBe(24) expect(ca20.manaCost256).toBe(6144) // Synergies: Twister (20) + Tornado (20) + Hurricane (20) = 60 * 7% = +420% const ca20Syn = calculateCycloneArmorStats(20, 20, 20, 20) expect(ca20Syn.synergyBonusPct).toBe(420) expect(ca20Syn.totalAbsorption).toBe(Math.trunc((268 * 520) / 100)) // 1393 }) it('1.6 Twister (240): Evaluates 3-way dispersion, physical damage, and 10-frame stun', () => { // slvl 1 const tw1 = calculateTwisterStats(1, 0, 0) expect(tw1.minPhysicalDamage).toBe(12) expect(tw1.maxPhysicalDamage).toBe(16) expect(tw1.twisterCount).toBe(3) expect(tw1.stunDurationFrames).toBe(10) expect(tw1.stunDurationSeconds).toBe(0.4) expect(tw1.manaCost).toBe(7) expect(tw1.manaCost256).toBe(1792) // slvl 10: 12 + 7*4 + 2*7 = 54 min, 16 + 7*4 + 2*7 = 58 max const tw10 = calculateTwisterStats(10, 0, 0) expect(tw10.minPhysicalDamage).toBe(54) expect(tw10.maxPhysicalDamage).toBe(58) // slvl 20: 54 + 6*7 + 4*10 = 136 min, 58 + 6*7 + 4*11 = 144 max const tw20 = calculateTwisterStats(20, 0, 0) expect(tw20.minPhysicalDamage).toBe(136) expect(tw20.maxPhysicalDamage).toBe(144) // Synergies: Tornado + Hurricane (+10%/lvl each) const tw20Syn = calculateTwisterStats(20, 20, 20) expect(tw20Syn.synergyBonusPct).toBe(400) expect(tw20Syn.minPhysicalDamage).toBe(136 * 5) // 680 expect(tw20Syn.maxPhysicalDamage).toBe(144 * 5) // 720 }) it('1.7 Volcano (244): Evaluates physical and fire damage split and cooldown', () => { // slvl 1 const v1 = calculateVolcanoStats(1, 0, 0, 0) expect(v1.minPhysicalDamage).toBe(8) expect(v1.maxPhysicalDamage).toBe(10) expect(v1.minFireDamage).toBe(8) expect(v1.maxFireDamage).toBe(10) expect(v1.cooldownFrames).toBe(100) expect(v1.cooldownSeconds).toBe(4.0) expect(v1.manaCost).toBe(25) expect(v1.manaCost256).toBe(6400) // slvl 10: phys: 8 + 7*2 + 2*4 = 30 min, 10 + 7*2 + 2*4 = 32 max // fire: same at slvl 10 const v10 = calculateVolcanoStats(10, 0, 0, 0) expect(v10.minPhysicalDamage).toBe(30) expect(v10.maxPhysicalDamage).toBe(32) expect(v10.minFireDamage).toBe(30) expect(v10.maxFireDamage).toBe(32) // slvl 20: phys: 30 + 6*4 + 4*6 = 78 min, 32 + 6*4 + 4*6 = 80 max const v20 = calculateVolcanoStats(20, 0, 0, 0) expect(v20.minPhysicalDamage).toBe(78) expect(v20.maxPhysicalDamage).toBe(80) // Synergies: Molten Boulder (+12% phys), Eruption & Armageddon (+12% fire) const v20Syn = calculateVolcanoStats(20, 20, 20, 20) expect(v20Syn.physSynergyBonusPct).toBe(240) expect(v20Syn.fireSynergyBonusPct).toBe(480) expect(v20Syn.minPhysicalDamage).toBe(Math.trunc((78 * 340) / 100)) // 265 expect(v20Syn.minFireDamage).toBe(Math.trunc((78 * 580) / 100)) // 452 }) it('1.8 Tornado (245): Evaluates heavy physical vortex damage and NextDelay', () => { // slvl 1 const tor1 = calculateTornadoStats(1, 0, 0, 0) expect(tor1.minPhysicalDamage).toBe(25) expect(tor1.maxPhysicalDamage).toBe(35) expect(tor1.nextDelayFrames).toBe(25) expect(tor1.manaCost).toBe(10) expect(tor1.manaCost256).toBe(2560) // slvl 10: 25 + 7*8 + 2*14 = 109 min, 35 + 7*8 + 2*15 = 121 max const tor10 = calculateTornadoStats(10, 0, 0, 0) expect(tor10.minPhysicalDamage).toBe(109) expect(tor10.maxPhysicalDamage).toBe(121) // slvl 20: 109 + 6*14 + 4*20 = 273 min, 121 + 6*15 + 4*21 = 295 max const tor20 = calculateTornadoStats(20, 0, 0, 0) expect(tor20.minPhysicalDamage).toBe(273) expect(tor20.maxPhysicalDamage).toBe(295) // Synergies: Cyclone Armor + Twister + Hurricane (+9% each / blvl) const tor20Syn = calculateTornadoStats(20, 20, 20, 20) expect(tor20Syn.synergyBonusPct).toBe(540) expect(tor20Syn.minPhysicalDamage).toBe(Math.trunc((273 * 640) / 100)) // 1747 expect(tor20Syn.maxPhysicalDamage).toBe(Math.trunc((295 * 640) / 100)) // 1888 }) it('1.9 Armageddon (249): Evaluates fire + physical meteor rain and duration scaling', () => { // slvl 1 const arm1 = calculateArmageddonStats(1, 0, 0, 0, 0) expect(arm1.minPhysicalDamage).toBe(4) expect(arm1.maxPhysicalDamage).toBe(16) expect(arm1.minFireDamage).toBe(25) expect(arm1.maxFireDamage).toBe(75) expect(arm1.durationFrames).toBe(250) // 10.0s expect(arm1.radiusYards).toBe(8) expect(arm1.cooldownFrames).toBe(150) // 6.0s expect(arm1.manaCost).toBe(35) expect(arm1.manaCost256).toBe(8960) // Duration synergy: +50 frames (2.0s) per blvl in Eruption (Fissure) const arm1WithFissure = calculateArmageddonStats(1, 15, 0, 0, 0) expect(arm1WithFissure.durationFrames).toBe(250 + 15 * 50) // 1000 frames = 40s expect(arm1WithFissure.durationSeconds).toBe(40.0) // slvl 20 fire damage: 25 + 7*15 + 8*20 + 4*25 = 390 min, 75 + 7*16 + 8*22 + 4*27 = 471 max const arm20 = calculateArmageddonStats(20, 0, 0, 0, 0) expect(arm20.minFireDamage).toBe(390) expect(arm20.maxFireDamage).toBe(471) // Damage synergy: Firestorm, Molten Boulder, Volcano (+14% each / blvl) const arm20Syn = calculateArmageddonStats(20, 0, 20, 20, 20) expect(arm20Syn.fireSynergyBonusPct).toBe(840) // 60 * 14 expect(arm20Syn.minFireDamage).toBe(Math.trunc((390 * 940) / 100)) // 3666 expect(arm20Syn.maxFireDamage).toBe(Math.trunc((471 * 940) / 100)) // 4427 }) it('1.10 Hurricane (250): Evaluates orbiting blizzard cold damage, chill, and duration scaling', () => { // slvl 1 const hurr1 = calculateHurricaneStats(1, 0, 0, 0) expect(hurr1.minColdDamage).toBe(25) expect(hurr1.maxColdDamage).toBe(50) expect(hurr1.chillDurationFrames).toBe(50) // 2.0s expect(hurr1.durationFrames).toBe(250) // 10.0s expect(hurr1.radiusYards).toBe(9) expect(hurr1.cooldownFrames).toBe(150) // 6.0s expect(hurr1.manaCost).toBe(30) expect(hurr1.manaCost256).toBe(7680) // Duration synergy: +50 frames (2.0s) per blvl in Cyclone Armor const hurr1WithArmor = calculateHurricaneStats(1, 20, 0, 0) expect(hurr1WithArmor.durationFrames).toBe(250 + 20 * 50) // 1250 frames = 50s expect(hurr1WithArmor.durationSeconds).toBe(50.0) // slvl 20 cold damage: 25 + 7*7 + 8*10 + 4*12 = 202 min, 50 + 7*7 + 8*10 + 4*12 = 227 max const hurr20 = calculateHurricaneStats(20, 0, 0, 0) expect(hurr20.minColdDamage).toBe(202) expect(hurr20.maxColdDamage).toBe(227) // Damage synergy: Twister + Tornado (+9% each / blvl) const hurr20Syn = calculateHurricaneStats(20, 0, 20, 20) expect(hurr20Syn.coldSynergyBonusPct).toBe(360) expect(hurr20Syn.minColdDamage).toBe(Math.trunc((202 * 460) / 100)) // 929 expect(hurr20Syn.maxColdDamage).toBe(Math.trunc((227 * 460) / 100)) // 1044 }) }) // ========================================================================= // Dimension 2: Cyclone Armor Strict Absorption Matrix & Edge Cases // ========================================================================= describe('Dimension 2: Cyclone Armor Strict Absorption Matrix & Edge Cases', () => { it('2.1 Strictly absorbs Fire, Cold, and Lightning elemental damage', () => { const initialCapacity = 500 // Fire damage const fireRes = simulateCycloneArmorAbsorption(initialCapacity, 150, 'fire') expect(fireRes.absorbed).toBe(150) expect(fireRes.penetrated).toBe(0) expect(fireRes.remainingArmor).toBe(350) expect(fireRes.didDissipate).toBe(false) // Cold damage const coldRes = simulateCycloneArmorAbsorption(fireRes.remainingArmor, 200, 'cold') expect(coldRes.absorbed).toBe(200) expect(coldRes.penetrated).toBe(0) expect(coldRes.remainingArmor).toBe(150) expect(coldRes.didDissipate).toBe(false) // Lightning damage const ltngRes = simulateCycloneArmorAbsorption(coldRes.remainingArmor, 100, 'lightning') expect(ltngRes.absorbed).toBe(100) expect(ltngRes.penetrated).toBe(0) expect(ltngRes.remainingArmor).toBe(50) expect(ltngRes.didDissipate).toBe(false) }) it('2.2 Passes Physical, Magic, and Poison damage straight through with zero absorption', () => { const initialCapacity = 500 // Physical damage (e.g. crushing blow, arrow) const physRes = simulateCycloneArmorAbsorption(initialCapacity, 250, 'physical') expect(physRes.absorbed).toBe(0) expect(physRes.penetrated).toBe(250) expect(physRes.remainingArmor).toBe(500) expect(physRes.didDissipate).toBe(false) // Magic damage (e.g. Bone Spear, Blessed Hammer) const magRes = simulateCycloneArmorAbsorption(initialCapacity, 300, 'magic') expect(magRes.absorbed).toBe(0) expect(magRes.penetrated).toBe(300) expect(magRes.remainingArmor).toBe(500) expect(magRes.didDissipate).toBe(false) // Poison damage (e.g. Poison Nova) const psnRes = simulateCycloneArmorAbsorption(initialCapacity, 400, 'poison') expect(psnRes.absorbed).toBe(0) expect(psnRes.penetrated).toBe(400) expect(psnRes.remainingArmor).toBe(500) expect(psnRes.didDissipate).toBe(false) }) it('2.3 Depletion & Dissipation: Absorbs up to remaining capacity and passes remaining damage to player', () => { const initialCapacity = 100 // Huge incoming fire attack (400 fire damage against 100 capacity) const overRes = simulateCycloneArmorAbsorption(initialCapacity, 400, 'fire') expect(overRes.absorbed).toBe(100) expect(overRes.penetrated).toBe(300) expect(overRes.remainingArmor).toBe(0) expect(overRes.didDissipate).toBe(true) // Subsequent attacks hit directly with 0 armor const postRes = simulateCycloneArmorAbsorption(overRes.remainingArmor, 50, 'lightning') expect(postRes.absorbed).toBe(0) expect(postRes.penetrated).toBe(50) expect(postRes.remainingArmor).toBe(0) expect(postRes.didDissipate).toBe(true) }) }) // ========================================================================= // Dimension 3: Kinematics, Trajectory & Multi-Missile Mechanics // ========================================================================= describe('Dimension 3: Kinematics, Trajectory & Multi-Missile Mechanics', () => { it('3.1 Firestorm: 3 snakes disperse along divergent angular paths with non-collinear coordinates', () => { const originX = 100 const originY = 100 const targetX = 300 const targetY = 100 // Step 10 const s0 = calculateFirestormPathPoint(originX, originY, targetX, targetY, 0, 10) const s1 = calculateFirestormPathPoint(originX, originY, targetX, targetY, 1, 10) const s2 = calculateFirestormPathPoint(originX, originY, targetX, targetY, 2, 10) // All 3 snakes must travel forward (x > originX) expect(s0.x).toBeGreaterThan(originX) expect(s1.x).toBeGreaterThan(originX) expect(s2.x).toBeGreaterThan(originX) // The 3 snakes must be spatially separated along the Y axis expect(s0.y).not.toBe(s1.y) expect(s1.y).not.toBe(s2.y) expect(s0.y).not.toBe(s2.y) }) it('3.2 Tornado: Wanders smoothly with continuous lateral oscillation without snapping backwards', () => { let x = 200 let y = 200 const baseHeading = 0 // moving East let phase = 0 const history: { x: number; y: number }[] = [] for (let step = 0; step < 20; step++) { const next = simulateTornadoWanderStep(x, y, baseHeading, phase, 8) expect(next.x).toBeGreaterThan(x) // Monotonically progresses forward in general heading x = next.x y = next.y phase = next.currentPhase history.push({ x, y }) } // Check that lateral Y oscillates const yValues = history.map((p) => p.y) const minY = Math.min(...yValues) const maxY = Math.max(...yValues) expect(maxY - minY).toBeGreaterThan(10) // Demonstrated non-zero lateral sway }) it('3.3 Armageddon: Drops meteors strictly within 8-yard radial boundary of Druid', () => { const druidX = 500 const druidY = 500 // Test multiple deterministic angles and radius ratios for (let i = 0; i < 36; i++) { const angle = (i * Math.PI * 2) / 36 const radiusRatio = (i % 10) / 10 const loc = calculateArmageddonImpactLocation(druidX, druidY, angle, radiusRatio, 8) expect(loc.distanceYards).toBeLessThanOrEqual(8.0) expect(loc.distanceYards).toBeGreaterThanOrEqual(0.0) } }) }) // ========================================================================= // Dimension 4: Invariants, DataRegistry, and SkillModule Integration // ========================================================================= describe('Dimension 4: Invariants, DataRegistry, and SkillModule Integration', () => { it('4.1 BATCH1_SKILLS length invariant = 38 is strictly preserved', () => { const batch1Keys = Object.keys(BATCH1_SKILLS) expect(batch1Keys.length).toBe(38) }) it('4.2 All 10 Druid Elemental skills are properly registered in SkillModule architecture', async () => { const elementalIds = [225, 229, 230, 234, 235, 240, 244, 245, 249, 250] const { skillModule: m225 } = await import('../../../src/server/skills/impl/dru/skill-225-firestorm.ts') const { skillModule: m229 } = await import('../../../src/server/skills/impl/dru/skill-229-molten-boulder.ts') const { skillModule: m230 } = await import('../../../src/server/skills/impl/dru/skill-230-arctic-blast.ts') const { skillModule: m234 } = await import('../../../src/server/skills/impl/dru/skill-234-eruption.ts') const { skillModule: m235 } = await import('../../../src/server/skills/impl/dru/skill-235-cyclone-armor.ts') const { skillModule: m240 } = await import('../../../src/server/skills/impl/dru/skill-240-twister.ts') const { skillModule: m244 } = await import('../../../src/server/skills/impl/dru/skill-244-volcano.ts') const { skillModule: m245 } = await import('../../../src/server/skills/impl/dru/skill-245-tornado.ts') const { skillModule: m249 } = await import('../../../src/server/skills/impl/dru/skill-249-armageddon.ts') const { skillModule: m250 } = await import('../../../src/server/skills/impl/dru/skill-250-hurricane.ts') const modules = [m225, m229, m230, m234, m235, m240, m244, m245, m249, m250] for (let i = 0; i < elementalIds.length; i++) { const id = elementalIds[i] const mod = modules[i] expect(mod).toBeDefined() expect(mod.skillId).toBe(id) expect(mod.charClass).toBe('dru') } }) it('4.3 DataRegistry loads all 10 Druid elemental skills from Patch_D2.mpq with exact attributes', async () => { const reg = await getSharedDataRegistry('samples/d2') // Firestorm const fs = reg.getSkillById(225) expect(fs).toBeDefined() expect(fs!.name).toBe('Firestorm') expect(fs!.castOverlay).toBe('druid_fire_cast_1') expect(fs!.delay).toBe('15') // Molten Boulder const mb = reg.getSkillById(229) expect(mb).toBeDefined() expect(mb!.name).toBe('Molten Boulder') expect(mb!.srvMissile).toBe('moltenboulderemerge') expect(mb!.delay).toBe('50') // Arctic Blast const ab = reg.getSkillById(230) expect(ab).toBeDefined() expect(ab!.name).toBe('Arctic Blast') expect(ab!.srvStFunc).toBe(11) expect(ab!.srvDoFunc).toBe(19) // Cyclone Armor const ca = reg.getSkillById(235) expect(ca).toBeDefined() expect(ca!.name).toBe('Cyclone Armor') expect(ca!.auraState).toBe('cyclonearmor') // Twister const tw = reg.getSkillById(240) expect(tw).toBeDefined() expect(tw!.name).toBe('Twister') expect(tw!.srvMissileA).toBe('twister') // Tornado const tor = reg.getSkillById(245) expect(tor).toBeDefined() expect(tor!.name).toBe('Tornado') expect(tor!.srvMissileA).toBe('tornado') // Armageddon const arm = reg.getSkillById(249) expect(arm).toBeDefined() expect(arm!.name).toBe('Armageddon') expect(arm!.auraState).toBe('armageddon') expect(arm!.delay).toBe('150') // Hurricane const hurr = reg.getSkillById(250) expect(hurr).toBeDefined() expect(hurr!.name).toBe('Hurricane') expect(hurr!.auraState).toBe('hurricane') expect(hurr!.delay).toBe('150') }) it('4.4 evaluateSkill113c evaluates Druid elemental skills against StatList with exact cooldown & mana', async () => { const reg = await getSharedDataRegistry('samples/d2') const stats = new UnitStatList() // Evaluate Firestorm const fsRes = evaluateSkill113c({ registry: reg, skill: reg.getSkillById(225)!, slvl: 5, blvl: 5, statList: stats, }) expect(fsRes.manaCost).toBe(4) expect(fsRes.cooldownFrames).toBe(15) // Evaluate Molten Boulder const mbRes = evaluateSkill113c({ registry: reg, skill: reg.getSkillById(229)!, slvl: 10, blvl: 10, statList: stats, }) expect(mbRes.manaCost).toBe(14) expect(mbRes.cooldownFrames).toBe(50) // Evaluate Hurricane const hurrRes = evaluateSkill113c({ registry: reg, skill: reg.getSkillById(250)!, slvl: 20, blvl: 20, statList: stats, }) expect(hurrRes.manaCost).toBe(30) expect(hurrRes.cooldownFrames).toBe(150) }) }) })