From 0c3388b18bccdc8f0f671d5525f53394b06dd64d Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Fri, 25 Sep 2026 02:02:04 +0000 Subject: [PATCH] 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 --- src/game/skills/barbarian-masteries.ts | 474 ++++++++++++++++++ .../bar/adv-bar-masteries-stress.test.ts | 283 +++++++++++ 2 files changed, 757 insertions(+) create mode 100644 src/game/skills/barbarian-masteries.ts create mode 100644 tests/skills/bar/adv-bar-masteries-stress.test.ts diff --git a/src/game/skills/barbarian-masteries.ts b/src/game/skills/barbarian-masteries.ts new file mode 100644 index 0000000..d0723ed --- /dev/null +++ b/src/game/skills/barbarian-masteries.ts @@ -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 + } +} diff --git a/tests/skills/bar/adv-bar-masteries-stress.test.ts b/tests/skills/bar/adv-bar-masteries-stress.test.ts new file mode 100644 index 0000000..9256e42 --- /dev/null +++ b/tests/skills/bar/adv-bar-masteries-stress.test.ts @@ -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) + }) + }) +})