import { describe, expect, it } from 'vitest' import { calculateWerewolfStats, calculateLycanthropyStats, calculateWerebearStats, calculateFeralRageStats, calculateMaulStats, calculateRabiesStats, calculateFireClawsStats, calculateHungerStats, calculateShockWaveStats, calculateFuryStats, canCastShapeShiftSkill, advanceChargeOrb, simulateRabiesContagion, distributeFuryStrikes, DRUID_SHAPESHIFT_SKILL_IDS, } from '../../../src/game/skills/druid-shapeshift.ts' import { BATCH1_SKILLS } from '../../../src/game/skills.ts' import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts' import { UnitStatList } from '../../../src/game/engine/stat-list.ts' import { evaluateSkill113c } from '../../../src/game/skills/registry.ts' describe('Milestone M23 — Druid Shape Shifting Skills Parity & Adversarial Stress Suite', () => { // ========================================================================= // Dimension 1: Exact 1.13c Numerical Scaling & Formulas // ========================================================================= describe('Dimension 1: Exact 1.13c Numerical Scaling & Formulas', () => { it('1.1 Werewolf (223): Evaluates attack rate diminishing returns, stamina, and Lycanthropy integration', () => { // slvl 1 without Lycanthropy const w1 = calculateWerewolfStats(1, 0) expect(w1.attackRateBonusPct).toBe(21) // 10 + floor(110*1*70 / 700) = 21% expect(w1.toHitBonusPct).toBe(50) expect(w1.staminaBonusPct).toBe(25) expect(w1.maxHpBonusPct).toBe(25) // 25 base expect(w1.durationFrames).toBe(1000) // 40s base expect(w1.durationSeconds).toBe(40.0) expect(w1.cooldownFrames).toBe(25) expect(w1.manaCost).toBe(15) expect(w1.manaCost256).toBe(3840) // slvl 10 with slvl 10 Lycanthropy // attack rate: 10 + floor(110*10*70 / 1600) = 10 + 48 = 58% // toHit: 50 + 9*15 = 185% // lycHp: 20 + 9*5 = 65% -> total HP = 25 + 65 = 90% // lycDuration: 1000 + 9*500 = 5500 -> total duration = 1000 + 5500 = 6500 frames (260s) const w10 = calculateWerewolfStats(10, 10) expect(w10.attackRateBonusPct).toBe(58) expect(w10.toHitBonusPct).toBe(185) expect(w10.maxHpBonusPct).toBe(90) expect(w10.durationFrames).toBe(6500) expect(w10.durationSeconds).toBe(260.0) // slvl 20 with slvl 20 Lycanthropy // attack rate: 10 + floor(110*20*70 / 2600) = 10 + 59 = 69% // toHit: 50 + 19*15 = 335% // lycHp: 20 + 19*5 = 115% -> total HP = 25 + 115 = 140% // lycDuration: 1000 + 19*500 = 10500 -> total duration = 1000 + 10500 = 11500 frames (460s) const w20 = calculateWerewolfStats(20, 20) expect(w20.attackRateBonusPct).toBe(69) expect(w20.toHitBonusPct).toBe(335) expect(w20.maxHpBonusPct).toBe(140) expect(w20.durationFrames).toBe(11500) expect(w20.durationSeconds).toBe(460.0) }) it('1.2 Lycanthropy (224): Evaluates passive form duration bonus and maximum HP progression', () => { // slvl 1 const lyc1 = calculateLycanthropyStats(1) expect(lyc1.durationBonusFrames).toBe(1000) // +40s expect(lyc1.durationBonusSeconds).toBe(40.0) expect(lyc1.maxHpBonusPct).toBe(20) expect(lyc1.isPassive).toBe(true) // slvl 10 const lyc10 = calculateLycanthropyStats(10) expect(lyc10.durationBonusFrames).toBe(1000 + 9 * 500) // 5500 frames (+220s) expect(lyc10.durationBonusSeconds).toBe(220.0) expect(lyc10.maxHpBonusPct).toBe(20 + 9 * 5) // 65% // slvl 20 const lyc20 = calculateLycanthropyStats(20) expect(lyc20.durationBonusFrames).toBe(1000 + 19 * 500) // 10500 frames (+420s) expect(lyc20.durationBonusSeconds).toBe(420.0) expect(lyc20.maxHpBonusPct).toBe(20 + 19 * 5) // 115% }) it('1.3 Werebear (228): Evaluates massive defense, enhanced damage, and life scaling', () => { // slvl 1 without Lycanthropy const wb1 = calculateWerebearStats(1, 0) expect(wb1.damageBonusPct).toBe(55) expect(wb1.defenseBonusPct).toBe(25) expect(wb1.maxHpBonusPct).toBe(75) // 75 base expect(wb1.durationFrames).toBe(1000) expect(wb1.durationSeconds).toBe(40.0) expect(wb1.cooldownFrames).toBe(25) expect(wb1.manaCost).toBe(15) // slvl 10 with slvl 10 Lycanthropy // damage: 55 + 9*8 = 127% // defense: 25 + 9*6 = 79% // hp: 75 + (20 + 9*5) = 75 + 65 = 140% const wb10 = calculateWerebearStats(10, 10) expect(wb10.damageBonusPct).toBe(127) expect(wb10.defenseBonusPct).toBe(79) expect(wb10.maxHpBonusPct).toBe(140) expect(wb10.durationFrames).toBe(6500) // slvl 20 with slvl 20 Lycanthropy // damage: 55 + 19*8 = 207% // defense: 25 + 19*6 = 139% // hp: 75 + (20 + 19*5) = 75 + 115 = 190% const wb20 = calculateWerebearStats(20, 20) expect(wb20.damageBonusPct).toBe(207) expect(wb20.defenseBonusPct).toBe(139) expect(wb20.maxHpBonusPct).toBe(190) expect(wb20.durationFrames).toBe(11500) }) it('1.4 Feral Rage (232): Evaluates damage, attack rating, speed bonus, and life leech', () => { // slvl 1 const fr1 = calculateFeralRageStats(1) expect(fr1.damageBonusPct).toBe(50) expect(fr1.toHitBonusPct).toBe(20) expect(fr1.lifeLeechPct).toBe(4) // 4 * 1 expect(fr1.velocityBonusPct).toBe(19) // 10 + floor(110*1*60 / 700) = 19% expect(fr1.durationFrames).toBe(500) // 20.0s expect(fr1.maxCharges).toBe(3) expect(fr1.manaCost).toBe(3) expect(fr1.manaCost256).toBe(768) // slvl 10 // damage: 50 + 9*5 = 95% // toHit: 20 + 9*10 = 110% // life leech: 4 * 10 = 40% // velocity: 10 + floor(110*10*60 / 1600) = 10 + 41 = 51% const fr10 = calculateFeralRageStats(10) expect(fr10.damageBonusPct).toBe(95) expect(fr10.toHitBonusPct).toBe(110) expect(fr10.lifeLeechPct).toBe(40) expect(fr10.velocityBonusPct).toBe(51) // slvl 20 // damage: 50 + 19*5 = 145% // toHit: 20 + 19*10 = 210% // life leech: 4 * 20 = 80% // velocity: 10 + floor(110*20*60 / 2600) = 10 + 50 = 60% const fr20 = calculateFeralRageStats(20) expect(fr20.damageBonusPct).toBe(145) expect(fr20.toHitBonusPct).toBe(210) expect(fr20.lifeLeechPct).toBe(80) expect(fr20.velocityBonusPct).toBe(60) }) it('1.5 Maul (233): Evaluates 5-stage charge damage and stun scaling', () => { // slvl 1 const m1 = calculateMaulStats(1) expect(m1.damageBonusPerChargePct).toBe(20) // 1 * 20 expect(m1.maxDamageBonusPct).toBe(100) // 5 charges = 100% expect(m1.toHitBonusPct).toBe(20) expect(m1.stunDurationFrames).toBe(24) // 10 + floor(110*1*90 / 700) = 24 frames (~0.96s) expect(m1.maxCharges).toBe(5) expect(m1.manaCost).toBe(3) // slvl 10 // damage/charge: 10 * 20 = 200%, max 5 charges = 1000% // toHit: 20 + 9*10 = 110% // stun: 10 + floor(110*10*90 / 1600) = 10 + 61 = 71 frames (2.84s) const m10 = calculateMaulStats(10) expect(m10.damageBonusPerChargePct).toBe(200) expect(m10.maxDamageBonusPct).toBe(1000) expect(m10.toHitBonusPct).toBe(110) expect(m10.stunDurationFrames).toBe(71) // slvl 20 // damage/charge: 20 * 20 = 400%, max 5 charges = 2000% // toHit: 20 + 19*10 = 210% // stun: 10 + floor(110*20*90 / 2600) = 10 + 76 = 86 frames (3.44s) const m20 = calculateMaulStats(20) expect(m20.damageBonusPerChargePct).toBe(400) expect(m20.maxDamageBonusPct).toBe(2000) expect(m20.toHitBonusPct).toBe(210) expect(m20.stunDurationFrames).toBe(86) }) it('1.6 Rabies (238): Evaluates poison progression, duration, and Poison Creeper synergy', () => { // slvl 1 const r1 = calculateRabiesStats(1, 0) expect(r1.minPoisonDamage).toBe(6) expect(r1.maxPoisonDamage).toBe(14) expect(r1.poisonDurationFrames).toBe(100) // 4.0s expect(r1.toHitBonusPct).toBe(50) expect(r1.contagionRadiusSubtiles).toBe(4) expect(r1.contagionSpreadIntervalFrames).toBe(4) expect(r1.manaCost).toBe(10) // slvl 10: min: 6 + 7*4 + 2*5 = 44, max: 14 + 7*4 + 2*5 = 52 // duration: 100 + 9*10 = 190 frames (7.6s) // toHit: 50 + 9*7 = 113% const r10 = calculateRabiesStats(10, 0) expect(r10.minPoisonDamage).toBe(44) expect(r10.maxPoisonDamage).toBe(52) expect(r10.poisonDurationFrames).toBe(190) expect(r10.toHitBonusPct).toBe(113) // slvl 20: min: 44 + 6*5 + 4*7 = 102, max: 52 + 6*5 + 4*7 = 110 // duration: 100 + 19*10 = 290 frames (11.6s) // toHit: 50 + 19*7 = 183% const r20 = calculateRabiesStats(20, 0) expect(r20.minPoisonDamage).toBe(102) expect(r20.maxPoisonDamage).toBe(110) expect(r20.poisonDurationFrames).toBe(290) expect(r20.toHitBonusPct).toBe(183) // Synergy: 20 points in Poison Creeper (+18%/blvl = +360%) const r20Syn = calculateRabiesStats(20, 20) expect(r20Syn.synergyBonusPct).toBe(360) expect(r20Syn.minPoisonDamage).toBe(Math.trunc((102 * 460) / 100)) // 469 expect(r20Syn.maxPoisonDamage).toBe(Math.trunc((110 * 460) / 100)) // 506 }) it('1.7 Fire Claws (239): Evaluates elemental fire melee damage and 4-way synergy scaling', () => { // slvl 1 const fc1 = calculateFireClawsStats(1, 0, 0, 0, 0) expect(fc1.minFireDamage).toBe(15) expect(fc1.maxFireDamage).toBe(20) expect(fc1.toHitBonusPct).toBe(50) expect(fc1.manaCost).toBe(4) // slvl 10: min: 15 + 7*8 + 2*12 = 95, max: 20 + 7*8 + 2*12 = 100 const fc10 = calculateFireClawsStats(10, 0, 0, 0, 0) expect(fc10.minFireDamage).toBe(95) expect(fc10.maxFireDamage).toBe(100) // slvl 20: min: 95 + 6*12 + 4*20 = 247, max: 100 + 6*12 + 4*22 = 260 const fc20 = calculateFireClawsStats(20, 0, 0, 0, 0) expect(fc20.minFireDamage).toBe(247) expect(fc20.maxFireDamage).toBe(260) // Synergies: 20 in Firestorm, 20 in Molten Boulder, 20 in Volcano, 20 in Fissure // total points = 80 -> synergy = 80 * 22% = +1760% const fc20Syn = calculateFireClawsStats(20, 20, 20, 20, 20) expect(fc20Syn.synergyBonusPct).toBe(1760) expect(fc20Syn.minFireDamage).toBe(Math.trunc((247 * 1860) / 100)) // 4594 expect(fc20Syn.maxFireDamage).toBe(Math.trunc((260 * 1860) / 100)) // 4836 }) it('1.8 Hunger (242): Evaluates -75% damage penalty and dual life & mana leech', () => { // slvl 1 // leech: 50 + floor(110*1*150 / 700) = 50 + 23 = 73% const h1 = calculateHungerStats(1) expect(h1.damagePenaltyPct).toBe(-75) expect(h1.lifeLeechPct).toBe(73) expect(h1.manaLeechPct).toBe(73) expect(h1.toHitBonusPct).toBe(50) expect(h1.manaCost).toBe(3) // slvl 10: 50 + floor(110*10*150 / 1600) = 50 + 103 = 153% const h10 = calculateHungerStats(10) expect(h10.lifeLeechPct).toBe(153) expect(h10.manaLeechPct).toBe(153) expect(h10.toHitBonusPct).toBe(140) // slvl 20: 50 + floor(110*20*150 / 2600) = 50 + 126 = 176% const h20 = calculateHungerStats(20) expect(h20.lifeLeechPct).toBe(176) expect(h20.manaLeechPct).toBe(176) expect(h20.toHitBonusPct).toBe(240) }) it('1.9 Shock Wave (243): Evaluates bear frontal sonic stun cone and Maul synergy', () => { // slvl 1 const sw1 = calculateShockWaveStats(1, 0) expect(sw1.minPhysicalDamage).toBe(10) expect(sw1.maxPhysicalDamage).toBe(20) expect(sw1.stunDurationFrames).toBe(40) // 1.6s expect(sw1.stunDurationSeconds).toBe(1.6) expect(sw1.manaCost).toBe(7) expect(sw1.missileName).toBe('shockwave') // slvl 10: min: 10 + 7*3 + 2*5 = 41, max: 20 + 7*3 + 2*5 = 51 // stun: 40 + 9*15 = 175 frames (7.0s) const sw10 = calculateShockWaveStats(10, 0) expect(sw10.minPhysicalDamage).toBe(41) expect(sw10.maxPhysicalDamage).toBe(51) expect(sw10.stunDurationFrames).toBe(175) expect(sw10.stunDurationSeconds).toBe(7.0) // slvl 20: min: 41 + 6*5 + 4*7 = 99, max: 51 + 6*5 + 4*7 = 109 // stun: 40 + 19*15 = 325 frames (13.0s) const sw20 = calculateShockWaveStats(20, 0) expect(sw20.minPhysicalDamage).toBe(99) expect(sw20.maxPhysicalDamage).toBe(109) expect(sw20.stunDurationFrames).toBe(325) expect(sw20.stunDurationSeconds).toBe(13.0) // Synergy: 20 in Maul (+5%/lvl = +100%) const sw20Syn = calculateShockWaveStats(20, 20) expect(sw20Syn.synergyBonusPct).toBe(100) expect(sw20Syn.minPhysicalDamage).toBe(99 * 2) // 198 expect(sw20Syn.maxPhysicalDamage).toBe(109 * 2) // 218 }) it('1.10 Fury (248): Evaluates 5-hit uninterrupted feral frenzy sequence', () => { // slvl 1: min(2 + 0, 5) = 2 hits const f1 = calculateFuryStats(1) expect(f1.damageBonusPct).toBe(100) expect(f1.toHitBonusPct).toBe(50) expect(f1.attackCount).toBe(2) expect(f1.isUninterruptible).toBe(true) expect(f1.manaCost).toBe(4) // slvl 2: min(2 + 1, 5) = 3 hits const f2 = calculateFuryStats(2) expect(f2.attackCount).toBe(3) // slvl 3: min(2 + 2, 5) = 4 hits const f3 = calculateFuryStats(3) expect(f3.attackCount).toBe(4) // slvl 4+: min(2 + 3, 5) = 5 hits (capped) const f4 = calculateFuryStats(4) expect(f4.attackCount).toBe(5) // slvl 10: 100 + 9*17 = 253%, 50 + 9*7 = 113% const f10 = calculateFuryStats(10) expect(f10.damageBonusPct).toBe(253) expect(f10.toHitBonusPct).toBe(113) expect(f10.attackCount).toBe(5) // slvl 20: 100 + 19*17 = 423%, 50 + 19*7 = 183% const f20 = calculateFuryStats(20) expect(f20.damageBonusPct).toBe(423) expect(f20.toHitBonusPct).toBe(183) expect(f20.attackCount).toBe(5) }) }) // ========================================================================= // Dimension 2: Beast Form Restrictions & State Validation Matrix // ========================================================================= describe('Dimension 2: Beast Form Restrictions & State Validation Matrix', () => { it('2.1 Enforces Wolf-only skills: Feral Rage, Rabies, Fury', () => { const wolfSkills = [ DRUID_SHAPESHIFT_SKILL_IDS.FERAL_RAGE, DRUID_SHAPESHIFT_SKILL_IDS.RABIES, DRUID_SHAPESHIFT_SKILL_IDS.FURY, ] for (const skillId of wolfSkills) { expect(canCastShapeShiftSkill('wolf', skillId)).toBe(true) expect(canCastShapeShiftSkill('human', skillId)).toBe(false) expect(canCastShapeShiftSkill('bear', skillId)).toBe(false) } }) it('2.2 Enforces Bear-only skills: Maul, Shock Wave', () => { const bearSkills = [ DRUID_SHAPESHIFT_SKILL_IDS.MAUL, DRUID_SHAPESHIFT_SKILL_IDS.SHOCK_WAVE, ] for (const skillId of bearSkills) { expect(canCastShapeShiftSkill('bear', skillId)).toBe(true) expect(canCastShapeShiftSkill('human', skillId)).toBe(false) expect(canCastShapeShiftSkill('wolf', skillId)).toBe(false) } }) it('2.3 Enforces dual-form beast skills: Fire Claws and Hunger', () => { const dualSkills = [ DRUID_SHAPESHIFT_SKILL_IDS.FIRE_CLAWS, DRUID_SHAPESHIFT_SKILL_IDS.HUNGER, ] for (const skillId of dualSkills) { expect(canCastShapeShiftSkill('wolf', skillId)).toBe(true) expect(canCastShapeShiftSkill('bear', skillId)).toBe(true) expect(canCastShapeShiftSkill('human', skillId)).toBe(false) } }) it('2.4 Enforces transformation and reversion capabilities', () => { // Werewolf: can cast in human (to become wolf) or wolf (to revert to human), but not bear expect(canCastShapeShiftSkill('human', DRUID_SHAPESHIFT_SKILL_IDS.WEREWOLF)).toBe(true) expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.WEREWOLF)).toBe(true) expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.WEREWOLF)).toBe(false) // Werebear: can cast in human (to become bear) or bear (to revert to human), but not wolf expect(canCastShapeShiftSkill('human', DRUID_SHAPESHIFT_SKILL_IDS.WEREBEAR)).toBe(true) expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.WEREBEAR)).toBe(true) expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.WEREBEAR)).toBe(false) }) }) // ========================================================================= // Dimension 3: Kinematics, Contagion & Multi-Hit Mechanics // ========================================================================= describe('Dimension 3: Kinematics, Contagion & Multi-Hit Mechanics', () => { it('3.1 Rabies: Contagion spreads to nearby targets within 4 subtiles and ignores distant targets', () => { const infected = [{ id: 'target_0', x: 100, y: 100 }] const healthy = [ { id: 'target_close_1', x: 120, y: 105 }, // ~22px away (inside 128px radius) { id: 'target_close_2', x: 80, y: 95 }, // ~22px away (inside 128px radius) { id: 'target_far', x: 400, y: 400 }, // ~424px away (outside 128px radius) ] const newlyInfected = simulateRabiesContagion(infected, healthy, 128) expect(newlyInfected).toContain('target_close_1') expect(newlyInfected).toContain('target_close_2') expect(newlyInfected).not.toContain('target_far') }) it('3.2 Maul & Feral Rage: Charge-up orb increments up to exact maximum charges', () => { // Feral Rage caps at 3 let frCharge = 0 frCharge = advanceChargeOrb(frCharge, 3) expect(frCharge).toBe(1) frCharge = advanceChargeOrb(frCharge, 3) expect(frCharge).toBe(2) frCharge = advanceChargeOrb(frCharge, 3) expect(frCharge).toBe(3) frCharge = advanceChargeOrb(frCharge, 3) expect(frCharge).toBe(3) // Capped at 3 // Maul caps at 5 let mCharge = 0 for (let i = 0; i < 10; i++) { mCharge = advanceChargeOrb(mCharge, 5) } expect(mCharge).toBe(5) // Capped at 5 }) it('3.3 Fury: Distributes 5-hit sequence evenly across multiple targets or focuses single target', () => { // Single target const singleStrikes = distributeFuryStrikes(['boss_1'], 5) expect(singleStrikes.length).toBe(5) expect(singleStrikes.every((s) => s.targetId === 'boss_1')).toBe(true) expect(singleStrikes.map((s) => s.hitIndex)).toEqual([1, 2, 3, 4, 5]) // 2 targets (alternating) const dualStrikes = distributeFuryStrikes(['enemy_a', 'enemy_b'], 5) expect(dualStrikes.length).toBe(5) expect(dualStrikes.map((s) => s.targetId)).toEqual([ 'enemy_a', 'enemy_b', 'enemy_a', 'enemy_b', 'enemy_a', ]) }) }) // ========================================================================= // 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 Shape Shifting skills are properly defined in SkillModule architecture', async () => { const shapeShiftIds = [223, 224, 228, 232, 233, 238, 239, 242, 243, 248] const { skillModule: m223 } = await import('../../../src/game/skills/impl/dru/skill-223-wearwolf.ts') const { skillModule: m224 } = await import('../../../src/game/skills/impl/dru/skill-224-shape-shifting.ts') const { skillModule: m228 } = await import('../../../src/game/skills/impl/dru/skill-228-wearbear.ts') const { skillModule: m232 } = await import('../../../src/game/skills/impl/dru/skill-232-feral-rage.ts') const { skillModule: m233 } = await import('../../../src/game/skills/impl/dru/skill-233-maul.ts') const { skillModule: m238 } = await import('../../../src/game/skills/impl/dru/skill-238-rabies.ts') const { skillModule: m239 } = await import('../../../src/game/skills/impl/dru/skill-239-fire-claws.ts') const { skillModule: m242 } = await import('../../../src/game/skills/impl/dru/skill-242-hunger.ts') const { skillModule: m243 } = await import('../../../src/game/skills/impl/dru/skill-243-shock-wave.ts') const { skillModule: m248 } = await import('../../../src/game/skills/impl/dru/skill-248-fury.ts') const modules = [m223, m224, m228, m232, m233, m238, m239, m242, m243, m248] for (let i = 0; i < shapeShiftIds.length; i++) { const id = shapeShiftIds[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 Shape Shifting skills from Patch_D2.mpq with exact attributes', async () => { const reg = await getSharedDataRegistry('samples/d2') // Werewolf const ww = reg.getSkillById(223) expect(ww).toBeDefined() expect(ww!.name).toBe('Wearwolf') expect(ww!.auraState).toBe('wolf') expect(ww!.delay).toBe('25') // Lycanthropy const lyc = reg.getSkillById(224) expect(lyc).toBeDefined() expect(lyc!.name).toBe('Shape Shifting') expect(lyc!.passive).toBe(true) // Werebear const wb = reg.getSkillById(228) expect(wb).toBeDefined() expect(wb!.name).toBe('Wearbear') expect(wb!.auraState).toBe('bear') expect(wb!.delay).toBe('25') // Feral Rage const fr = reg.getSkillById(232) expect(fr).toBeDefined() expect(fr!.name).toBe('Feral Rage') expect(fr!.auraState).toBe('feralrage') // Maul const m = reg.getSkillById(233) expect(m).toBeDefined() expect(m!.name).toBe('Maul') expect(m!.auraState).toBe('maul') // Rabies const r = reg.getSkillById(238) expect(r).toBeDefined() expect(r!.name).toBe('Rabies') expect(r!.srvMissileA).toBe('rabiesplague') // Fire Claws const fc = reg.getSkillById(239) expect(fc).toBeDefined() expect(fc!.name).toBe('Fire Claws') expect(fc!.eType).toBe('fire') // Hunger const h = reg.getSkillById(242) expect(h).toBeDefined() expect(h!.name).toBe('Hunger') // Shock Wave const sw = reg.getSkillById(243) expect(sw).toBeDefined() expect(sw!.name).toBe('Shock Wave') expect(sw!.srvMissileA).toBe('shockwave') // Fury const f = reg.getSkillById(248) expect(f).toBeDefined() expect(f!.name).toBe('Fury') }) it('4.4 evaluateSkill113c evaluates Druid Shape Shifting skills against StatList with exact attributes', async () => { const reg = await getSharedDataRegistry('samples/d2') const stats = new UnitStatList() // Werewolf const wwRes = evaluateSkill113c({ registry: reg, skill: reg.getSkillById(223)!, slvl: 1, blvl: 1, statList: stats, }) expect(wwRes.manaCost).toBe(15) expect(wwRes.cooldownFrames).toBe(25) // Maul const mRes = evaluateSkill113c({ registry: reg, skill: reg.getSkillById(233)!, slvl: 10, blvl: 10, statList: stats, }) expect(mRes.manaCost).toBe(3) expect(mRes.toHitBonusPct).toBe(110) // Fury const fRes = evaluateSkill113c({ registry: reg, skill: reg.getSkillById(248)!, slvl: 20, blvl: 20, statList: stats, }) expect(fRes.manaCost).toBe(4) expect(fRes.toHitBonusPct).toBe(183) }) }) })