241 lines
10 KiB
TypeScript
241 lines
10 KiB
TypeScript
/**
|
|
* Diablo II: Lord of Destruction v1.13c — Barbarian Milestone M20 Class Gate & End-to-End Suite
|
|
*
|
|
* Comprehensive cross-tree integration and architectural verification across all 30 Barbarian skills:
|
|
*
|
|
* Skill Trees Covered:
|
|
* 1. Combat Skills:
|
|
* - Bash (126), Leap (132), Double Swing (133), Stun (139), Double Throw (140),
|
|
* Leap Attack (143), Concentrate (144), Frenzy (147), Whirlwind (151), Berserk (152).
|
|
* 2. Combat Masteries:
|
|
* - Sword Mastery (127), Axe Mastery (128), Mace Mastery (129), Polearm Mastery (134),
|
|
* Throwing Mastery (135), Spear Mastery (136), Increased Stamina (141), Iron Skin (145),
|
|
* Increased Speed (148), Natural Resistance (153).
|
|
* 3. Warcries:
|
|
* - Howl (130), Find Potion (131), Taunt (137), Shout (138), Find Item (142),
|
|
* Battle Cry (146), Battle Orders (149), Grim Ward (150), War Cry (154), Battle Command (155).
|
|
*
|
|
* Core Verification Scenarios:
|
|
* Scenario 1: Cross-Tree Buff & Warcry Duration Network (Shout, Battle Orders, Battle Command)
|
|
* Scenario 2: Berserk 100% Magic Conversion & Zero-Defense Vulnerability Duration
|
|
* Scenario 3: Frenzy Dual-Wield Stacking & Momentum Kinematics
|
|
* Scenario 4: Whirlwind Uninterruptible Area Sweeping & 1.13c Damage Progression
|
|
* Scenario 5: War Cry Radial Stun & 5-Band Physical Damage Synergies
|
|
* Scenario 6: Weapon Masteries, Diminishing Returns Critical Strike & Stacking
|
|
* Scenario 7: Corpse & Crowd Control Utilities (Find Potion, Find Item, Grim Ward, Taunt, Battle Cry)
|
|
* Scenario 8: Complete 30-Skill Barbarian Tree Registry & Architectural Invariants
|
|
*
|
|
* ZERO MOCKS: vi.mock / vi.spyOn strictly forbidden.
|
|
* ZERO CIRCULAR ORACLES: Precalculated independently from 1.13c ground truth constants.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import { BATCH1_SKILLS } from '../../../src/game/skills.ts'
|
|
import {
|
|
calculateBashStats,
|
|
calculateLeapStats,
|
|
calculateDoubleSwingStats,
|
|
calculateStunStats,
|
|
calculateDoubleThrowStats,
|
|
calculateLeapAttackStats,
|
|
calculateConcentrateStats,
|
|
calculateFrenzyStats,
|
|
calculateWhirlwindStats,
|
|
calculateBerserkStats,
|
|
} from '../../../src/game/skills/barbarian-combat.ts'
|
|
import {
|
|
calculateSwordMasteryStats,
|
|
calculateAxeMasteryStats,
|
|
calculateMaceMasteryStats,
|
|
calculatePolearmMasteryStats,
|
|
calculateThrowingMasteryStats,
|
|
calculateSpearMasteryStats,
|
|
calculateIncreasedStaminaStats,
|
|
calculateIronSkinStats,
|
|
calculateIncreasedSpeedStats,
|
|
calculateNaturalResistanceStats,
|
|
isWeaponMatchingMastery,
|
|
calculateEffectiveCriticalStrike,
|
|
evaluateBarbarianMastery,
|
|
} from '../../../src/game/skills/barbarian-masteries.ts'
|
|
import {
|
|
calculateHowlStats,
|
|
calculateFindPotionStats,
|
|
calculateTauntStats,
|
|
calculateShoutStats,
|
|
calculateFindItemStats,
|
|
calculateBattleCryStats,
|
|
calculateBattleOrdersStats,
|
|
calculateGrimWardStats,
|
|
calculateWarCryStats,
|
|
calculateBattleCommandStats,
|
|
evaluateBarbarianWarcry,
|
|
} from '../../../src/game/skills/barbarian-warcries.ts'
|
|
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
|
|
|
|
describe('Milestone M20 — Barbarian Class Gate & Comprehensive E2E Integration Suite', () => {
|
|
describe('Scenario 1: Cross-Tree Buff & Warcry Duration Network (Shout, Battle Orders, Battle Command)', () => {
|
|
it('verifies mutual duration synergy (+125 frames / 5s per blvl)', () => {
|
|
// Base durations
|
|
const shoutBase = calculateShoutStats(20, 0, 0)
|
|
const boBase = calculateBattleOrdersStats(20, 0, 0)
|
|
const bcBase = calculateBattleCommandStats(20, 0, 0)
|
|
|
|
expect(shoutBase.durationFrames).toBe(5250) // 210s
|
|
expect(boBase.durationFrames).toBe(5500) // 220s
|
|
expect(bcBase.durationFrames).toBe(4875) // 195s
|
|
|
|
// Fully synergized party shout (20 BO, 20 BC): +5000 frames (200s)
|
|
const shoutMax = calculateShoutStats(20, 20, 20)
|
|
expect(shoutMax.durationFrames).toBe(10250) // 410s
|
|
expect(shoutMax.durationSeconds).toBe(410.0)
|
|
|
|
// Fully synergized Battle Orders (20 Shout, 20 BC): +5000 frames
|
|
const boMax = calculateBattleOrdersStats(20, 20, 20)
|
|
expect(boMax.durationFrames).toBe(10500) // 420s
|
|
expect(boMax.durationSeconds).toBe(420.0)
|
|
|
|
// Stat boosts: Shout defense (+290%), BO life/mana (+92%), BC all skills (+1)
|
|
expect(shoutMax.defenseBonusPct).toBe(290)
|
|
expect(boMax.maxLifeBonusPct).toBe(92)
|
|
expect(boMax.maxManaBonusPct).toBe(92)
|
|
expect(bcBase.allSkillsBonus).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('Scenario 2: Berserk 100% Magic Conversion & Zero-Defense Vulnerability', () => {
|
|
it('verifies 100% physical to magic damage conversion and vulnerability duration formula', () => {
|
|
const zrk1 = calculateBerserkStats(1, 0, 0)
|
|
expect(zrk1.magicConversionPct).toBe(100)
|
|
expect(zrk1.zeroDefenseDurationFrames).toBe(68) // 75 - floor(110/7 * 0.5) = 75 - 7 = 68
|
|
|
|
const zrk10 = calculateBerserkStats(10, 0, 0)
|
|
expect(zrk10.magicConversionPct).toBe(100)
|
|
expect(zrk10.zeroDefenseDurationFrames).toBeLessThan(68)
|
|
expect(zrk10.zeroDefenseDurationFrames).toBeGreaterThan(25)
|
|
|
|
const zrk20 = calculateBerserkStats(20, 0, 0)
|
|
expect(zrk20.zeroDefenseDurationFrames).toBeLessThan(zrk10.zeroDefenseDurationFrames)
|
|
expect(zrk20.zeroDefenseDurationFrames).toBeGreaterThanOrEqual(25)
|
|
|
|
// Synergies: Howl +10%/lvl, Shout +10%/lvl (150 base + 19 * 15 + 400 = 835%)
|
|
const zrkMax = calculateBerserkStats(20, 20, 20)
|
|
expect(zrkMax.damageBonusPct).toBe(150 + 19 * 15 + 400)
|
|
expect(zrkMax.magicConversionPct).toBe(100)
|
|
})
|
|
})
|
|
|
|
describe('Scenario 3: Frenzy Dual-Wield Stacking & Momentum Kinematics', () => {
|
|
it('verifies dual-wield requirements, speed scaling, and 150-frame timer', () => {
|
|
const fr1 = calculateFrenzyStats(1, 0, 0, 0)
|
|
expect(fr1.requiresDualWield).toBe(true)
|
|
expect(fr1.durationFrames).toBe(150) // 6.0s
|
|
expect(fr1.minRunSpeedPct).toBe(20)
|
|
|
|
const fr20 = calculateFrenzyStats(20, 20, 20, 20)
|
|
expect(fr20.durationFrames).toBe(150)
|
|
expect(fr20.damageBonusPct).toBe(90 + 19 * 5 + 20 * 8 + 20 * 8) // 90 base + 95 lvl + 160 DS + 160 Taunt
|
|
expect(fr20.magicConversionPct).toBe(20) // 20% from Berserk
|
|
})
|
|
})
|
|
|
|
describe('Scenario 4: Whirlwind Uninterruptible Area Sweeping & 1.13c Damage Progression', () => {
|
|
it('verifies authentic negative scaling at slvl 1 and 4-frame hit checks', () => {
|
|
const ww1 = calculateWhirlwindStats(1)
|
|
expect(ww1.damageBonusPct).toBe(-50)
|
|
expect(ww1.hitFrequencyFrames).toBe(4)
|
|
|
|
const ww10 = calculateWhirlwindStats(10)
|
|
expect(ww10.damageBonusPct).toBe(22) // -50 + 8 * 9
|
|
|
|
const ww20 = calculateWhirlwindStats(20)
|
|
expect(ww20.damageBonusPct).toBe(102) // -50 + 8 * 19
|
|
expect(ww20.hitFrequencyFrames).toBe(4)
|
|
})
|
|
})
|
|
|
|
describe('Scenario 5: War Cry Radial Stun & 5-Band Physical Damage Synergies', () => {
|
|
it('verifies 5-band damage scaling, stun length, and synergies', () => {
|
|
const wc1 = calculateWarCryStats(1, 0, 0, 0)
|
|
expect(wc1.minPhysicalDamage).toBe(20)
|
|
expect(wc1.maxPhysicalDamage).toBe(30)
|
|
expect(wc1.stunDurationFrames).toBe(25) // 1.0s
|
|
|
|
const wc10 = calculateWarCryStats(10, 0, 0, 0)
|
|
expect(wc10.minPhysicalDamage).toBe(76)
|
|
expect(wc10.maxPhysicalDamage).toBe(86)
|
|
expect(wc10.stunDurationFrames).toBe(70) // 2.8s
|
|
|
|
const wc20 = calculateWarCryStats(20, 0, 0, 0)
|
|
expect(wc20.minPhysicalDamage).toBe(150)
|
|
expect(wc20.maxPhysicalDamage).toBe(160)
|
|
expect(wc20.stunDurationFrames).toBe(120) // 4.8s
|
|
|
|
// Fully synergized (20 Howl, 20 Taunt, 20 Battle Cry): +360% dmg
|
|
const wcMax = calculateWarCryStats(20, 20, 20, 20)
|
|
expect(wcMax.synergyBonusPct).toBe(360)
|
|
expect(wcMax.minPhysicalDamage).toBe(690)
|
|
expect(wcMax.maxPhysicalDamage).toBe(736)
|
|
})
|
|
})
|
|
|
|
describe('Scenario 6: Weapon Masteries, Diminishing Returns Critical Strike & Stacking', () => {
|
|
it('verifies weapon category recognition and non-quadrupling critical strike rolls', () => {
|
|
// Weapon matching
|
|
expect(isWeaponMatchingMastery('legend sword', 127)).toBe(true)
|
|
expect(isWeaponMatchingMastery('glorious axe', 128)).toBe(true)
|
|
expect(isWeaponMatchingMastery('thunder maul', 129)).toBe(true)
|
|
expect(isWeaponMatchingMastery('cryptic axe', 134)).toBe(true)
|
|
expect(isWeaponMatchingMastery('winged harpoon', 135)).toBe(true)
|
|
expect(isWeaponMatchingMastery('ghost spear', 136)).toBe(true)
|
|
|
|
// Sequential CS + DS roll
|
|
const combined = calculateEffectiveCriticalStrike(29, 50)
|
|
// 29% CS, 50% DS -> 29 + 50 * (1 - 0.29) = 29 + 35.5 = 64.5%
|
|
expect(combined).toBe(64.5)
|
|
|
|
// Masteries evaluate cleanly
|
|
const sw20 = calculateSwordMasteryStats(20)
|
|
expect(sw20.criticalStrikeChancePct).toBe(29)
|
|
expect(sw20.attackRatingBonusPct).toBe(180)
|
|
expect(sw20.damageBonusPct).toBe(123)
|
|
})
|
|
})
|
|
|
|
describe('Scenario 7: Corpse & Crowd Control Utilities (Find Potion, Find Item, Grim Ward, Taunt, Battle Cry)', () => {
|
|
it('verifies corpse requirements and debuff percentages', () => {
|
|
expect(calculateFindPotionStats(20).requiresCorpse).toBe(true)
|
|
expect(calculateFindItemStats(20).requiresCorpse).toBe(true)
|
|
expect(calculateGrimWardStats(20).requiresCorpse).toBe(true)
|
|
|
|
expect(calculateGrimWardStats(20).totemDurationFrames).toBe(1000)
|
|
expect(calculateGrimWardStats(20).fleeRadiusYards).toBe(22)
|
|
|
|
const bc20 = calculateBattleCryStats(20)
|
|
expect(bc20.targetDefenseReductionPct).toBe(88)
|
|
expect(bc20.targetDamageReductionPct).toBe(44)
|
|
|
|
const taunt20 = calculateTauntStats(20)
|
|
expect(taunt20.targetAttackRatingReductionPct).toBe(43)
|
|
expect(taunt20.targetDamageReductionPct).toBe(43)
|
|
expect(taunt20.forcesMeleeAttack).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Scenario 8: Complete 30-Skill Barbarian Tree Registry & Architectural Invariants', () => {
|
|
it('verifies all 30 Barbarian skills exist in the 1.13c Data Registry', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
// Barbarian skill IDs are 126..155 inclusive
|
|
for (let id = 126; id <= 155; id++) {
|
|
const skill = registry.getSkillById(id)
|
|
expect(skill, `Skill ${id} must exist in registry`).toBeDefined()
|
|
expect(skill?.charClass).toBe('bar')
|
|
}
|
|
})
|
|
|
|
it('enforces BATCH1_SKILLS length invariant = strictly 38', () => {
|
|
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
|
|
})
|
|
})
|
|
})
|