feat(bar): Barbarian Class Final Quality Gate & E2E Verification (30/30 Skills)
- Complete E2E verification across all 30 Barbarian skills (IDs 126–155) - Cross-tree Warcry buff duration network (Shout, Battle Orders, Battle Command) - Berserk 100% magic conversion & zero-defense vulnerability duration - Frenzy momentum speed scaling & dual-wield requirements - Whirlwind uninterruptible path sweeping & 4-frame hit checks - War Cry 5-band physical damage progression & stun length - Combat Masteries & Critical Strike sequential roll without 4x stacking - Refined weapon matching for cryptic axe, poleaxe, and mauls - Enforces BATCH1_SKILLS length invariant = strictly 38 - Fixes #467
This commit is contained in:
parent
dbd80db251
commit
1f14b311c7
|
|
@ -379,12 +379,17 @@ export function isWeaponMatchingMastery(weaponType: string, skillId: number): bo
|
||||||
norm.includes('zweihander')
|
norm.includes('zweihander')
|
||||||
)
|
)
|
||||||
case 128: // Axe Mastery
|
case 128: // Axe Mastery
|
||||||
return norm.includes('axe') || norm.includes('cleaver') || norm.includes('hatchet')
|
return (
|
||||||
|
(norm.includes('axe') && !norm.includes('poleaxe') && !norm.includes('cryptic axe')) ||
|
||||||
|
norm.includes('cleaver') ||
|
||||||
|
norm.includes('hatchet')
|
||||||
|
)
|
||||||
case 129: // Mace Mastery
|
case 129: // Mace Mastery
|
||||||
return (
|
return (
|
||||||
norm.includes('mace') ||
|
norm.includes('mace') ||
|
||||||
norm.includes('club') ||
|
norm.includes('club') ||
|
||||||
norm.includes('hammer') ||
|
norm.includes('hammer') ||
|
||||||
|
norm.includes('maul') ||
|
||||||
norm.includes('flail') ||
|
norm.includes('flail') ||
|
||||||
norm.includes('scepter') ||
|
norm.includes('scepter') ||
|
||||||
norm.includes('morning star')
|
norm.includes('morning star')
|
||||||
|
|
@ -398,7 +403,9 @@ export function isWeaponMatchingMastery(weaponType: string, skillId: number): bo
|
||||||
norm.includes('halberd') ||
|
norm.includes('halberd') ||
|
||||||
norm.includes('bardiche') ||
|
norm.includes('bardiche') ||
|
||||||
norm.includes('lochaber') ||
|
norm.includes('lochaber') ||
|
||||||
norm.includes('bill')
|
norm.includes('bill') ||
|
||||||
|
norm.includes('poleaxe') ||
|
||||||
|
norm.includes('cryptic axe')
|
||||||
)
|
)
|
||||||
case 135: // Throwing Mastery
|
case 135: // Throwing Mastery
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue