/** * 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') } }) }) })