feat(dru): Milestone M25 Druid Class Gate & End-to-End Suite across all 30 skills (closes #472)

This commit is contained in:
troytt 2026-09-25 03:11:06 +00:00
parent 853f61d634
commit 2e65415aed
1 changed files with 395 additions and 0 deletions

View File

@ -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')
}
})
})
})