From 2e65415aed9ee60c16091733909cc2321312062d Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Fri, 25 Sep 2026 03:11:06 +0000 Subject: [PATCH] feat(dru): Milestone M25 Druid Class Gate & End-to-End Suite across all 30 skills (closes #472) --- tests/skills/dru/adv-dru-e2e-gate.test.ts | 395 ++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 tests/skills/dru/adv-dru-e2e-gate.test.ts diff --git a/tests/skills/dru/adv-dru-e2e-gate.test.ts b/tests/skills/dru/adv-dru-e2e-gate.test.ts new file mode 100644 index 0000000..44d243e --- /dev/null +++ b/tests/skills/dru/adv-dru-e2e-gate.test.ts @@ -0,0 +1,395 @@ +/** + * Diablo II: Lord of Destruction v1.13c — Druid Milestone M25 Class Gate & End-to-End Suite + * + * Comprehensive cross-tree integration and architectural verification across all 30 Druid skills: + * + * Skill Trees Covered: + * 1. Elemental (10 skills): + * - Firestorm (225), Molten Boulder (229), Arctic Blast (230), Fissure (234), + * Cyclone Armor (235), Twister (240), Volcano (244), Tornado (245), + * Armageddon (249), Hurricane (250). + * 2. Shape Shifting (10 skills): + * - Werewolf (223), Lycanthropy (224), Werebear (228), Feral Rage (232), + * Maul (233), Rabies (238), Fire Claws (239), Hunger (242), + * Shock Wave (243), Fury (248). + * 3. Summoning (10 skills): + * - Raven (221), Poison Creeper (222), Oak Sage (226), Summon Spirit Wolf (227), + * Carrion Vine (231), Heart of Wolverine (236), Summon Dire Wolf (237), + * Solar Creeper (241), Spirit of Barbs (246), Summon Grizzly (247). + * + * Core Verification Scenarios: + * Scenario 1: Cross-Tree Synergy: Armageddon + Hurricane + Cyclone Armor Elemental Triad + * Scenario 2: Fire Claws Quad-Elemental Synergy (+22%/blvl) in Beast Form + * Scenario 3: Beast Form State Restriction & Transformation Lifecycle (Werewolf vs Werebear) + * Scenario 4: Rabies Contagion & Poison Creeper Synergy + * Scenario 5: Druid Summoning Hierarchy, Mutual Exclusivity, and Raven Coexistence + * Scenario 6: Wolf & Grizzly Cross-Species Passive Synergy Network + * Scenario 7: Vine Corpse Consumption & Dire Wolf Corpse Frenzy Cycles + * Scenario 8: Complete 30-Skill Druid 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 { + calculateCycloneArmorStats, + calculateArmageddonStats, + calculateHurricaneStats, +} from '../../../src/game/skills/druid-elemental.ts' +import { + DRUID_SHAPESHIFT_SKILL_IDS, + calculateWerewolfStats, + calculateLycanthropyStats, + calculateWerebearStats, + calculateRabiesStats, + calculateFireClawsStats, + canCastShapeShiftSkill, + simulateRabiesContagion, +} from '../../../src/game/skills/druid-shapeshift.ts' +import { + DRUID_SUMMON_SKILL_IDS, + DruidPetGroup, + calculateSpiritWolfStats, + calculateDireWolfStats, + calculateGrizzlyStats, + resolvePetSummon, + simulateVineConsumption, + simulateDireWolfCorpseFrenzy, + type ActivePetRecord, +} from '../../../src/game/skills/druid-summon.ts' +import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts' + +describe('Milestone M25 — Druid Class Gate & Comprehensive E2E Integration Suite', () => { + // ========================================================================= + // Scenario 1: Cross-Tree Synergy: Armageddon + Hurricane + Cyclone Armor + // ========================================================================= + describe('Scenario 1: Cross-Tree Synergy: Armageddon + Hurricane + Cyclone Armor Elemental Triad', () => { + it('verifies Armageddon fire impact damage and duration scaling with full synergies', () => { + // Base Armageddon slvl 20 (no synergies) + const baseArm = calculateArmageddonStats(20, 0, 0, 0, 0) + expect(baseArm.durationFrames).toBe(250) // 10s base + expect(baseArm.minFireDamage).toBe(390) + expect(baseArm.maxFireDamage).toBe(471) + + // Fully synergized Armageddon (20 Fissure, 20 Firestorm, 0 Molten, 20 Volcano): + // Fissure duration: +20 * 50 = +1000 frames -> 1250 frames (50s) + // Fire damage synergy: 20 * 14% (Volcano) + 20 * 14% (Firestorm) = +560% + const synArm = calculateArmageddonStats(20, 20, 20, 0, 20) + expect(synArm.durationFrames).toBe(1250) // 50s + expect(synArm.durationSeconds).toBe(50.0) + expect(synArm.minFireDamage).toBe(Math.trunc((390 * 660) / 100)) // 2574 + expect(synArm.maxFireDamage).toBe(Math.trunc((471 * 660) / 100)) // 3108 + }) + + it('verifies Hurricane cold damage and duration scaling with full synergies', () => { + // Base Hurricane slvl 20 (no synergies) + const baseHur = calculateHurricaneStats(20, 0, 0, 0) + expect(baseHur.durationFrames).toBe(250) // 10s base + expect(baseHur.minColdDamage).toBe(202) + expect(baseHur.maxColdDamage).toBe(227) + + // Fully synergized Hurricane (20 Cyclone Armor, 20 Twister, 20 Tornado): + // Cyclone Armor duration: +20 * 50 = +1000 frames -> 1250 frames (50s) + // Cold damage synergy: 20 * 9% (Twister) + 20 * 9% (Tornado) = +360% + const synHur = calculateHurricaneStats(20, 20, 20, 20) + expect(synHur.durationFrames).toBe(1250) // 50s + expect(synHur.minColdDamage).toBe(Math.trunc((202 * 460) / 100)) // 929 + expect(synHur.maxColdDamage).toBe(Math.trunc((227 * 460) / 100)) // 1044 + }) + + it('verifies Cyclone Armor absorption scaling across Fire, Cold, and Lightning', () => { + // Base Cyclone Armor slvl 20: 268 absorbed damage (40 + 19*12) + const baseCa = calculateCycloneArmorStats(20, 0, 0, 0) + expect(baseCa.baseAbsorption).toBe(268) + + // Fully synergized Cyclone Armor (20 Twister, 20 Tornado, 0 Hurricane): + // Synergy: 20 * 7% (Twister) + 20 * 7% (Tornado) = +280% + const synCa = calculateCycloneArmorStats(20, 20, 20, 0) + expect(synCa.totalAbsorption).toBe(Math.trunc((268 * 380) / 100)) // 1018 + expect(synCa.absorbedElements).toEqual(['fire', 'cold', 'lightning']) + }) + }) + + // ========================================================================= + // Scenario 2: Fire Claws Quad-Elemental Synergy in Beast Form + // ========================================================================= + describe('Scenario 2: Fire Claws Quad-Elemental Synergy (+22%/blvl) in Beast Form', () => { + it('verifies Fire Claws 4-way synergy scaling up to +1760% damage', () => { + // Base Fire Claws slvl 20: 247-260 fire damage + const baseFc = calculateFireClawsStats(20, 0, 0, 0, 0) + expect(baseFc.minFireDamage).toBe(247) + expect(baseFc.maxFireDamage).toBe(260) + expect(baseFc.synergyBonusPct).toBe(0) + + // Quad-elemental max synergy (20 in Firestorm, Molten Boulder, Volcano, Eruption): + // 80 total hard points * 22% = +1760% fire damage! + const maxFc = calculateFireClawsStats(20, 20, 20, 20, 20) + expect(maxFc.synergyBonusPct).toBe(1760) + expect(maxFc.minFireDamage).toBe(Math.trunc((247 * 1860) / 100)) // 4594 + expect(maxFc.maxFireDamage).toBe(Math.trunc((260 * 1860) / 100)) // 4836 + }) + + it('enforces that Fire Claws is valid only in Werewolf or Werebear form, not Human', () => { + expect(canCastShapeShiftSkill('human', DRUID_SHAPESHIFT_SKILL_IDS.FIRE_CLAWS)).toBe(false) + expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.FIRE_CLAWS)).toBe(true) + expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.FIRE_CLAWS)).toBe(true) + }) + }) + + // ========================================================================= + // Scenario 3: Beast Form State Restriction & Transformation Lifecycle + // ========================================================================= + describe('Scenario 3: Beast Form State Restriction & Transformation Lifecycle (Werewolf vs Werebear)', () => { + it('verifies Werewolf stats and Lycanthropy passive duration & life integration', () => { + // Werewolf slvl 20 with Lycanthropy slvl 20: + // Werewolf duration = 1000 frames (40s) + 1000 + 19*500 frames (Lycanthropy) = 11500 frames (460s) + // Max HP% = +25% (Werewolf) + 20% + 19*5% (Lycanthropy = 115%) = +140% total + const ww = calculateWerewolfStats(20, 20) + expect(ww.attackRateBonusPct).toBe(69) + expect(ww.toHitBonusPct).toBe(335) + expect(ww.staminaBonusPct).toBe(25) + expect(ww.durationSeconds).toBe(460) + expect(ww.durationFrames).toBe(11500) + expect(ww.maxHpBonusPct).toBe(140) + }) + + it('verifies Werebear massive armor, physical damage, and life scaling', () => { + // Werebear slvl 20 with Lycanthropy slvl 20: + // Defense bonus: 25 + 19*6 = +139% + // Enhanced Damage: 55 + 19*8 = +207% + // Max HP: +75% (Werebear) + 115% (Lycanthropy) = +190% + const wb = calculateWerebearStats(20, 20) + expect(wb.defenseBonusPct).toBe(139) + expect(wb.damageBonusPct).toBe(207) + expect(wb.maxHpBonusPct).toBe(190) + }) + + it('enforces strict exclusive skill restriction between Wolf and Bear forms', () => { + // Wolf-only skills + expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.FERAL_RAGE)).toBe(true) + expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.FERAL_RAGE)).toBe(false) + expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.RABIES)).toBe(true) + expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.RABIES)).toBe(false) + expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.FURY)).toBe(true) + expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.FURY)).toBe(false) + + // Bear-only skills + expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.MAUL)).toBe(true) + expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.MAUL)).toBe(false) + expect(canCastShapeShiftSkill('bear', DRUID_SHAPESHIFT_SKILL_IDS.SHOCK_WAVE)).toBe(true) + expect(canCastShapeShiftSkill('wolf', DRUID_SHAPESHIFT_SKILL_IDS.SHOCK_WAVE)).toBe(false) + + // Human cannot cast beast attacks + expect(canCastShapeShiftSkill('human', DRUID_SHAPESHIFT_SKILL_IDS.FERAL_RAGE)).toBe(false) + expect(canCastShapeShiftSkill('human', DRUID_SHAPESHIFT_SKILL_IDS.MAUL)).toBe(false) + }) + }) + + // ========================================================================= + // Scenario 4: Rabies Contagion & Poison Creeper Synergy + // ========================================================================= + describe('Scenario 4: Rabies Contagion & Poison Creeper Synergy', () => { + it('verifies Rabies damage scaling and +18%/blvl Poison Creeper synergy', () => { + // Base Rabies slvl 20: 102-110 poison damage over 290 frames (11.6s) + const baseR = calculateRabiesStats(20, 0) + expect(baseR.minPoisonDamage).toBe(102) + expect(baseR.maxPoisonDamage).toBe(110) + expect(baseR.poisonDurationFrames).toBe(290) + expect(baseR.poisonDurationSeconds).toBe(11.6) + + // Fully synergized Rabies (20 hard points in Poison Creeper): +360% poison damage + const synR = calculateRabiesStats(20, 20) + expect(synR.minPoisonDamage).toBe(Math.trunc((102 * 460) / 100)) // 469 + expect(synR.maxPoisonDamage).toBe(Math.trunc((110 * 460) / 100)) // 506 + }) + + it('verifies Rabies contagion spread within 4 subtiles and ignores distant targets', () => { + const infected = [{ id: 'source_mob', x: 100, y: 100 }] + const healthy = [ + { id: 'close_mob', x: 140, y: 110 }, // dx=40, dy=10*2=20 -> distSq=1600+400=2000 <= 16384 (128px) + { id: 'far_mob', x: 300, y: 300 }, // far away + ] + + const spread = simulateRabiesContagion(infected, healthy, 128) + expect(spread).toEqual(['close_mob']) + }) + }) + + // ========================================================================= + // Scenario 5: Druid Summoning Hierarchy, Mutual Exclusivity & Coexistence + // ========================================================================= + describe('Scenario 5: Druid Summoning Hierarchy, Mutual Exclusivity, and Raven Coexistence', () => { + it('verifies complete group mutual exclusivity and raven coexistence (up to 8 pets)', () => { + let activePets: ActivePetRecord[] = [] + + // 1. Summon 5 Spirit Wolves (Group 1) + for (let i = 1; i <= 5; i++) { + activePets = resolvePetSummon(activePets, { + id: `sw_${i}`, + skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, + slvl: 5, + }).newActivePets + } + expect(activePets.length).toBe(5) + + // 2. Summon Grizzly (Group 1) -> must evict all 5 Spirit Wolves + const gzRes = resolvePetSummon(activePets, { + id: 'grizzly', + skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, + slvl: 1, + }) + expect(gzRes.evictedPetIds.length).toBe(5) + expect(gzRes.newActivePets.length).toBe(1) + activePets = gzRes.newActivePets + + // 3. Summon Oak Sage (Group 2) + activePets = resolvePetSummon(activePets, { + id: 'oak', + skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE, + slvl: 1, + }).newActivePets + expect(activePets.length).toBe(2) // 1 Bear + 1 Spirit + + // 4. Summon Heart of Wolverine (Group 2) -> must evict Oak Sage + const hwRes = resolvePetSummon(activePets, { + id: 'wolverine', + skillId: DRUID_SUMMON_SKILL_IDS.HEART_OF_WOLVERINE, + slvl: 1, + }) + expect(hwRes.evictedPetIds).toEqual(['oak']) + expect(hwRes.newActivePets.length).toBe(2) + activePets = hwRes.newActivePets + + // 5. Summon Carrion Vine (Group 3) + activePets = resolvePetSummon(activePets, { + id: 'vine_carrion', + skillId: DRUID_SUMMON_SKILL_IDS.CARRION_VINE, + slvl: 1, + }).newActivePets + expect(activePets.length).toBe(3) // 1 Bear + 1 Spirit + 1 Vine + + // 6. Summon 5 Ravens (Group 0) -> all 5 coexist with Bear, Spirit, and Vine! + for (let i = 1; i <= 5; i++) { + const res = resolvePetSummon(activePets, { + id: `raven_${i}`, + skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, + slvl: 5, + }) + expect(res.evictedPetIds).toEqual([]) + activePets = res.newActivePets + } + + // Total active pets = 1 Grizzly + 1 Wolverine + 1 Carrion Vine + 5 Ravens = 8 pets! + expect(activePets.length).toBe(8) + expect(activePets.filter(p => p.petGroup === DruidPetGroup.WOLF_BEAR).length).toBe(1) + expect(activePets.filter(p => p.petGroup === DruidPetGroup.SPIRIT).length).toBe(1) + expect(activePets.filter(p => p.petGroup === DruidPetGroup.VINE).length).toBe(1) + expect(activePets.filter(p => p.petGroup === DruidPetGroup.RAVEN).length).toBe(5) + }) + }) + + // ========================================================================= + // Scenario 6: Wolf & Grizzly Cross-Species Passive Synergy Network + // ========================================================================= + describe('Scenario 6: Wolf & Grizzly Cross-Species Passive Synergy Network', () => { + it('verifies mutual passive bonus distribution across all 3 species', () => { + // 20 points in Spirit Wolf (+525% AR & +240% Def) + // 20 points in Dire Wolf (+525% Life) + // 20 points in Grizzly (+215% Damage) + const swStats = calculateSpiritWolfStats(20, { direWolfBlvl: 20, grizzlyBlvl: 20 }) + const dwStats = calculateDireWolfStats(20, { spiritWolfBlvl: 20, grizzlyBlvl: 20 }) + const gzStats = calculateGrizzlyStats(20, { spiritWolfBlvl: 20, direWolfBlvl: 20 }) + + // Spirit Wolf receives +525% Life from Dire Wolf and +215% Damage from Grizzly + expect(swStats.passiveLifeBonusPct).toBe(525) + expect(swStats.passiveDmgBonusPct).toBe(215) + expect(swStats.minDamage).toBe(Math.trunc((41 * 315) / 100)) // 129 + expect(swStats.maxDamage).toBe(Math.trunc((45 * 315) / 100)) // 141 + + // Dire Wolf receives +525% AR & +240% Def from Spirit Wolf, +215% Damage from Grizzly + expect(dwStats.passiveArBonusPct).toBe(525) + expect(dwStats.passiveDefBonusPct).toBe(240) + expect(dwStats.passiveDmgBonusPct).toBe(215) + expect(dwStats.minDamage).toBe(Math.trunc((69 * 315) / 100)) // 217 + expect(dwStats.maxDamage).toBe(Math.trunc((74 * 315) / 100)) // 233 + + // Grizzly receives +525% Life from Dire Wolf and +525% AR & +240% Def from Spirit Wolf + expect(gzStats.passiveLifeBonusPct).toBe(525) + expect(gzStats.passiveArBonusPct).toBe(525) + expect(gzStats.passiveDefBonusPct).toBe(240) + expect(gzStats.smiteKnockback).toBe(true) + }) + }) + + // ========================================================================= + // Scenario 7: Vine Corpse Consumption & Dire Wolf Corpse Frenzy Cycles + // ========================================================================= + describe('Scenario 7: Vine Corpse Consumption & Dire Wolf Corpse Frenzy Cycles', () => { + it('verifies Carrion Vine heals life, Solar restores mana, and boss corpses are rejected', () => { + const owner = { maxHp: 2000, maxMana: 1000 } + + // Normal corpse: Carrion Vine slvl 20 heals 10% max HP = 200 HP + const normalCorpse = { consumed: false, isBoss: false } + const cRes = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 20, normalCorpse, owner) + expect(cRes.corpseConsumed).toBe(true) + expect(cRes.lifeHealed).toBe(200) + expect(normalCorpse.consumed).toBe(true) + + // Boss corpse: Rejected! + const bossCorpse = { consumed: false, isBoss: true } + const bRes = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 20, bossCorpse, owner) + expect(bRes.corpseConsumed).toBe(false) + expect(bRes.lifeHealed).toBe(0) + expect(bossCorpse.consumed).toBe(false) + + // Normal corpse: Solar Creeper slvl 20 restores 6% max Mana = 60 Mana + const manaCorpse = { consumed: false, isBoss: false } + const sRes = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER, 20, manaCorpse, owner) + expect(sRes.corpseConsumed).toBe(true) + expect(sRes.manaRestored).toBe(60) + }) + + it('verifies Dire Wolf corpse frenzy triggers 500-frame +100% damage rage', () => { + const corpse = { consumed: false, isBoss: false } + const frenzy = simulateDireWolfCorpseFrenzy(corpse) + expect(frenzy.consumed).toBe(true) + expect(frenzy.rageActivated).toBe(true) + expect(frenzy.rageDurationFrames).toBe(500) + expect(frenzy.damageMultiplier).toBe(2.0) + }) + }) + + // ========================================================================= + // Scenario 8: Complete 30-Skill Druid Tree Registry & Architectural Invariants + // ========================================================================= + describe('Scenario 8: Complete 30-Skill Druid Tree Registry & Architectural Invariants', () => { + it('verifies BATCH1_SKILLS length invariant = 38 is strictly preserved', () => { + const batch1Keys = Object.keys(BATCH1_SKILLS) + expect(batch1Keys.length).toBe(38) + }) + + it('verifies DataRegistry contains all 30 Druid skills (IDs 221–250) with exact 1.13c metadata', async () => { + const registry = await getSharedDataRegistry('samples/d2') + + const druidSkillIds = [ + // Summoning (10) + 221, 222, 226, 227, 231, 236, 237, 241, 246, 247, + // Shape Shifting (10) + 223, 224, 228, 232, 233, 238, 239, 242, 243, 248, + // Elemental (10) + 225, 229, 230, 234, 235, 240, 244, 245, 249, 250, + ] + + expect(druidSkillIds.length).toBe(30) + + for (const id of druidSkillIds) { + const skill = registry.getSkillById(id) + expect(skill).toBeDefined() + expect(skill!.id).toBe(id) + expect(skill!.charClass).toBe('dru') + } + }) + }) +})