697 lines
27 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|