import { describe, expect, it } from 'vitest' import { calculateBashStats, calculateLeapStats, calculateDoubleSwingStats, calculateStunStats, calculateDoubleThrowStats, calculateLeapAttackStats, calculateConcentrateStats, calculateFrenzyStats, calculateWhirlwindStats, calculateBerserkStats, } from '../../../src/game/skills/barbarian-combat.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 M17 — Barbarian Combat 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 Bash (126): Evaluates exact damage %, flat damage, and knockback at slvl 1, 10, 20', () => { // slvl 1 const b1 = calculateBashStats(1, 0) expect(b1.damageBonusPct).toBe(50) expect(b1.flatPhysicalMin).toBe(1) expect(b1.flatPhysicalMax).toBe(1) expect(b1.knockbackChancePct).toBe(100) expect(b1.toHitBonusPct).toBe(20) expect(b1.manaCost).toBe(2) expect(b1.manaCost256).toBe(512) // slvl 10 const b10 = calculateBashStats(10, 0) expect(b10.damageBonusPct).toBe(50 + 9 * 5) // 95% expect(b10.flatPhysicalMin).toBe(1 + 9 * 1) // 10 expect(b10.flatPhysicalMax).toBe(10) expect(b10.toHitBonusPct).toBe(20 + 9 * 5) // 65% // slvl 20 const b20 = calculateBashStats(20, 0) expect(b20.damageBonusPct).toBe(50 + 19 * 5) // 145% expect(b20.flatPhysicalMin).toBe(20) expect(b20.flatPhysicalMax).toBe(20) expect(b20.toHitBonusPct).toBe(20 + 19 * 5) // 115% }) it('1.2 Leap (132): Evaluates knockback radius scaling', () => { const l1 = calculateLeapStats(1) expect(l1.knockbackRadiusSubtiles).toBe(4) expect(l1.minDistanceYards).toBe(4) expect(l1.maxDistanceYards).toBe(30) expect(l1.manaCost).toBe(2) const l10 = calculateLeapStats(10) expect(l10.knockbackRadiusSubtiles).toBe(4 + 9 * 1) // 13 subtiles const l20 = calculateLeapStats(20) expect(l20.knockbackRadiusSubtiles).toBe(4 + 19 * 1) // 23 subtiles }) it('1.3 Double Swing (133): Evaluates attack rating and descending mana cost to 0 at slvl 9+', () => { const ds1 = calculateDoubleSwingStats(1, 0) expect(ds1.damageBonusPct).toBe(0) expect(ds1.attackRateBonusPct).toBe(50) expect(ds1.toHitBonusPct).toBe(15) expect(ds1.manaCost).toBe(2) expect(ds1.manaCost256).toBe(512) expect(ds1.requiresDualWield).toBe(true) const ds5 = calculateDoubleSwingStats(5, 0) expect(ds5.toHitBonusPct).toBe(15 + 4 * 5) // 35% expect(ds5.manaCost256).toBe(256) // 1 mana const ds9 = calculateDoubleSwingStats(9, 0) expect(ds9.manaCost).toBe(0) expect(ds9.manaCost256).toBe(0) const ds20 = calculateDoubleSwingStats(20, 0) expect(ds20.manaCost).toBe(0) expect(ds20.manaCost256).toBe(0) expect(ds20.toHitBonusPct).toBe(15 + 19 * 5) // 110% }) it('1.4 Stun (139): Evaluates base stun duration and attack rating', () => { const s1 = calculateStunStats(1, 0, 0) expect(s1.baseDurationFrames).toBe(30) // 1.2s expect(s1.stunDurationFrames).toBe(30) expect(s1.toHitBonusPct).toBe(15) expect(s1.manaCost).toBe(2) expect(s1.overlayName).toBe('stun') const s10 = calculateStunStats(10, 0, 0) expect(s10.baseDurationFrames).toBe(30 + 9 * 5) // 75 frames = 3.0s expect(s10.toHitBonusPct).toBe(15 + 9 * 5) // 60% const s20 = calculateStunStats(20, 0, 0) expect(s20.baseDurationFrames).toBe(30 + 19 * 5) // 125 frames = 5.0s expect(s20.toHitBonusPct).toBe(15 + 19 * 5) // 110% }) it('1.5 Double Throw (140): Evaluates attack rating progression', () => { const dt1 = calculateDoubleThrowStats(1, 0) expect(dt1.toHitBonusPct).toBe(20) expect(dt1.manaCost).toBe(2) expect(dt1.requiresDualThrowing).toBe(true) const dt10 = calculateDoubleThrowStats(10, 0) expect(dt10.toHitBonusPct).toBe(20 + 9 * 10) // 110% const dt20 = calculateDoubleThrowStats(20, 0) expect(dt20.toHitBonusPct).toBe(20 + 19 * 10) // 210% }) it('1.6 Leap Attack (143): Evaluates massive damage and knockback radius', () => { const la1 = calculateLeapAttackStats(1, 0) expect(la1.damageBonusPct).toBe(100) expect(la1.knockbackRadiusSubtiles).toBe(4) expect(la1.toHitBonusPct).toBe(50) expect(la1.manaCost).toBe(9) expect(la1.manaCost256).toBe(2304) const la10 = calculateLeapAttackStats(10, 0) expect(la10.damageBonusPct).toBe(100 + 9 * 30) // 370% expect(la10.knockbackRadiusSubtiles).toBe(4 + 9 * 2) // 22 subtiles expect(la10.toHitBonusPct).toBe(50 + 9 * 15) // 185% const la20 = calculateLeapAttackStats(20, 0) expect(la20.damageBonusPct).toBe(100 + 19 * 30) // 670% expect(la20.knockbackRadiusSubtiles).toBe(4 + 19 * 2) // 42 subtiles expect(la20.toHitBonusPct).toBe(50 + 19 * 15) // 335% }) it('1.7 Concentrate (144): Evaluates defense bonus and uninterruptible attack', () => { const c1 = calculateConcentrateStats(1, 0, 0, 0) expect(c1.damageBonusPct).toBe(70) expect(c1.defenseBonusPct).toBe(100) expect(c1.toHitBonusPct).toBe(60) expect(c1.isUninterruptible).toBe(true) expect(c1.manaCost).toBe(2) const c10 = calculateConcentrateStats(10, 0, 0, 0) expect(c10.damageBonusPct).toBe(70 + 9 * 5) // 115% expect(c10.defenseBonusPct).toBe(100 + 9 * 10) // 190% expect(c10.toHitBonusPct).toBe(60 + 9 * 10) // 150% const c20 = calculateConcentrateStats(20, 0, 0, 0) expect(c20.damageBonusPct).toBe(70 + 19 * 5) // 165% expect(c20.defenseBonusPct).toBe(100 + 19 * 10) // 290% expect(c20.toHitBonusPct).toBe(60 + 19 * 10) // 250% }) it('1.8 Frenzy (147): Evaluates speed caps, duration (150 frames), and attack progression', () => { const f1 = calculateFrenzyStats(1, 0, 0, 0) expect(f1.damageBonusPct).toBe(90) expect(f1.toHitBonusPct).toBe(100) expect(f1.minRunSpeedPct).toBe(20) expect(f1.durationFrames).toBe(150) expect(f1.manaCost).toBe(3) expect(f1.overlayName).toBe('frenzy') expect(f1.requiresDualWield).toBe(true) const f10 = calculateFrenzyStats(10, 0, 0, 0) expect(f10.damageBonusPct).toBe(90 + 9 * 5) // 135% expect(f10.toHitBonusPct).toBe(100 + 9 * 7) // 163% expect(f10.maxRunSpeedPct).toBe(20 + 9 * 5) // 65% const f20 = calculateFrenzyStats(20, 0, 0, 0) expect(f20.damageBonusPct).toBe(90 + 19 * 5) // 185% expect(f20.toHitBonusPct).toBe(100 + 19 * 7) // 233% expect(f20.maxRunSpeedPct).toBe(20 + 19 * 5) // 115% expect(f20.maxAttackSpeedPct).toBe(Math.min(50, 7 + 19 * 2)) // 45% }) it('1.9 Whirlwind (151): Evaluates initial negative damage scaling (-50% to +102%) and hit frequency (4 frames)', () => { const ww1 = calculateWhirlwindStats(1) expect(ww1.damageBonusPct).toBe(-50) expect(ww1.toHitBonusPct).toBe(0) expect(ww1.hitFrequencyFrames).toBe(4) expect(ww1.manaCost).toBe(25) expect(ww1.manaCost256).toBe(6400) const ww10 = calculateWhirlwindStats(10) expect(ww10.damageBonusPct).toBe(-50 + 9 * 8) // +22% expect(ww10.toHitBonusPct).toBe(9 * 5) // 45% expect(ww10.manaCost).toBe(34) const ww20 = calculateWhirlwindStats(20) expect(ww20.damageBonusPct).toBe(-50 + 19 * 8) // +102% expect(ww20.toHitBonusPct).toBe(19 * 5) // 95% expect(ww20.manaCost).toBe(44) }) it('1.10 Berserk (152): Evaluates 100% magic damage conversion and decreasing zero-defense duration', () => { const bz1 = calculateBerserkStats(1, 0, 0) expect(bz1.damageBonusPct).toBe(150) expect(bz1.magicConversionPct).toBe(100) expect(bz1.toHitBonusPct).toBe(100) expect(bz1.manaCost).toBe(4) expect(bz1.zeroDefenseDurationFrames).toBeLessThanOrEqual(75) expect(bz1.zeroDefenseDurationFrames).toBeGreaterThanOrEqual(25) expect(bz1.overlayName).toBe('berserkfront') const bz20 = calculateBerserkStats(20, 0, 0) expect(bz20.damageBonusPct).toBe(150 + 19 * 15) // 435% expect(bz20.toHitBonusPct).toBe(100 + 19 * 15) // 385% // Duration decreases with level expect(bz20.zeroDefenseDurationFrames).toBeLessThan(bz1.zeroDefenseDurationFrames) }) }) // ========================================================================= // Dimension 2: 1.13c Synergy Integration & Blvl Rules // ========================================================================= describe('Dimension 2: 1.13c Synergy Integration', () => { it('2.1 Bash receives +5% damage per hard point in Stun', () => { const bBase = calculateBashStats(10, 0) const bSyn10 = calculateBashStats(10, 10) const bSyn20 = calculateBashStats(10, 20) expect(bSyn10.damageBonusPct - bBase.damageBonusPct).toBe(50) // +50% expect(bSyn20.damageBonusPct - bBase.damageBonusPct).toBe(100) // +100% }) it('2.2 Double Swing receives +10% damage per hard point in Bash', () => { const dsBase = calculateDoubleSwingStats(1, 0) const dsSyn20 = calculateDoubleSwingStats(1, 20) expect(dsSyn20.damageBonusPct - dsBase.damageBonusPct).toBe(200) // +200% }) it('2.3 Stun receives +8% damage from Bash and +5% duration from War Cry', () => { const sBase = calculateStunStats(10, 0, 0) const sSyn = calculateStunStats(10, 20, 20) expect(sSyn.damageBonusPct).toBe(160) // 20 * 8% expect(sSyn.stunDurationFrames).toBe(Math.trunc(sBase.baseDurationFrames * 2.0)) // +100% duration }) it('2.4 Concentrate receives +5% dmg from Bash, +10% dmg from Battle Orders, +1% magic from Berserk', () => { const cBase = calculateConcentrateStats(10, 0, 0, 0) const cSyn = calculateConcentrateStats(10, 20, 20, 20) expect(cSyn.damageBonusPct - cBase.damageBonusPct).toBe(20 * 5 + 20 * 10) // +300% expect(cSyn.magicConversionPct).toBe(20) // 20% magic damage conversion }) it('2.5 Frenzy receives +8% dmg from Double Swing and Taunt, +1% magic from Berserk', () => { const fBase = calculateFrenzyStats(10, 0, 0, 0) const fSyn = calculateFrenzyStats(10, 20, 20, 20) expect(fSyn.damageBonusPct - fBase.damageBonusPct).toBe((20 + 20) * 8) // +320% expect(fSyn.magicConversionPct).toBe(20) }) it('2.6 Berserk receives +10% magic damage from Howl and Shout', () => { const bzBase = calculateBerserkStats(10, 0, 0) const bzSyn = calculateBerserkStats(10, 20, 20) expect(bzSyn.damageBonusPct - bzBase.damageBonusPct).toBe((20 + 20) * 10) // +400% }) }) // ========================================================================= // Dimension 3: Adversarial Boundary & Stress Cases // ========================================================================= describe('Dimension 3: Adversarial Stress & Boundary Sanitization', () => { it('3.1 Handles zero and negative levels safely without NaN or crash', () => { for (const slvl of [-10, 0, 1]) { const bash = calculateBashStats(slvl) expect(Number.isFinite(bash.damageBonusPct)).toBe(true) expect(bash.flatPhysicalMin).toBeGreaterThan(0) const ds = calculateDoubleSwingStats(slvl) expect(Number.isFinite(ds.manaCost)).toBe(true) expect(ds.manaCost).toBeGreaterThanOrEqual(0) const ww = calculateWhirlwindStats(slvl) expect(Number.isFinite(ww.damageBonusPct)).toBe(true) expect(ww.manaCost).toBeGreaterThan(0) } }) it('3.2 Handles massive extreme levels (slvl 99) monotonically', () => { const bz99 = calculateBerserkStats(99, 20, 20) expect(bz99.damageBonusPct).toBe(150 + 98 * 15 + 400) // 2020% expect(bz99.zeroDefenseDurationFrames).toBeGreaterThanOrEqual(25) const la99 = calculateLeapAttackStats(99, 20) expect(la99.damageBonusPct).toBe(100 + 98 * 30 + 200) // 3240% const ww99 = calculateWhirlwindStats(99) expect(ww99.damageBonusPct).toBe(-50 + 98 * 8) // 734% }) }) // ========================================================================= // Dimension 4: Registry Integration & Data-Driven Parity // ========================================================================= describe('Dimension 4: Data-Driven Registry Integration', () => { it('4.1 Verifies all 10 Barbarian Combat Skills evaluate via evaluateSkill113c', async () => { const registry = await getSharedDataRegistry() const stats = new UnitStatList() const skillIds = [126, 132, 133, 139, 140, 143, 144, 147, 151, 152] for (const id of skillIds) { const skillRec = registry.getSkillById(id) expect(skillRec, `Skills.txt record missing for skill ${id}`).toBeDefined() stats.setBaseSkillLevel(id, 10) const res = evaluateSkill113c({ registry, skill: skillRec!, slvl: 10, blvl: 10, statList: stats, }) expect(res.skillId).toBe(id) expect(res.manaCost).toBeGreaterThanOrEqual(0) expect(Number.isFinite(res.manaCost256)).toBe(true) expect(Number.isFinite(res.toHitBonusPct)).toBe(true) } }) it('4.2 Enforces BATCH1_SKILLS length invariant = strictly 38', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) }) }) })