From 1f14b311c71275e25238159e3db546accfa41a60 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Fri, 25 Sep 2026 02:12:47 +0000 Subject: [PATCH] feat(bar): Barbarian Class Final Quality Gate & E2E Verification (30/30 Skills) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/game/skills/barbarian-masteries.ts | 11 +- tests/skills/bar/adv-bar-e2e-gate.test.ts | 240 ++++++++++++++++++++++ 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 tests/skills/bar/adv-bar-e2e-gate.test.ts diff --git a/src/game/skills/barbarian-masteries.ts b/src/game/skills/barbarian-masteries.ts index d0723ed..4269717 100644 --- a/src/game/skills/barbarian-masteries.ts +++ b/src/game/skills/barbarian-masteries.ts @@ -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 ( diff --git a/tests/skills/bar/adv-bar-e2e-gate.test.ts b/tests/skills/bar/adv-bar-e2e-gate.test.ts new file mode 100644 index 0000000..d061fac --- /dev/null +++ b/tests/skills/bar/adv-bar-e2e-gate.test.ts @@ -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) + }) + }) +})