diablo2-web/tests/skills/bar/adv-bar-masteries-stress.te...

284 lines
12 KiB
TypeScript

/**
* Diablo II: Lord of Destruction v1.13c — Milestone M18
* Barbarian Combat Masteries Adversarial Stress & Parity Suite
*
* Ground Truth:
* - Skills.txt (Patch_D2.mpq)
* - Exact linear progressions ln12 (AR), ln34 (Dmg), ln12 (Stamina, Defense)
* - Diminishing returns dm56 (Crit), dm12 (Speed, Resist All)
* - Sequential roll for Critical Strike & Deadly Strike
* - Strict BATCH1_SKILLS invariant: length === 38
*/
import { describe, it, expect } from 'vitest'
import {
calculateSwordMasteryStats,
calculateAxeMasteryStats,
calculateMaceMasteryStats,
calculatePolearmMasteryStats,
calculateThrowingMasteryStats,
calculateSpearMasteryStats,
calculateIncreasedStaminaStats,
calculateIronSkinStats,
calculateIncreasedSpeedStats,
calculateNaturalResistanceStats,
isWeaponMatchingMastery,
calculateEffectiveCriticalStrike,
evaluateBarbarianMastery,
} from '../../../src/game/skills/barbarian-masteries.ts'
import { BATCH1_SKILLS } from '../../../src/game/skills.ts'
describe('Milestone M18 — Barbarian Combat Masteries Parity & Adversarial Stress Suite', () => {
describe('Dimension 1: Exact 1.13c Numerical Scaling & Formulas', () => {
it('1.1 Sword Mastery (127): Evaluates exact AR %, Dmg %, and Crit % at slvl 1, 10, 20', () => {
const s1 = calculateSwordMasteryStats(1)
expect(s1.attackRatingBonusPct).toBe(28)
expect(s1.damageBonusPct).toBe(28)
expect(s1.criticalStrikeChancePct).toBe(5)
expect(s1.manaCost).toBe(0)
const s10 = calculateSwordMasteryStats(10)
expect(s10.attackRatingBonusPct).toBe(100) // 28 + 8 * 9
expect(s10.damageBonusPct).toBe(73) // 28 + 5 * 9
expect(s10.criticalStrikeChancePct).toBe(24) // 110*10*35 / (100*16) = 24.0625 -> 24
const s20 = calculateSwordMasteryStats(20)
expect(s20.attackRatingBonusPct).toBe(180) // 28 + 8 * 19
expect(s20.damageBonusPct).toBe(123) // 28 + 5 * 19
expect(s20.criticalStrikeChancePct).toBe(29)
})
it('1.2 Axe Mastery (128): Evaluates exact AR %, Dmg %, and Crit % at slvl 1, 10, 20', () => {
const s1 = calculateAxeMasteryStats(1)
expect(s1.attackRatingBonusPct).toBe(28)
expect(s1.damageBonusPct).toBe(28)
expect(s1.criticalStrikeChancePct).toBe(5)
const s10 = calculateAxeMasteryStats(10)
expect(s10.attackRatingBonusPct).toBe(100)
expect(s10.damageBonusPct).toBe(73)
expect(s10.criticalStrikeChancePct).toBe(24)
const s20 = calculateAxeMasteryStats(20)
expect(s20.attackRatingBonusPct).toBe(180)
expect(s20.damageBonusPct).toBe(123)
expect(s20.criticalStrikeChancePct).toBe(29)
})
it('1.3 Mace Mastery (129): Evaluates exact AR %, Dmg %, and Crit % at slvl 1, 10, 20', () => {
const s1 = calculateMaceMasteryStats(1)
expect(s1.attackRatingBonusPct).toBe(28)
expect(s1.damageBonusPct).toBe(28)
expect(s1.criticalStrikeChancePct).toBe(5)
const s10 = calculateMaceMasteryStats(10)
expect(s10.attackRatingBonusPct).toBe(100)
expect(s10.damageBonusPct).toBe(73)
expect(s10.criticalStrikeChancePct).toBe(24)
const s20 = calculateMaceMasteryStats(20)
expect(s20.attackRatingBonusPct).toBe(180)
expect(s20.damageBonusPct).toBe(123)
expect(s20.criticalStrikeChancePct).toBe(29)
})
it('1.4 Polearm Mastery (134): Evaluates exact AR %, Dmg %, and Crit % at slvl 1, 10, 20', () => {
const s1 = calculatePolearmMasteryStats(1)
expect(s1.attackRatingBonusPct).toBe(30) // Param1 = 30
expect(s1.damageBonusPct).toBe(28)
expect(s1.criticalStrikeChancePct).toBe(5)
const s10 = calculatePolearmMasteryStats(10)
expect(s10.attackRatingBonusPct).toBe(102) // 30 + 8 * 9
expect(s10.damageBonusPct).toBe(73)
expect(s10.criticalStrikeChancePct).toBe(24)
const s20 = calculatePolearmMasteryStats(20)
expect(s20.attackRatingBonusPct).toBe(182) // 30 + 8 * 19
expect(s20.damageBonusPct).toBe(123)
expect(s20.criticalStrikeChancePct).toBe(29)
})
it('1.5 Throwing Mastery (135): Evaluates exact AR %, Dmg %, and Crit % at slvl 1, 10, 20', () => {
const s1 = calculateThrowingMasteryStats(1)
expect(s1.attackRatingBonusPct).toBe(30)
expect(s1.damageBonusPct).toBe(28)
expect(s1.criticalStrikeChancePct).toBe(5)
const s10 = calculateThrowingMasteryStats(10)
expect(s10.attackRatingBonusPct).toBe(102)
expect(s10.damageBonusPct).toBe(73)
expect(s10.criticalStrikeChancePct).toBe(24)
const s20 = calculateThrowingMasteryStats(20)
expect(s20.attackRatingBonusPct).toBe(182)
expect(s20.damageBonusPct).toBe(123)
expect(s20.criticalStrikeChancePct).toBe(29)
})
it('1.6 Spear Mastery (136): Evaluates exact AR %, Dmg %, and Crit % at slvl 1, 10, 20', () => {
const s1 = calculateSpearMasteryStats(1)
expect(s1.attackRatingBonusPct).toBe(30)
expect(s1.damageBonusPct).toBe(28)
expect(s1.criticalStrikeChancePct).toBe(5)
const s10 = calculateSpearMasteryStats(10)
expect(s10.attackRatingBonusPct).toBe(102)
expect(s10.damageBonusPct).toBe(73)
expect(s10.criticalStrikeChancePct).toBe(24)
const s20 = calculateSpearMasteryStats(20)
expect(s20.attackRatingBonusPct).toBe(182)
expect(s20.damageBonusPct).toBe(123)
expect(s20.criticalStrikeChancePct).toBe(29)
})
it('1.7 Increased Stamina (141): Evaluates stamina % bonus at slvl 1, 10, 20', () => {
const s1 = calculateIncreasedStaminaStats(1)
expect(s1.staminaBonusPct).toBe(30)
expect(s1.manaCost).toBe(0)
const s10 = calculateIncreasedStaminaStats(10)
expect(s10.staminaBonusPct).toBe(165) // 30 + 15 * 9
const s20 = calculateIncreasedStaminaStats(20)
expect(s20.staminaBonusPct).toBe(315) // 30 + 15 * 19
})
it('1.8 Iron Skin (145): Evaluates defense % bonus at slvl 1, 10, 20', () => {
const s1 = calculateIronSkinStats(1)
expect(s1.defenseBonusPct).toBe(30)
expect(s1.manaCost).toBe(0)
const s10 = calculateIronSkinStats(10)
expect(s10.defenseBonusPct).toBe(120) // 30 + 10 * 9
const s20 = calculateIronSkinStats(20)
expect(s20.defenseBonusPct).toBe(220) // 30 + 10 * 19
})
it('1.9 Increased Speed (148): Evaluates run/walk velocity % at slvl 1, 10, 20', () => {
const s1 = calculateIncreasedSpeedStats(1)
expect(s1.runWalkSpeedBonusPct).toBe(13) // dm(7, 50, 1) = 7 + floor(43 * 15 / 100) = 13
expect(s1.manaCost).toBe(0)
const s10 = calculateIncreasedSpeedStats(10)
expect(s10.runWalkSpeedBonusPct).toBe(36) // dm(7, 50, 10) = 7 + floor(43 * 68 / 100) = 36
const s20 = calculateIncreasedSpeedStats(20)
expect(s20.runWalkSpeedBonusPct).toBe(43) // dm(7, 50, 20) = 7 + floor(43 * 84 / 100) = 43
})
it('1.10 Natural Resistance (153): Evaluates resist all % at slvl 1, 10, 20', () => {
const s1 = calculateNaturalResistanceStats(1)
expect(s1.resistAllPct).toBe(12) // dm(0, 80, 1) = floor(80 * 15 / 100) = 12
expect(s1.fireResistPct).toBe(12)
expect(s1.coldResistPct).toBe(12)
expect(s1.lightningResistPct).toBe(12)
expect(s1.poisonResistPct).toBe(12)
expect(s1.manaCost).toBe(0)
const s10 = calculateNaturalResistanceStats(10)
expect(s10.resistAllPct).toBe(55) // dm(0, 80, 10) = floor(110*10*80 / 1600) = 55
expect(s10.fireResistPct).toBe(55)
const s20 = calculateNaturalResistanceStats(20)
expect(s20.resistAllPct).toBe(67) // dm(0, 80, 20) = floor(80 * 84 / 100) = 67
expect(s20.resistAllPct).toBe(s20.poisonResistPct)
})
})
describe('Dimension 2: Mechanics & Weapon Interaction', () => {
it('2.1 Correctly identifies matching weapon categories', () => {
expect(isWeaponMatchingMastery('crystal sword', 127)).toBe(true)
expect(isWeaponMatchingMastery('colossus blade', 127)).toBe(true)
expect(isWeaponMatchingMastery('berserker axe', 127)).toBe(false)
expect(isWeaponMatchingMastery('berserker axe', 128)).toBe(true)
expect(isWeaponMatchingMastery('war club', 128)).toBe(false)
expect(isWeaponMatchingMastery('flail', 129)).toBe(true)
expect(isWeaponMatchingMastery('war hammer', 129)).toBe(true)
expect(isWeaponMatchingMastery('grand scepter', 129)).toBe(true)
expect(isWeaponMatchingMastery('short sword', 129)).toBe(false)
expect(isWeaponMatchingMastery('colossus voulge', 134)).toBe(true)
expect(isWeaponMatchingMastery('thresher', 134)).toBe(true)
expect(isWeaponMatchingMastery('polearm', 134)).toBe(true)
expect(isWeaponMatchingMastery('short sword', 134)).toBe(false)
expect(isWeaponMatchingMastery('throwing axe', 135)).toBe(true)
expect(isWeaponMatchingMastery('javelin', 135)).toBe(true)
expect(isWeaponMatchingMastery('war spear', 136)).toBe(true)
expect(isWeaponMatchingMastery('javelin', 136)).toBe(true)
})
it('2.2 Enforces 1.13c Critical Strike + Deadly Strike sequential roll formula', () => {
// 20% CS, 50% DS -> 20 + 50 * (1 - 0.2) = 20 + 40 = 60%
expect(calculateEffectiveCriticalStrike(20, 50)).toBe(60.0)
// 0% CS, 30% DS -> 30%
expect(calculateEffectiveCriticalStrike(0, 30)).toBe(30.0)
// 25% CS, 0% DS -> 25%
expect(calculateEffectiveCriticalStrike(25, 0)).toBe(25.0)
// 30% CS, 100% DS -> 30 + 100 * 0.7 = 100%
expect(calculateEffectiveCriticalStrike(30, 100)).toBe(100.0)
})
})
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 sw = calculateSwordMasteryStats(slvl)
expect(sw.attackRatingBonusPct).toBe(0)
expect(sw.damageBonusPct).toBe(0)
expect(sw.criticalStrikeChancePct).toBe(0)
const st = calculateIncreasedStaminaStats(slvl)
expect(st.staminaBonusPct).toBe(0)
const def = calculateIronSkinStats(slvl)
expect(def.defenseBonusPct).toBe(0)
const spd = calculateIncreasedSpeedStats(slvl)
expect(spd.runWalkSpeedBonusPct).toBe(0)
const res = calculateNaturalResistanceStats(slvl)
expect(res.resistAllPct).toBe(0)
}
})
it('3.2 Handles massive extreme levels (slvl 99) monotonically within 1.13c asymptotic limits', () => {
const sw99 = calculateSwordMasteryStats(99)
expect(sw99.criticalStrikeChancePct).toBeLessThanOrEqual(38) // dm(0, 35, 99) = 36 (asymptote 1.1 * 35 = 38.5)
expect(sw99.criticalStrikeChancePct).toBeGreaterThan(30)
const spd99 = calculateIncreasedSpeedStats(99)
expect(spd99.runWalkSpeedBonusPct).toBeLessThanOrEqual(55) // dm(7, 50, 99) = 51 (asymptote 7 + 1.1 * 43 = 54.3)
expect(spd99.runWalkSpeedBonusPct).toBeGreaterThan(45)
const res99 = calculateNaturalResistanceStats(99)
expect(res99.resistAllPct).toBeLessThanOrEqual(88) // dm(0, 80, 99) = 83 (asymptote 1.1 * 80 = 88)
expect(res99.resistAllPct).toBeGreaterThan(75)
})
})
describe('Dimension 4: Data-Driven Registry Integration', () => {
it('4.1 Verifies all 10 Barbarian Combat Masteries evaluate via evaluateBarbarianMastery', () => {
const masteryIds = [127, 128, 129, 134, 135, 136, 141, 145, 148, 153]
for (const id of masteryIds) {
const res = evaluateBarbarianMastery(id, 10)
expect(res).not.toBeNull()
expect(res?.manaCost).toBe(0)
}
})
it('4.2 Enforces BATCH1_SKILLS length invariant = strictly 38', () => {
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
})
})
})