/** * Diablo II: Lord of Destruction v1.13c — Milestone M19 * Barbarian Warcries Adversarial Stress & Parity Suite * * Ground Truth: * - Skills.txt & Missiles.txt (Patch_D2.mpq) * - Strict fixed-point 24.8 mana calculations * - 5-band physical damage progression for War Cry * - Diminishing returns dm12 for Find Potion (0..100) and Find Item (5..60) * - Shout / Battle Orders / Battle Command mutual duration synergy (+125 frames / blvl) * - Grim Ward 1000 frame (40s) totem life * - Strict BATCH1_SKILLS invariant: length === 38 */ import { describe, it, expect } from 'vitest' import { calculateHowlStats, calculateFindPotionStats, calculateTauntStats, calculateShoutStats, calculateFindItemStats, calculateBattleCryStats, calculateBattleOrdersStats, calculateGrimWardStats, calculateWarCryStats, calculateBattleCommandStats, evaluateBarbarianWarcry, } from '../../../src/game/skills/barbarian-warcries.ts' import { BATCH1_SKILLS } from '../../../src/game/skills.ts' describe('Milestone M19 — Barbarian Warcries Parity & Adversarial Stress Suite', () => { describe('Dimension 1: Exact 1.13c Numerical Scaling & Formulas', () => { it('1.1 Howl (130): Evaluates exact radius, duration, and mana at slvl 1, 10, 20', () => { const s1 = calculateHowlStats(1) expect(s1.radiusSubtiles).toBe(2) expect(s1.fleeDurationFrames).toBe(24) expect(s1.manaCost).toBe(4) const s10 = calculateHowlStats(10) expect(s10.radiusSubtiles).toBe(11) // 2 + 1 * 9 expect(s10.fleeDurationFrames).toBe(69) // 24 + 5 * 9 const s20 = calculateHowlStats(20) expect(s20.radiusSubtiles).toBe(21) // 2 + 1 * 19 expect(s20.fleeDurationFrames).toBe(119) // 24 + 5 * 19 }) it('1.2 Find Potion (131): Evaluates success chance % and corpse requirement at slvl 1, 10, 20', () => { const s1 = calculateFindPotionStats(1) expect(s1.successChancePct).toBe(15) // dm(0, 100, 1) = floor(110*100/700) = 15 expect(s1.requiresCorpse).toBe(true) expect(s1.manaCost).toBe(2) const s10 = calculateFindPotionStats(10) expect(s10.successChancePct).toBe(68) // dm(0, 100, 10) = floor(1100*100/1600) = 68 const s20 = calculateFindPotionStats(20) expect(s20.successChancePct).toBe(84) // dm(0, 100, 20) = floor(2200*100/2600) = 84 }) it('1.3 Taunt (137): Evaluates AR and damage debuff % and melee forcing at slvl 1, 10, 20', () => { const s1 = calculateTauntStats(1) expect(s1.targetAttackRatingReductionPct).toBe(5) expect(s1.targetDamageReductionPct).toBe(5) expect(s1.forcesMeleeAttack).toBe(true) expect(s1.manaCost).toBe(3) const s10 = calculateTauntStats(10) expect(s10.targetAttackRatingReductionPct).toBe(23) // 5 + 2 * 9 expect(s10.targetDamageReductionPct).toBe(23) const s20 = calculateTauntStats(20) expect(s20.targetAttackRatingReductionPct).toBe(43) // 5 + 2 * 19 expect(s20.targetDamageReductionPct).toBe(43) }) it('1.4 Shout (138): Evaluates party defense bonus % and base duration at slvl 1, 10, 20', () => { const s1 = calculateShoutStats(1) expect(s1.defenseBonusPct).toBe(100) expect(s1.durationFrames).toBe(500) // 20s expect(s1.durationSeconds).toBe(20.0) expect(s1.manaCost).toBe(6) const s10 = calculateShoutStats(10) expect(s10.defenseBonusPct).toBe(190) // 100 + 10 * 9 expect(s10.durationFrames).toBe(2750) // 500 + 250 * 9 = 110s expect(s10.durationSeconds).toBe(110.0) const s20 = calculateShoutStats(20) expect(s20.defenseBonusPct).toBe(290) // 100 + 10 * 19 expect(s20.durationFrames).toBe(5250) // 500 + 250 * 19 = 210s expect(s20.durationSeconds).toBe(210.0) }) it('1.5 Find Item (142): Evaluates item drop chance % and corpse requirement at slvl 1, 10, 20', () => { const s1 = calculateFindItemStats(1) expect(s1.dropChancePct).toBe(13) // dm(5, 60, 1) = 5 + floor(110*55/700) = 5 + 8 = 13 expect(s1.requiresCorpse).toBe(true) expect(s1.manaCost).toBe(7) const s10 = calculateFindItemStats(10) expect(s10.dropChancePct).toBe(42) // dm(5, 60, 10) = 5 + floor(1100*55/1600) = 5 + 37 = 42 const s20 = calculateFindItemStats(20) expect(s20.dropChancePct).toBe(51) // dm(5, 60, 20) = 5 + floor(2200*55/2600) = 5 + 46 = 51 }) it('1.6 Battle Cry (146): Evaluates target defense & damage reduction and duration at slvl 1, 10, 20', () => { const s1 = calculateBattleCryStats(1) expect(s1.targetDefenseReductionPct).toBe(50) expect(s1.targetDamageReductionPct).toBe(25) expect(s1.durationFrames).toBe(300) // 12s expect(s1.durationSeconds).toBe(12.0) expect(s1.manaCost).toBe(5) const s10 = calculateBattleCryStats(10) expect(s10.targetDefenseReductionPct).toBe(68) // 50 + 2 * 9 expect(s10.targetDamageReductionPct).toBe(34) // 25 + 1 * 9 expect(s10.durationFrames).toBe(840) // 300 + 60 * 9 = 33.6s expect(s10.durationSeconds).toBe(33.6) const s20 = calculateBattleCryStats(20) expect(s20.targetDefenseReductionPct).toBe(88) // 50 + 2 * 19 expect(s20.targetDamageReductionPct).toBe(44) // 25 + 1 * 19 expect(s20.durationFrames).toBe(1440) // 300 + 60 * 19 = 57.6s expect(s20.durationSeconds).toBe(57.6) }) it('1.7 Battle Orders (149): Evaluates life/mana/stamina bonus % and base duration at slvl 1, 10, 20', () => { const s1 = calculateBattleOrdersStats(1) expect(s1.maxLifeBonusPct).toBe(35) expect(s1.maxManaBonusPct).toBe(35) expect(s1.maxStaminaBonusPct).toBe(35) expect(s1.durationFrames).toBe(750) // 30s expect(s1.durationSeconds).toBe(30.0) expect(s1.manaCost).toBe(7) const s10 = calculateBattleOrdersStats(10) expect(s10.maxLifeBonusPct).toBe(62) // 35 + 3 * 9 expect(s10.durationFrames).toBe(3000) // 750 + 250 * 9 = 120s expect(s10.durationSeconds).toBe(120.0) const s20 = calculateBattleOrdersStats(20) expect(s20.maxLifeBonusPct).toBe(92) // 35 + 3 * 19 expect(s20.durationFrames).toBe(5500) // 750 + 250 * 19 = 220s expect(s20.durationSeconds).toBe(220.0) }) it('1.8 Grim Ward (150): Evaluates totem duration (1000 frames) and flee radius at slvl 1, 10, 20', () => { const s1 = calculateGrimWardStats(1) expect(s1.totemDurationFrames).toBe(1000) expect(s1.totemDurationSeconds).toBe(40.0) expect(s1.fleeRadiusYards).toBe(3) expect(s1.fleeDurationFrames).toBe(60) expect(s1.requiresCorpse).toBe(true) expect(s1.manaCost).toBe(4) const s10 = calculateGrimWardStats(10) expect(s10.totemDurationFrames).toBe(1000) expect(s10.fleeRadiusYards).toBe(12) // 3 + 1 * 9 const s20 = calculateGrimWardStats(20) expect(s20.totemDurationFrames).toBe(1000) expect(s20.fleeRadiusYards).toBe(22) // 3 + 1 * 19 }) it('1.9 War Cry (154): Evaluates 5-band physical damage, stun duration, and scaling mana at slvl 1, 10, 20', () => { const s1 = calculateWarCryStats(1) expect(s1.minPhysicalDamage).toBe(20) expect(s1.maxPhysicalDamage).toBe(30) expect(s1.stunDurationFrames).toBe(25) // 1.0s expect(s1.stunDurationSeconds).toBe(1.0) expect(s1.manaCost).toBe(10) const s10 = calculateWarCryStats(10) expect(s10.minPhysicalDamage).toBe(76) expect(s10.maxPhysicalDamage).toBe(86) expect(s10.stunDurationFrames).toBe(70) // 25 + 5 * 9 = 2.8s expect(s10.stunDurationSeconds).toBe(2.8) expect(s10.manaCost).toBe(19) const s20 = calculateWarCryStats(20) expect(s20.minPhysicalDamage).toBe(150) expect(s20.maxPhysicalDamage).toBe(160) expect(s20.stunDurationFrames).toBe(120) // 25 + 5 * 19 = 4.8s expect(s20.stunDurationSeconds).toBe(4.8) expect(s20.manaCost).toBe(29) }) it('1.10 Battle Command (155): Evaluates fixed +1 all skills and base duration at slvl 1, 10, 20', () => { const s1 = calculateBattleCommandStats(1) expect(s1.allSkillsBonus).toBe(1) expect(s1.durationFrames).toBe(125) // 5s expect(s1.durationSeconds).toBe(5.0) expect(s1.manaCost).toBe(11) const s10 = calculateBattleCommandStats(10) expect(s10.allSkillsBonus).toBe(1) expect(s10.durationFrames).toBe(2375) // 125 + 250 * 9 = 95s expect(s10.durationSeconds).toBe(95.0) const s20 = calculateBattleCommandStats(20) expect(s20.allSkillsBonus).toBe(1) expect(s20.durationFrames).toBe(4875) // 125 + 250 * 19 = 195s expect(s20.durationSeconds).toBe(195.0) }) }) describe('Dimension 2: 1.13c Synergy Integration', () => { it('2.1 Shout receives +125 frames (5.0s) per blvl in Battle Orders and Battle Command', () => { const base = calculateShoutStats(20, 0, 0) expect(base.durationFrames).toBe(5250) const withBO = calculateShoutStats(20, 10, 0) expect(withBO.durationFrames).toBe(5250 + 125 * 10) // 6500 frames const withBoth = calculateShoutStats(20, 10, 10) expect(withBoth.durationFrames).toBe(5250 + 125 * 20) // 7750 frames }) it('2.2 Battle Orders receives +125 frames (5.0s) per blvl in Shout and Battle Command', () => { const base = calculateBattleOrdersStats(20, 0, 0) expect(base.durationFrames).toBe(5500) const withShout = calculateBattleOrdersStats(20, 10, 0) expect(withShout.durationFrames).toBe(5500 + 125 * 10) // 6750 frames const withBoth = calculateBattleOrdersStats(20, 20, 20) expect(withBoth.durationFrames).toBe(5500 + 125 * 40) // 10500 frames }) it('2.3 Battle Command receives +125 frames (5.0s) per blvl in Shout and Battle Orders', () => { const base = calculateBattleCommandStats(20, 0, 0) expect(base.durationFrames).toBe(4875) const withBoth = calculateBattleCommandStats(20, 20, 20) expect(withBoth.durationFrames).toBe(4875 + 125 * 40) // 9875 frames }) it('2.4 War Cry receives +6% physical damage per hard point in Howl, Taunt, Battle Cry', () => { const base = calculateWarCryStats(20, 0, 0, 0) expect(base.minPhysicalDamage).toBe(150) expect(base.maxPhysicalDamage).toBe(160) // 20 points in Howl: +120% dmg const withHowl = calculateWarCryStats(20, 20, 0, 0) expect(withHowl.synergyBonusPct).toBe(120) expect(withHowl.minPhysicalDamage).toBe(Math.floor((150 * 220) / 100)) // 330 expect(withHowl.maxPhysicalDamage).toBe(Math.floor((160 * 220) / 100)) // 352 // 20 points in Howl, Taunt, Battle Cry (60 total): +360% dmg const maxSyn = calculateWarCryStats(20, 20, 20, 20) expect(maxSyn.synergyBonusPct).toBe(360) expect(maxSyn.minPhysicalDamage).toBe(Math.floor((150 * 460) / 100)) // 690 expect(maxSyn.maxPhysicalDamage).toBe(Math.floor((160 * 460) / 100)) // 736 }) }) describe('Dimension 3: Adversarial Stress & Boundary Sanitization', () => { it('3.1 Handles zero and negative levels safely without NaN or crash', () => { for (const slvl of [0, -1, -50]) { const howl = calculateHowlStats(slvl) expect(howl.radiusSubtiles).toBe(0) const pot = calculateFindPotionStats(slvl) expect(pot.successChancePct).toBe(0) const taunt = calculateTauntStats(slvl) expect(taunt.targetAttackRatingReductionPct).toBe(0) const shout = calculateShoutStats(slvl) expect(shout.defenseBonusPct).toBe(0) const item = calculateFindItemStats(slvl) expect(item.dropChancePct).toBe(0) const cry = calculateBattleCryStats(slvl) expect(cry.targetDefenseReductionPct).toBe(0) const bo = calculateBattleOrdersStats(slvl) expect(bo.maxLifeBonusPct).toBe(0) const ward = calculateGrimWardStats(slvl) expect(ward.fleeRadiusYards).toBe(0) const wc = calculateWarCryStats(slvl) expect(wc.minPhysicalDamage).toBe(0) const bc = calculateBattleCommandStats(slvl) expect(bc.allSkillsBonus).toBe(0) } }) it('3.2 Handles massive extreme levels (slvl 99) monotonically', () => { const pot99 = calculateFindPotionStats(99) expect(pot99.successChancePct).toBeLessThanOrEqual(100) expect(pot99.successChancePct).toBeGreaterThan(90) const item99 = calculateFindItemStats(99) expect(item99.dropChancePct).toBeLessThanOrEqual(65) expect(item99.dropChancePct).toBeGreaterThan(55) const wc99 = calculateWarCryStats(99) expect(wc99.minPhysicalDamage).toBeGreaterThan(500) expect(wc99.maxPhysicalDamage).toBeGreaterThan(wc99.minPhysicalDamage) }) }) describe('Dimension 4: Data-Driven Registry Integration', () => { it('4.1 Verifies all 10 Barbarian Warcries evaluate via evaluateBarbarianWarcry', () => { const warcryIds = [130, 131, 137, 138, 142, 146, 149, 150, 154, 155] for (const id of warcryIds) { const res = evaluateBarbarianWarcry(id, 10, { howlBlvl: 5, shoutBlvl: 5, battleOrdersBlvl: 5, battleCommandBlvl: 5, }) expect(res).not.toBeNull() expect(res?.manaCost).toBeGreaterThan(0) } }) it('4.2 Enforces BATCH1_SKILLS length invariant = strictly 38', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) }) }) })