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')
|
||||
)
|
||||
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
|
||||
return (
|
||||
norm.includes('mace') ||
|
||||
norm.includes('club') ||
|
||||
norm.includes('hammer') ||
|
||||
norm.includes('maul') ||
|
||||
norm.includes('flail') ||
|
||||
norm.includes('scepter') ||
|
||||
norm.includes('morning star')
|
||||
|
|
@ -398,7 +403,9 @@ export function isWeaponMatchingMastery(weaponType: string, skillId: number): bo
|
|||
norm.includes('halberd') ||
|
||||
norm.includes('bardiche') ||
|
||||
norm.includes('lochaber') ||
|
||||
norm.includes('bill')
|
||||
norm.includes('bill') ||
|
||||
norm.includes('poleaxe') ||
|
||||
norm.includes('cryptic axe')
|
||||
)
|
||||
case 135: // Throwing Mastery
|
||||
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