diablo2-web/tests/skills/dru/adv-dru-e2e-gate.test.ts

396 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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