diablo2-web/tests/skills/dru/adv-dru-summon-stress.test.ts

697 lines
27 KiB
TypeScript

/**
* Milestone M24 — Druid Summoning Skills Parity & Adversarial Stress Suite
*
* Exhaustive 4-dimension verification suite for the 10 Druid Summoning Skills:
* - Skill 221: Raven
* - Skill 222: Poison Creeper (Plague Poppy)
* - Skill 226: Oak Sage
* - Skill 227: Summon Spirit Wolf
* - Skill 231: Carrion Vine (Cycle of Life)
* - Skill 236: Heart of Wolverine
* - Skill 237: Summon Dire Wolf (Fenris)
* - Skill 241: Solar Creeper (Vines)
* - Skill 246: Spirit of Barbs
* - Skill 247: Summon Grizzly
*
* Verifies:
* - Dimension 1: Exact 1.13c numerical scaling, level progressions, and mana curves
* - Dimension 2: PetType.txt group mutual exclusivity, species eviction, and FIFO limits
* - Dimension 3: Kinematics, corpse consumption cycles, raven hit counters, and wolf/grizzly passive synergies
* - Dimension 4: Invariants (BATCH1_SKILLS=38), DataRegistry, and SkillModule integration
*/
import { describe, expect, it } from 'vitest'
import {
DRUID_SUMMON_SKILL_IDS,
DruidPetGroup,
calculateRavenStats,
calculatePoisonCreeperStats,
calculateOakSageStats,
calculateSpiritWolfStats,
calculateCarrionVineStats,
calculateHeartOfWolverineStats,
calculateDireWolfStats,
calculateSolarCreeperStats,
calculateSpiritOfBarbsStats,
calculateGrizzlyStats,
calculateDruidSummonSynergyProfile,
resolvePetSummon,
simulateRavenPeck,
simulateVineConsumption,
simulateDireWolfCorpseFrenzy,
type ActivePetRecord,
} from '../../../src/game/skills/druid-summon.ts'
import { BATCH1_SKILLS } from '../../../src/game/skills.ts'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { UnitStatList } from '../../../src/game/engine/stat-list.ts'
import { evaluateSkill113c, getRegisteredSkillModule } from '../../../src/game/skills/registry.ts'
describe('Milestone M24 — Druid Summoning Skills Parity & Adversarial Stress Suite', () => {
// =========================================================================
// Dimension 1: Exact 1.13c Numerical Scaling & Level Progressions
// =========================================================================
describe('Dimension 1: Exact 1.13c Numerical Scaling & Formulas', () => {
it('1.1 Raven (221): Evaluates hits scaling (11 + slvl), max ravens, and 6 mana', () => {
// slvl 1
const r1 = calculateRavenStats(1)
expect(r1.maxRavens).toBe(1)
expect(r1.hitsRemaining).toBe(12)
expect(r1.minDamage).toBe(2)
expect(r1.maxDamage).toBe(4)
expect(r1.attackRating).toBe(100)
expect(r1.manaCost).toBe(6)
expect(r1.manaCost256).toBe(1536)
// slvl 5
const r5 = calculateRavenStats(5)
expect(r5.maxRavens).toBe(5)
expect(r5.hitsRemaining).toBe(16)
expect(r5.minDamage).toBe(6)
expect(r5.maxDamage).toBe(8)
expect(r5.attackRating).toBe(160)
// slvl 10: max ravens capped at 5
const r10 = calculateRavenStats(10)
expect(r10.maxRavens).toBe(5)
expect(r10.hitsRemaining).toBe(21)
expect(r10.minDamage).toBe(11)
expect(r10.maxDamage).toBe(13)
expect(r10.attackRating).toBe(235)
// slvl 20
const r20 = calculateRavenStats(20)
expect(r20.maxRavens).toBe(5)
expect(r20.hitsRemaining).toBe(31)
expect(r20.minDamage).toBe(21)
expect(r20.maxDamage).toBe(23)
expect(r20.attackRating).toBe(385)
})
it('1.2 Poison Creeper (222): Evaluates HP scaling (+25%/lvl) and Rabies synergy', () => {
const pc1 = calculatePoisonCreeperStats(1, 0)
expect(pc1.maxPets).toBe(1)
expect(pc1.hpBonusPct).toBe(0)
expect(pc1.minPoisonDamage).toBe(4)
expect(pc1.maxPoisonDamage).toBe(6)
expect(pc1.manaCost).toBe(8)
expect(pc1.manaCost256).toBe(2048)
const pc10 = calculatePoisonCreeperStats(10, 0)
expect(pc10.hpBonusPct).toBe(225) // 25 * 9%
expect(pc10.minPoisonDamage).toBe(40)
expect(pc10.maxPoisonDamage).toBe(51)
// With 10 blvl in Rabies (+180% synergy)
const pc10Syn = calculatePoisonCreeperStats(10, 10)
expect(pc10Syn.synergyBonusPct).toBe(180)
expect(pc10Syn.minPoisonDamage).toBe(Math.trunc((40 * 280) / 100)) // 112
})
it('1.3 Oak Sage (226): Evaluates party Max Life% aura (25 + 5*slvl) and mana progression', () => {
// slvl 1: 30% life, radius 30 subtiles (20 yards), mana 15
const os1 = calculateOakSageStats(1)
expect(os1.maxPets).toBe(1)
expect(os1.lifeBonusPct).toBe(30)
expect(os1.radiusSubtiles).toBe(30)
expect(os1.radiusYards).toBe(20.0)
expect(os1.hpBonusPct).toBe(0)
expect(os1.manaCost).toBe(15)
expect(os1.manaCost256).toBe(3840)
// slvl 10: 75% life, radius 48 subtiles (32 yards), mana 24
const os10 = calculateOakSageStats(10)
expect(os10.lifeBonusPct).toBe(75)
expect(os10.radiusSubtiles).toBe(48)
expect(os10.radiusYards).toBe(32.0)
expect(os10.hpBonusPct).toBe(270) // 9 * 30%
expect(os10.manaCost).toBe(24)
expect(os10.manaCost256).toBe(6144)
// slvl 20: 125% life, radius 68 subtiles (45.3 yards), mana 34
const os20 = calculateOakSageStats(20)
expect(os20.lifeBonusPct).toBe(125)
expect(os20.radiusSubtiles).toBe(68)
expect(os20.radiusYards).toBe(45.3)
expect(os20.hpBonusPct).toBe(570)
expect(os20.manaCost).toBe(34)
expect(os20.manaCost256).toBe(8704)
})
it('1.4 Summon Spirit Wolf (227): Evaluates 5-band damage scaling and passive AR/Def bonuses', () => {
// slvl 1: 2-6 damage, passive +50% AR, +50% Def, max 1 wolf
const sw1 = calculateSpiritWolfStats(1)
expect(sw1.maxWolves).toBe(1)
expect(sw1.minDamage).toBe(2)
expect(sw1.maxDamage).toBe(6)
expect(sw1.passiveArBonusPct).toBe(50)
expect(sw1.passiveDefBonusPct).toBe(50)
expect(sw1.manaCost).toBe(15)
expect(sw1.coldDamagePct).toBe(50)
// slvl 5: max 5 wolves
const sw5 = calculateSpiritWolfStats(5)
expect(sw5.maxWolves).toBe(5)
// slvl 10: base: 2+7*1+2*2 = 13 min, 6+7*1+2*2 = 17 max
const sw10 = calculateSpiritWolfStats(10)
expect(sw10.maxWolves).toBe(5)
expect(sw10.minDamage).toBe(13)
expect(sw10.maxDamage).toBe(17)
expect(sw10.passiveArBonusPct).toBe(275) // 50 + 9*25
expect(sw10.passiveDefBonusPct).toBe(140) // 50 + 9*10
// slvl 20: base: 13+6*2+4*4 = 41 min, 17+6*2+4*4 = 45 max
const sw20 = calculateSpiritWolfStats(20)
expect(sw20.minDamage).toBe(41)
expect(sw20.maxDamage).toBe(45)
// With synergies: 10 hard points in Dire Wolf (+275% life) and 10 in Grizzly (+115% dmg)
const sw20Syn = calculateSpiritWolfStats(20, { direWolfBlvl: 10, grizzlyBlvl: 10 })
expect(sw20Syn.passiveLifeBonusPct).toBe(275)
expect(sw20Syn.passiveDmgBonusPct).toBe(115)
expect(sw20Syn.minDamage).toBe(Math.trunc((41 * 215) / 100)) // 88
expect(sw20Syn.maxDamage).toBe(Math.trunc((45 * 215) / 100)) // 96
})
it('1.5 Carrion Vine (231): Evaluates corpse heal % diminishing returns and fixed 10 mana', () => {
// slvl 1: heal 4% max HP (dm12 with 3, 12)
const cv1 = calculateCarrionVineStats(1)
expect(cv1.maxPets).toBe(1)
expect(cv1.healLifePct).toBe(4)
expect(cv1.searchRadiusSubtiles).toBe(10)
expect(cv1.manaCost).toBe(10)
expect(cv1.manaCost256).toBe(2560)
// slvl 10: diminishing returns between 3 and 12 -> 9%
const cv10 = calculateCarrionVineStats(10)
expect(cv10.healLifePct).toBe(9)
expect(cv10.hpBonusPct).toBe(225) // 9 * 25%
// slvl 20: 10%
const cv20 = calculateCarrionVineStats(20)
expect(cv20.healLifePct).toBe(10)
})
it('1.6 Heart of Wolverine (236): Evaluates party Enhanced Damage% and Attack Rating% aura', () => {
// slvl 1: +20% ED, +25% AR, radius 30 subtiles (20 yards), mana 20
const hw1 = calculateHeartOfWolverineStats(1)
expect(hw1.maxPets).toBe(1)
expect(hw1.damageBonusPct).toBe(20)
expect(hw1.attackRatingBonusPct).toBe(25)
expect(hw1.radiusSubtiles).toBe(30)
expect(hw1.manaCost).toBe(20)
expect(hw1.manaCost256).toBe(5120)
// slvl 10: 20 + 9*7 = 83% ED, 25 + 9*7 = 88% AR, radius 48 subtiles, mana 29
const hw10 = calculateHeartOfWolverineStats(10)
expect(hw10.damageBonusPct).toBe(83)
expect(hw10.attackRatingBonusPct).toBe(88)
expect(hw10.radiusSubtiles).toBe(48)
expect(hw10.manaCost).toBe(29)
expect(hw10.manaCost256).toBe(7424)
// slvl 20: 20 + 19*7 = 153% ED, 25 + 19*7 = 158% AR, radius 68 subtiles, mana 39
const hw20 = calculateHeartOfWolverineStats(20)
expect(hw20.damageBonusPct).toBe(153)
expect(hw20.attackRatingBonusPct).toBe(158)
expect(hw20.radiusSubtiles).toBe(68)
expect(hw20.manaCost).toBe(39)
})
it('1.7 Summon Dire Wolf (237): Evaluates 5-band damage scaling, passive Life% bonus, and frenzy', () => {
// slvl 1: 7-12 damage, +50% life bonus, max 1 wolf, 20 mana
const dw1 = calculateDireWolfStats(1)
expect(dw1.maxWolves).toBe(1)
expect(dw1.minDamage).toBe(7)
expect(dw1.maxDamage).toBe(12)
expect(dw1.passiveLifeBonusPct).toBe(50)
expect(dw1.corpseFrenzyDamageBonusPct).toBe(100)
expect(dw1.corpseFrenzyDurationFrames).toBe(500)
expect(dw1.manaCost).toBe(20)
expect(dw1.manaCost256).toBe(5120)
// slvl 3: max 3 wolves
const dw3 = calculateDireWolfStats(3)
expect(dw3.maxWolves).toBe(3)
// slvl 10: base: 7+7*2+2*3 = 27 min, 12+7*2+2*3 = 32 max
const dw10 = calculateDireWolfStats(10)
expect(dw10.maxWolves).toBe(3)
expect(dw10.minDamage).toBe(27)
expect(dw10.maxDamage).toBe(32)
expect(dw10.passiveLifeBonusPct).toBe(275) // 50 + 9*25
// slvl 20: base: 27+6*3+4*6 = 69 min, 32+6*3+4*6 = 74 max
const dw20 = calculateDireWolfStats(20)
expect(dw20.minDamage).toBe(69)
expect(dw20.maxDamage).toBe(74)
expect(dw20.passiveLifeBonusPct).toBe(525) // 50 + 19*25
// With Grizzly synergy (+115% damage)
const dw20Syn = calculateDireWolfStats(20, { grizzlyBlvl: 10 })
expect(dw20Syn.minDamage).toBe(Math.trunc((69 * 215) / 100)) // 148
expect(dw20Syn.maxDamage).toBe(Math.trunc((74 * 215) / 100)) // 159
})
it('1.8 Solar Creeper (241): Evaluates corpse mana % diminishing returns and mana scaling', () => {
// slvl 1: restore 2% max mana, mana 14 (dm12 with 1, 8)
const sc1 = calculateSolarCreeperStats(1)
expect(sc1.maxPets).toBe(1)
expect(sc1.restoreManaPct).toBe(2)
expect(sc1.searchRadiusSubtiles).toBe(10)
expect(sc1.manaCost).toBe(14)
expect(sc1.manaCost256).toBe(3584)
// slvl 10: 5% mana, mana 23
const sc10 = calculateSolarCreeperStats(10)
expect(sc10.restoreManaPct).toBe(5)
expect(sc10.hpBonusPct).toBe(180) // 9 * 20%
expect(sc10.manaCost).toBe(23)
expect(sc10.manaCost256).toBe(5888)
// slvl 20: 6% mana, mana 33
const sc20 = calculateSolarCreeperStats(20)
expect(sc20.restoreManaPct).toBe(6)
expect(sc20.manaCost).toBe(33)
})
it('1.9 Spirit of Barbs (246): Evaluates thorns reflection % scaling and mana progression', () => {
// slvl 1: 50% thorns, radius 30 subtiles (20 yards), mana 25
const sb1 = calculateSpiritOfBarbsStats(1)
expect(sb1.maxPets).toBe(1)
expect(sb1.thornsReturnPct).toBe(50)
expect(sb1.radiusSubtiles).toBe(30)
expect(sb1.radiusYards).toBe(20.0)
expect(sb1.manaCost).toBe(25)
expect(sb1.manaCost256).toBe(6400)
// slvl 10: 50 + 9*10 = 140% thorns, mana 34
const sb10 = calculateSpiritOfBarbsStats(10)
expect(sb10.thornsReturnPct).toBe(140)
expect(sb10.manaCost).toBe(34)
expect(sb10.manaCost256).toBe(8704)
// slvl 20: 50 + 19*10 = 240% thorns, mana 44
const sb20 = calculateSpiritOfBarbsStats(20)
expect(sb20.thornsReturnPct).toBe(240)
expect(sb20.manaCost).toBe(44)
})
it('1.10 Summon Grizzly (247): Evaluates apex solo pet damage, Smite stun, and passive synergy', () => {
// slvl 1: 30-60 damage, +25% passive damage, smite stun 15 frames, 40 mana
const gz1 = calculateGrizzlyStats(1)
expect(gz1.maxPets).toBe(1)
expect(gz1.minDamage).toBe(30)
expect(gz1.maxDamage).toBe(60)
expect(gz1.passiveDmgBonusPct).toBe(25)
expect(gz1.smiteStunFrames).toBe(15)
expect(gz1.smiteKnockback).toBe(true)
expect(gz1.manaCost).toBe(40)
expect(gz1.manaCost256).toBe(10240)
// slvl 10: base: 30+7*10+2*15 = 130 min, 60+7*10+2*15 = 160 max
const gz10 = calculateGrizzlyStats(10)
expect(gz10.minDamage).toBe(130)
expect(gz10.maxDamage).toBe(160)
expect(gz10.passiveDmgBonusPct).toBe(115) // 25 + 9*10
expect(gz10.smiteStunFrames).toBe(60) // 15 + 9*5
// slvl 20: base: 130+6*15+4*20 = 300 min, 160+6*15+4*20 = 330 max
const gz20 = calculateGrizzlyStats(20)
expect(gz20.minDamage).toBe(300)
expect(gz20.maxDamage).toBe(330)
expect(gz20.passiveDmgBonusPct).toBe(215) // 25 + 19*10
expect(gz20.smiteStunFrames).toBe(110) // 15 + 19*5
})
})
// =========================================================================
// Dimension 2: Pet Group Exclusivity & Slot Eviction Matrix
// =========================================================================
describe('Dimension 2: Pet Group Exclusivity & Slot Eviction Matrix (5)', () => {
it('2.1 Group 1 (Wolf & Bear): Grizzly and wolves mutually evict one another', () => {
let activePets: ActivePetRecord[] = []
// Summon 3 Spirit Wolves
for (let i = 1; i <= 3; i++) {
const res = resolvePetSummon(activePets, {
id: `wolf_${i}`,
skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF,
slvl: 5,
})
activePets = res.newActivePets
}
expect(activePets.length).toBe(3)
expect(activePets.every(p => p.petType === 'spiritwolf')).toBe(true)
// Summoning Grizzly MUST evict all 3 Spirit Wolves!
const gzRes = resolvePetSummon(activePets, {
id: 'grizzly_1',
skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY,
slvl: 1,
})
expect(gzRes.evictedPetIds).toEqual(['wolf_1', 'wolf_2', 'wolf_3'])
expect(gzRes.newActivePets.length).toBe(1)
expect(gzRes.newActivePets[0]!.petType).toBe('grizzly')
activePets = gzRes.newActivePets
// Summoning Dire Wolf MUST evict Grizzly!
const dwRes = resolvePetSummon(activePets, {
id: 'dire_1',
skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_DIRE_WOLF,
slvl: 1,
})
expect(dwRes.evictedPetIds).toEqual(['grizzly_1'])
expect(dwRes.newActivePets.length).toBe(1)
expect(dwRes.newActivePets[0]!.petType).toBe('fenris')
})
it('2.2 Group 2 (Spirit Totems): Only 1 spirit totem can be active at a time', () => {
let activePets: ActivePetRecord[] = []
// Summon Oak Sage
const r1 = resolvePetSummon(activePets, {
id: 'spirit_oak',
skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE,
slvl: 1,
})
activePets = r1.newActivePets
expect(activePets.length).toBe(1)
expect(activePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.OAK_SAGE)
// Summon Heart of Wolverine -> evicts Oak Sage
const r2 = resolvePetSummon(activePets, {
id: 'spirit_wolverine',
skillId: DRUID_SUMMON_SKILL_IDS.HEART_OF_WOLVERINE,
slvl: 1,
})
expect(r2.evictedPetIds).toEqual(['spirit_oak'])
expect(r2.newActivePets.length).toBe(1)
expect(r2.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.HEART_OF_WOLVERINE)
activePets = r2.newActivePets
// Summon Spirit of Barbs -> evicts Heart of Wolverine
const r3 = resolvePetSummon(activePets, {
id: 'spirit_barbs',
skillId: DRUID_SUMMON_SKILL_IDS.SPIRIT_OF_BARBS,
slvl: 1,
})
expect(r3.evictedPetIds).toEqual(['spirit_wolverine'])
expect(r3.newActivePets.length).toBe(1)
expect(r3.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.SPIRIT_OF_BARBS)
})
it('2.3 Group 3 (Vines): Only 1 vine creature can be active at a time', () => {
let activePets: ActivePetRecord[] = []
// Summon Poison Creeper
const r1 = resolvePetSummon(activePets, {
id: 'vine_poison',
skillId: DRUID_SUMMON_SKILL_IDS.POISON_CREEPER,
slvl: 1,
})
activePets = r1.newActivePets
expect(activePets.length).toBe(1)
// Summon Carrion Vine -> evicts Poison Creeper
const r2 = resolvePetSummon(activePets, {
id: 'vine_carrion',
skillId: DRUID_SUMMON_SKILL_IDS.CARRION_VINE,
slvl: 1,
})
expect(r2.evictedPetIds).toEqual(['vine_poison'])
expect(r2.newActivePets.length).toBe(1)
expect(r2.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.CARRION_VINE)
activePets = r2.newActivePets
// Summon Solar Creeper -> evicts Carrion Vine
const r3 = resolvePetSummon(activePets, {
id: 'vine_solar',
skillId: DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER,
slvl: 1,
})
expect(r3.evictedPetIds).toEqual(['vine_carrion'])
expect(r3.newActivePets.length).toBe(1)
expect(r3.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER)
})
it('2.4 Group 0 (Ravens): Ravens can coexist with wolf/bear, spirit, and vine simultaneously', () => {
let activePets: ActivePetRecord[] = []
// Summon Grizzly (Group 1)
activePets = resolvePetSummon(activePets, {
id: 'bear',
skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY,
slvl: 1,
}).newActivePets
// Summon Oak Sage (Group 2)
activePets = resolvePetSummon(activePets, {
id: 'spirit',
skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE,
slvl: 1,
}).newActivePets
// Summon Carrion Vine (Group 3)
activePets = resolvePetSummon(activePets, {
id: 'vine',
skillId: DRUID_SUMMON_SKILL_IDS.CARRION_VINE,
slvl: 1,
}).newActivePets
// Summon 5 Ravens (Group 0)
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([]) // None evicted!
activePets = res.newActivePets
}
// Total active pets = 1 Grizzly + 1 Spirit + 1 Vine + 5 Ravens = 8 pets!
expect(activePets.length).toBe(8)
expect(activePets.filter(p => p.petGroup === DruidPetGroup.RAVEN).length).toBe(5)
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)
})
it('2.5 FIFO Eviction: Enforces species caps and replaces oldest pet', () => {
let activePets: ActivePetRecord[] = []
// Summon 5 Spirit Wolves
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)
// Summon 6th Spirit Wolf -> evicts sw_1 (FIFO)
const res6 = resolvePetSummon(activePets, {
id: 'sw_6',
skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF,
slvl: 5,
})
expect(res6.evictedPetIds).toEqual(['sw_1'])
expect(res6.newActivePets.length).toBe(5)
expect(res6.newActivePets.map(p => p.id)).toEqual(['sw_2', 'sw_3', 'sw_4', 'sw_5', 'sw_6'])
})
})
// =========================================================================
// Dimension 3: Kinematics, Corpse Cycles & Combat Synergies
// =========================================================================
describe('Dimension 3: Kinematics, Corpse Cycles & Combat Synergies (4)', () => {
it('3.1 Raven peck: Decrements hits counter, inflicts blind, and departs at 0 hits', () => {
let hits = 3
const target = { isImmuneToCurse: false }
// Peck 1
const p1 = simulateRavenPeck(hits, target)
expect(p1.hitsRemainingAfter).toBe(2)
expect(p1.blindInflicted).toBe(true)
expect(p1.dismissed).toBe(false)
hits = p1.hitsRemainingAfter
// Peck 2
const p2 = simulateRavenPeck(hits, target)
expect(p2.hitsRemainingAfter).toBe(1)
expect(p2.dismissed).toBe(false)
hits = p2.hitsRemainingAfter
// Peck 3 (Final)
const p3 = simulateRavenPeck(hits, target)
expect(p3.hitsRemainingAfter).toBe(0)
expect(p3.dismissed).toBe(true)
// Curse immune target (Boss)
const bossTarget = { isImmuneToCurse: true }
const pBoss = simulateRavenPeck(5, bossTarget)
expect(pBoss.blindInflicted).toBe(false)
})
it('3.2 Vine corpse consumption: Carrion heals life, Solar restores mana, ignores consumed/boss corpses', () => {
const owner = { maxHp: 1000, maxMana: 500 }
// Carrion Vine slvl 10 (9% max HP = 90 HP)
const corpse1 = { consumed: false, isBoss: false }
const cRes = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, corpse1, owner)
expect(cRes.corpseConsumed).toBe(true)
expect(cRes.lifeHealed).toBe(90)
expect(cRes.manaRestored).toBe(0)
expect(corpse1.consumed).toBe(true)
// Trying to consume already consumed corpse fails
const cRes2 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, corpse1, owner)
expect(cRes2.corpseConsumed).toBe(false)
expect(cRes2.lifeHealed).toBe(0)
// Solar Creeper slvl 10 (5% max Mana = 25 Mana)
const corpse2 = { consumed: false, isBoss: false }
const sRes = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER, 10, corpse2, owner)
expect(sRes.corpseConsumed).toBe(true)
expect(sRes.manaRestored).toBe(25)
expect(sRes.lifeHealed).toBe(0)
// Boss corpses cannot be eaten by vines
const bossCorpse = { consumed: false, isBoss: true }
const bRes = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, bossCorpse, owner)
expect(bRes.corpseConsumed).toBe(false)
expect(bossCorpse.consumed).toBe(false)
})
it('3.3 Dire Wolf corpse frenzy: Consumes combat corpse to activate 500-frame +100% damage rage', () => {
const corpse = { consumed: false, isBoss: false }
const out = simulateDireWolfCorpseFrenzy(corpse)
expect(out.consumed).toBe(true)
expect(out.rageActivated).toBe(true)
expect(out.rageDurationFrames).toBe(500)
expect(out.damageMultiplier).toBe(2.0) // +100%
expect(corpse.consumed).toBe(true)
// Already consumed corpse cannot re-trigger frenzy
const out2 = simulateDireWolfCorpseFrenzy(corpse)
expect(out2.consumed).toBe(false)
expect(out2.rageActivated).toBe(false)
})
it('3.4 Wolf & Grizzly cross-species synergy network correctly calculates mutual bonuses', () => {
const syn = calculateDruidSummonSynergyProfile({
spiritWolf: 10, // +275% AR & Def
direWolf: 10, // +275% Life
grizzly: 10, // +115% Damage
})
expect(syn.spiritWolfArDefBonusPct).toBe(275)
expect(syn.direWolfLifeBonusPct).toBe(275)
expect(syn.grizzlyDmgBonusPct).toBe(115)
})
})
// =========================================================================
// Dimension 4: Invariants, DataRegistry & SkillModule Architecture
// =========================================================================
describe('Dimension 4: Invariants, DataRegistry & SkillModule Architecture (4)', () => {
it('4.1 BATCH1_SKILLS length invariant = 38 is strictly preserved', () => {
const batch1Keys = Object.keys(BATCH1_SKILLS)
expect(batch1Keys.length).toBe(38)
})
it('4.2 All 10 Druid Summoning skills are properly defined in SkillModule architecture', async () => {
const summonIds = [221, 222, 226, 227, 231, 236, 237, 241, 246, 247]
const { skillModule: m221 } = await import('../../../src/game/skills/impl/dru/skill-221-raven.ts')
const { skillModule: m222 } = await import('../../../src/game/skills/impl/dru/skill-222-plague-poppy.ts')
const { skillModule: m226 } = await import('../../../src/game/skills/impl/dru/skill-226-oak-sage.ts')
const { skillModule: m227 } = await import('../../../src/game/skills/impl/dru/skill-227-summon-spirit-wolf.ts')
const { skillModule: m231 } = await import('../../../src/game/skills/impl/dru/skill-231-cycle-of-life.ts')
const { skillModule: m236 } = await import('../../../src/game/skills/impl/dru/skill-236-heart-of-wolverine.ts')
const { skillModule: m237 } = await import('../../../src/game/skills/impl/dru/skill-237-summon-fenris.ts')
const { skillModule: m241 } = await import('../../../src/game/skills/impl/dru/skill-241-vines.ts')
const { skillModule: m246 } = await import('../../../src/game/skills/impl/dru/skill-246-spirit-of-barbs.ts')
const { skillModule: m247 } = await import('../../../src/game/skills/impl/dru/skill-247-summon-grizzly.ts')
const modules = [m221, m222, m226, m227, m231, m236, m237, m241, m246, m247]
for (let i = 0; i < summonIds.length; i++) {
const id = summonIds[i]
const mod = modules[i]
expect(mod).toBeDefined()
expect(mod.skillId).toBe(id)
expect(mod.charClass).toBe('dru')
}
})
it('4.3 DataRegistry loads all 10 Druid Summoning skills from Patch_D2.mpq with exact attributes', async () => {
const registry = await getSharedDataRegistry()
const druSummonIds = [221, 222, 226, 227, 231, 236, 237, 241, 246, 247]
for (const id of druSummonIds) {
const rec = registry.getSkillById(id)
expect(rec).toBeDefined()
expect(rec!.charClass).toBe('dru')
expect(rec!.summon).toBeTruthy()
}
})
it('4.4 evaluateSkill113c evaluates all 10 Druid Summoning skills against UnitStatList with exact attributes', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList()
// Raven (221)
const rRaven = evaluateSkill113c({
registry,
skill: registry.getSkillById(221)!,
slvl: 10,
blvl: 10,
statList: stats,
})
expect(rRaven.manaCost).toBe(6)
expect(rRaven.minPhysDmg).toBe(11)
expect(rRaven.maxPhysDmg).toBe(13)
expect(rRaven.petMax).toBe(5)
// Oak Sage (226)
const rOak = evaluateSkill113c({
registry,
skill: registry.getSkillById(226)!,
slvl: 10,
blvl: 10,
statList: stats,
})
expect(rOak.manaCost).toBe(24)
expect(rOak.radiusSubtiles).toBe(75)
expect(rOak.petMax).toBe(1)
// Spirit Wolf (227)
const rSW = evaluateSkill113c({
registry,
skill: registry.getSkillById(227)!,
slvl: 10,
blvl: 10,
statList: stats,
})
expect(rSW.manaCost).toBe(15)
expect(rSW.minPhysDmg).toBe(13)
expect(rSW.maxPhysDmg).toBe(17)
expect(rSW.petMax).toBe(5)
// Grizzly (247)
const rGz = evaluateSkill113c({
registry,
skill: registry.getSkillById(247)!,
slvl: 10,
blvl: 10,
statList: stats,
})
expect(rGz.manaCost).toBe(40)
expect(rGz.minPhysDmg).toBe(130)
expect(rGz.maxPhysDmg).toBe(160)
expect(rGz.petMax).toBe(1)
})
})
})