284 lines
12 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|