feat(bar): implement Barbarian Combat Masteries (IDs 127-129, 134-136, 141, 145, 148, 153)

- Authoritative 1.13c parity for 10 Barbarian Combat Masteries
- Exact linear AR, Damage, Stamina, Defense progression
- Exact diminishing returns Critical Strike (dm56), Speed, Resist All (dm12)
- Sequential roll for Critical Strike & Deadly Strike
- Adversarial stress tests (slvl 0, 1, 10, 20, 99)
- Enforces BATCH1_SKILLS length invariant = strictly 38
- Fixes #465
This commit is contained in:
troytt 2026-09-25 02:02:04 +00:00
parent bd4920f3aa
commit 0c3388b18b
2 changed files with 757 additions and 0 deletions

View File

@ -0,0 +1,474 @@
/**
* Diablo II: Lord of Destruction v1.13c — Barbarian Combat Masteries Module
*
* Dedicated pure calculation and kinematics formulas for the 10 Barbarian Combat Masteries:
* - Skill 127: Sword Mastery (ReqLevel 1)
* - Skill 128: Axe Mastery (ReqLevel 1)
* - Skill 129: Mace Mastery (ReqLevel 1)
* - Skill 134: Polearm Mastery (ReqLevel 6)
* - Skill 135: Throwing Mastery (ReqLevel 6)
* - Skill 136: Spear Mastery (ReqLevel 6)
* - Skill 141: Increased Stamina (ReqLevel 12)
* - Skill 145: Iron Skin (ReqLevel 18)
* - Skill 148: Increased Speed (ReqLevel 24)
* - Skill 153: Natural Resistance (ReqLevel 30)
*
* 1.13c 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 (no double-crit / 4x dmg)
* - Zero runtime MPQ reading
*/
import { computeDiminishingReturns } from '../engine/calc-ast.ts'
// --- Interfaces for Skill Calculations ---
export interface WeaponMasteryStats {
readonly skillId: number
readonly skillName: string
readonly weaponCategory: string
readonly attackRatingBonusPct: number
readonly damageBonusPct: number
readonly criticalStrikeChancePct: number
readonly manaCost: number
readonly manaCost256: number
}
export interface IncreasedStaminaStats {
readonly staminaBonusPct: number
readonly manaCost: number
readonly manaCost256: number
}
export interface IronSkinStats {
readonly defenseBonusPct: number
readonly manaCost: number
readonly manaCost256: number
}
export interface IncreasedSpeedStats {
readonly runWalkSpeedBonusPct: number
readonly manaCost: number
readonly manaCost256: number
}
export interface NaturalResistanceStats {
readonly resistAllPct: number
readonly fireResistPct: number
readonly coldResistPct: number
readonly lightningResistPct: number
readonly poisonResistPct: number
readonly manaCost: number
readonly manaCost256: number
}
// --- Calculation Implementations ---
/**
* Skill 127: Sword Mastery
* AR: 28 + 8 * (slvl - 1)%
* Dmg: 28 + 5 * (slvl - 1)%
* Crit: dm(0, 35, slvl)%
*/
export function calculateSwordMasteryStats(slvl: number): WeaponMasteryStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
skillId: 127,
skillName: 'Sword Mastery',
weaponCategory: 'sword',
attackRatingBonusPct: 0,
damageBonusPct: 0,
criticalStrikeChancePct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
skillId: 127,
skillName: 'Sword Mastery',
weaponCategory: 'sword',
attackRatingBonusPct: 28 + 8 * (lvl - 1),
damageBonusPct: 28 + 5 * (lvl - 1),
criticalStrikeChancePct: computeDiminishingReturns(0, 35, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 128: Axe Mastery
* AR: 28 + 8 * (slvl - 1)%
* Dmg: 28 + 5 * (slvl - 1)%
* Crit: dm(0, 35, slvl)%
*/
export function calculateAxeMasteryStats(slvl: number): WeaponMasteryStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
skillId: 128,
skillName: 'Axe Mastery',
weaponCategory: 'axe',
attackRatingBonusPct: 0,
damageBonusPct: 0,
criticalStrikeChancePct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
skillId: 128,
skillName: 'Axe Mastery',
weaponCategory: 'axe',
attackRatingBonusPct: 28 + 8 * (lvl - 1),
damageBonusPct: 28 + 5 * (lvl - 1),
criticalStrikeChancePct: computeDiminishingReturns(0, 35, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 129: Mace Mastery
* AR: 28 + 8 * (slvl - 1)%
* Dmg: 28 + 5 * (slvl - 1)%
* Crit: dm(0, 35, slvl)%
* Applies to: Maces, Clubs, Hammers, Flails, Scepters
*/
export function calculateMaceMasteryStats(slvl: number): WeaponMasteryStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
skillId: 129,
skillName: 'Mace Mastery',
weaponCategory: 'mace',
attackRatingBonusPct: 0,
damageBonusPct: 0,
criticalStrikeChancePct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
skillId: 129,
skillName: 'Mace Mastery',
weaponCategory: 'mace',
attackRatingBonusPct: 28 + 8 * (lvl - 1),
damageBonusPct: 28 + 5 * (lvl - 1),
criticalStrikeChancePct: computeDiminishingReturns(0, 35, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 134: Polearm Mastery
* AR: 30 + 8 * (slvl - 1)%
* Dmg: 28 + 5 * (slvl - 1)%
* Crit: dm(0, 35, slvl)%
*/
export function calculatePolearmMasteryStats(slvl: number): WeaponMasteryStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
skillId: 134,
skillName: 'Polearm Mastery',
weaponCategory: 'polearm',
attackRatingBonusPct: 0,
damageBonusPct: 0,
criticalStrikeChancePct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
skillId: 134,
skillName: 'Polearm Mastery',
weaponCategory: 'polearm',
attackRatingBonusPct: 30 + 8 * (lvl - 1),
damageBonusPct: 28 + 5 * (lvl - 1),
criticalStrikeChancePct: computeDiminishingReturns(0, 35, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 135: Throwing Mastery
* AR: 30 + 8 * (slvl - 1)%
* Dmg: 28 + 5 * (slvl - 1)%
* Crit: dm(0, 35, slvl)%
* Applies to: Throwing Knives, Throwing Axes, Javelins (when thrown)
*/
export function calculateThrowingMasteryStats(slvl: number): WeaponMasteryStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
skillId: 135,
skillName: 'Throwing Mastery',
weaponCategory: 'throwing',
attackRatingBonusPct: 0,
damageBonusPct: 0,
criticalStrikeChancePct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
skillId: 135,
skillName: 'Throwing Mastery',
weaponCategory: 'throwing',
attackRatingBonusPct: 30 + 8 * (lvl - 1),
damageBonusPct: 28 + 5 * (lvl - 1),
criticalStrikeChancePct: computeDiminishingReturns(0, 35, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 136: Spear Mastery
* AR: 30 + 8 * (slvl - 1)%
* Dmg: 28 + 5 * (slvl - 1)%
* Crit: dm(0, 35, slvl)%
* Applies to: Spears and Javelins (in melee)
*/
export function calculateSpearMasteryStats(slvl: number): WeaponMasteryStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
skillId: 136,
skillName: 'Spear Mastery',
weaponCategory: 'spear',
attackRatingBonusPct: 0,
damageBonusPct: 0,
criticalStrikeChancePct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
skillId: 136,
skillName: 'Spear Mastery',
weaponCategory: 'spear',
attackRatingBonusPct: 30 + 8 * (lvl - 1),
damageBonusPct: 28 + 5 * (lvl - 1),
criticalStrikeChancePct: computeDiminishingReturns(0, 35, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 141: Increased Stamina
* Stamina %: 30 + 15 * (slvl - 1)%
*/
export function calculateIncreasedStaminaStats(slvl: number): IncreasedStaminaStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
staminaBonusPct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
staminaBonusPct: 30 + 15 * (lvl - 1),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 145: Iron Skin
* Defense %: 30 + 10 * (slvl - 1)%
*/
export function calculateIronSkinStats(slvl: number): IronSkinStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
defenseBonusPct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
defenseBonusPct: 30 + 10 * (lvl - 1),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 148: Increased Speed
* Velocity %: dm(7, 50, slvl)%
*/
export function calculateIncreasedSpeedStats(slvl: number): IncreasedSpeedStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
runWalkSpeedBonusPct: 0,
manaCost: 0,
manaCost256: 0,
}
}
return {
runWalkSpeedBonusPct: computeDiminishingReturns(7, 50, lvl),
manaCost: 0,
manaCost256: 0,
}
}
/**
* Skill 153: Natural Resistance
* Elemental Resistances (Fire, Cold, Lightning, Poison): dm(0, 80, slvl)%
*/
export function calculateNaturalResistanceStats(slvl: number): NaturalResistanceStats {
const lvl = Math.max(0, Math.floor(slvl))
if (lvl === 0) {
return {
resistAllPct: 0,
fireResistPct: 0,
coldResistPct: 0,
lightningResistPct: 0,
poisonResistPct: 0,
manaCost: 0,
manaCost256: 0,
}
}
const res = computeDiminishingReturns(0, 80, lvl)
return {
resistAllPct: res,
fireResistPct: res,
coldResistPct: res,
lightningResistPct: res,
poisonResistPct: res,
manaCost: 0,
manaCost256: 0,
}
}
// --- Weapon & Crit Evaluation Helpers ---
/**
* Checks if a given weapon type string matches the target Barbarian Mastery skill.
*/
export function isWeaponMatchingMastery(weaponType: string, skillId: number): boolean {
const norm = weaponType.toLowerCase().trim()
switch (skillId) {
case 127: // Sword Mastery
return (
norm.includes('sword') ||
norm.includes('blade') ||
norm.includes('falchion') ||
norm.includes('scimitar') ||
norm.includes('saber') ||
norm.includes('claymore') ||
norm.includes('zweihander')
)
case 128: // Axe Mastery
return norm.includes('axe') || norm.includes('cleaver') || norm.includes('hatchet')
case 129: // Mace Mastery
return (
norm.includes('mace') ||
norm.includes('club') ||
norm.includes('hammer') ||
norm.includes('flail') ||
norm.includes('scepter') ||
norm.includes('morning star')
)
case 134: // Polearm Mastery
return (
norm.includes('polearm') ||
norm.includes('scythe') ||
norm.includes('voulge') ||
norm.includes('thresher') ||
norm.includes('halberd') ||
norm.includes('bardiche') ||
norm.includes('lochaber') ||
norm.includes('bill')
)
case 135: // Throwing Mastery
return (
norm.includes('throw') ||
norm.includes('javelin') ||
norm.includes('dart') ||
norm.includes('pilum') ||
norm.includes('harpoon')
)
case 136: // Spear Mastery
return (
norm.includes('spear') ||
norm.includes('pike') ||
norm.includes('lance') ||
norm.includes('trident') ||
norm.includes('javelin')
)
default:
return false
}
}
/**
* Evaluates effective double-damage proc chance combining Mastery Critical Strike and Deadly Strike.
*
* In Diablo II 1.13c:
* - Critical Strike (Mastery/Amazon) rolls first.
* - If CS does not proc, Deadly Strike (items) rolls second.
* - They NEVER stack to quadruple damage; damage is doubled at most once:
* P(double damage) = CS% + DS% * (1 - CS% / 100)
*/
export function calculateEffectiveCriticalStrike(
masteryCritPct: number,
deadlyStrikePct: number
): number {
const cs = Math.max(0, Math.min(100, masteryCritPct)) / 100
const ds = Math.max(0, Math.min(100, deadlyStrikePct)) / 100
const combined = cs + ds * (1 - cs)
return Math.round(combined * 1000) / 10 // rounded to 1 decimal place %
}
/**
* Unified dispatch evaluation for Barbarian Combat Masteries.
*/
export function evaluateBarbarianMastery(
skillId: number,
slvl: number
): WeaponMasteryStats | IncreasedStaminaStats | IronSkinStats | IncreasedSpeedStats | NaturalResistanceStats | null {
switch (skillId) {
case 127:
return calculateSwordMasteryStats(slvl)
case 128:
return calculateAxeMasteryStats(slvl)
case 129:
return calculateMaceMasteryStats(slvl)
case 134:
return calculatePolearmMasteryStats(slvl)
case 135:
return calculateThrowingMasteryStats(slvl)
case 136:
return calculateSpearMasteryStats(slvl)
case 141:
return calculateIncreasedStaminaStats(slvl)
case 145:
return calculateIronSkinStats(slvl)
case 148:
return calculateIncreasedSpeedStats(slvl)
case 153:
return calculateNaturalResistanceStats(slvl)
default:
return null
}
}

View File

@ -0,0 +1,283 @@
/**
* 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)
})
})
})