/** * Milestone M24 / Cohort 6 — Druid Summoning Adversarial Stress Testing Suite 2 * * Empirical challenger verification suite probing: * 1. Pet Exclusivity Groups (Spirits, Wolves/Bear, Vines, Ravens) * 2. Raven Hit Count Lifecycle & Despawn * 3. Wolf & Bear Cross-Species Passive Synergies & AR/Def Divergence * 4. Vine Corpse Consumption, Healing, and Shattered/Non-Corpse Rejection * 5. Spirit Aura Dissemination to Allies, Mercenaries & Minions */ 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, isVineCorpseSkill, simulateDireWolfCorpseFrenzy, type ActivePetRecord, } from '../../../src/server/skills/druid-summon.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { UnitStatList, FIXED_ONE } from '../../../src/server/engine/stat-list.ts' import { StateBus } from '../../../src/server/engine/state-bus.ts' import { SummonManager } from '../../../src/server/ai/summon-manager.ts' import { AuraScanner } from '../../../src/server/engine/aura-scanner.ts' import { MissileEngine } from '../../../src/server/engine/missile-engine.ts' import { WorldArena } from '../../../src/server/engine/world-arena.ts' import type { CombatUnitContext } from '../../../src/server/engine/combat-pipeline.ts' describe('Cohort 6: Druid Summoning Pet Caps, Synergies & Corpses — Adversarial Challenge Suite', () => { // ========================================================================= // 1. Pet Exclusivity Groups & Caps // ========================================================================= describe('1. Pet Exclusivity Groups & Caps', () => { it('1.1 Spirits: Heart of Wolverine replaces active Oak Sage or Spirit of Barbs (max 1)', () => { let pets: ActivePetRecord[] = [] // Summon Oak Sage const rOak = resolvePetSummon(pets, { id: 'oak_1', skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE, slvl: 10, }) pets = rOak.newActivePets expect(pets.length).toBe(1) expect(pets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.OAK_SAGE) // Summon Heart of Wolverine -> evicts Oak Sage const rWolverine = resolvePetSummon(pets, { id: 'wolverine_1', skillId: DRUID_SUMMON_SKILL_IDS.HEART_OF_WOLVERINE, slvl: 10, }) expect(rWolverine.evictedPetIds).toEqual(['oak_1']) expect(rWolverine.newActivePets.length).toBe(1) expect(rWolverine.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.HEART_OF_WOLVERINE) pets = rWolverine.newActivePets // Summon Spirit of Barbs -> evicts Heart of Wolverine const rBarbs = resolvePetSummon(pets, { id: 'barbs_1', skillId: DRUID_SUMMON_SKILL_IDS.SPIRIT_OF_BARBS, slvl: 10, }) expect(rBarbs.evictedPetIds).toEqual(['wolverine_1']) expect(rBarbs.newActivePets.length).toBe(1) expect(rBarbs.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.SPIRIT_OF_BARBS) }) it('1.2 Wolves & Bear: Summon Grizzly evicts Spirit Wolves and Dire Wolves; Spirit Wolves evict Grizzly', () => { let pets: ActivePetRecord[] = [] // Summon 5 Spirit Wolves for (let i = 1; i <= 5; i++) { pets = resolvePetSummon(pets, { id: `sw_${i}`, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, slvl: 5, }).newActivePets } expect(pets.length).toBe(5) // Summon Grizzly -> evicts all 5 Spirit Wolves const rGz = resolvePetSummon(pets, { id: 'bear_1', skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, slvl: 5, }) expect(rGz.evictedPetIds).toEqual(['sw_1', 'sw_2', 'sw_3', 'sw_4', 'sw_5']) expect(rGz.newActivePets.length).toBe(1) expect(rGz.newActivePets[0]!.petType).toBe('grizzly') pets = rGz.newActivePets // Summon Dire Wolf -> evicts Grizzly const rDW = resolvePetSummon(pets, { id: 'dw_1', skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_DIRE_WOLF, slvl: 5, }) expect(rDW.evictedPetIds).toEqual(['bear_1']) expect(rDW.newActivePets.length).toBe(1) expect(rDW.newActivePets[0]!.petType).toBe('fenris') pets = rDW.newActivePets // Summon Spirit Wolf -> evicts Dire Wolf const rSW = resolvePetSummon(pets, { id: 'sw_new', skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, slvl: 5, }) expect(rSW.evictedPetIds).toEqual(['dw_1']) expect(rSW.newActivePets.length).toBe(1) expect(rSW.newActivePets[0]!.petType).toBe('spiritwolf') }) it('1.3 Vines: Carrion Vine replaces Poison Creeper or Solar Creeper (max 1)', () => { let pets: ActivePetRecord[] = [] // Summon Poison Creeper pets = resolvePetSummon(pets, { id: 'pc_1', skillId: DRUID_SUMMON_SKILL_IDS.POISON_CREEPER, slvl: 1, }).newActivePets expect(pets.length).toBe(1) // Summon Carrion Vine -> evicts Poison Creeper const rCV = resolvePetSummon(pets, { id: 'cv_1', skillId: DRUID_SUMMON_SKILL_IDS.CARRION_VINE, slvl: 1, }) expect(rCV.evictedPetIds).toEqual(['pc_1']) expect(rCV.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.CARRION_VINE) pets = rCV.newActivePets // Summon Solar Creeper -> evicts Carrion Vine const rSC = resolvePetSummon(pets, { id: 'sc_1', skillId: DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER, slvl: 1, }) expect(rSC.evictedPetIds).toEqual(['cv_1']) expect(rSC.newActivePets[0]!.skillId).toBe(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER) }) it('1.4 Ravens: Up to 5 ravens coexist alongside active spirit, wolves/bear, and vine (total 8 pets)', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } // 1. Bear sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, slvl: 1 }) // 2. Spirit sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE, slvl: 1 }) // 3. Vine sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.CARRION_VINE, slvl: 1 }) // 4. 5 Ravens for (let i = 1; i <= 5; i++) { const out = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 5 }) expect(out.evictedPetUids).toEqual([]) } const active = sm.getActivePets() expect(active.length).toBe(8) expect(active.filter(p => p.petType === 'grizzly').length).toBe(1) expect(active.filter(p => p.petType === 'totem').length).toBe(1) expect(active.filter(p => p.petType === 'vine').length).toBe(1) expect(active.filter(p => p.petType === 'raven').length).toBe(5) }) }) // ========================================================================= // 2. Raven Hit Count Lifecycle // ========================================================================= describe('2. Raven Hit Count Lifecycle', () => { it('2.1 Pure formula: hits remaining equals 11 + slvl per 1.13c ground truth', () => { expect(calculateRavenStats(1).hitsRemaining).toBe(12) // 11 + 1 expect(calculateRavenStats(5).hitsRemaining).toBe(16) // 11 + 5 expect(calculateRavenStats(10).hitsRemaining).toBe(21) // 11 + 10 expect(calculateRavenStats(20).hitsRemaining).toBe(31) // 11 + 20 }) it('2.2 simulateRavenPeck: decrements hits and despawns upon reaching 0', () => { let hits = 2 const target = { isImmuneToCurse: false } const peck1 = simulateRavenPeck(hits, target) expect(peck1.hitsRemainingAfter).toBe(1) expect(peck1.blindInflicted).toBe(true) expect(peck1.dismissed).toBe(false) const peck2 = simulateRavenPeck(peck1.hitsRemainingAfter, target) expect(peck2.hitsRemainingAfter).toBe(0) expect(peck2.dismissed).toBe(true) // Despawns! }) it('2.3 Runtime SummonManager initializes Raven shotsRemaining to 11 + slvl', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } const out = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 1 }) const raven = out.pet! // In 1.13c, Raven slvl 1 must have 12 hits remaining (11 + 1). expect(raven.shotsRemaining).toBe(12) }) it('2.4 Runtime SummonManager.tickPets decrements Raven hit count on attack and despawns upon exhaustion', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } const enemyStats = new UnitStatList(reg, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE }) const enemyBus = new StateBus(enemyStats, reg) const enemy: CombatUnitContext = { id: 'dummy', name: 'Target_Dummy', statList: enemyStats, stateBus: enemyBus } const enemyPositions = new Map([['dummy', { x: 0, y: 100 }]]) const mEngine = new MissileEngine(reg) const aScanner = new AuraScanner(reg) const out = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 1 }) const raven = out.pet! // Step 50 ticks (5 melee attacks at every 10th tick: 12 - 5 = 7 hits remaining) for (let tick = 1; tick <= 50; tick++) { sm.tickPets({ currentTick: tick, owner, enemies: [enemy], enemyPositions, missileEngine: mEngine, auraScanner: aScanner, }) } expect(raven.shotsRemaining).toBe(7) expect(sm.getActivePets().length).toBe(1) // Step 70 more ticks (7 more attacks: 7 - 7 = 0 hits remaining -> despawned) for (let tick = 51; tick <= 120; tick++) { sm.tickPets({ currentTick: tick, owner, enemies: [enemy], enemyPositions, missileEngine: mEngine, auraScanner: aScanner, }) } expect(sm.getActivePets().length).toBe(0) }) it('2.5 Multi-Raven concurrent attrition: independent hit pools and sequential dismissal', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } const enemyStats = new UnitStatList(reg, { maxhp: 50000 * FIXED_ONE, hitpoints: 50000 * FIXED_ONE }) const enemyBus = new StateBus(enemyStats, reg) const enemy: CombatUnitContext = { id: 'dummy', name: 'Target_Dummy', statList: enemyStats, stateBus: enemyBus } const enemyPositions = new Map([['dummy', { x: 0, y: 100 }]]) const mEngine = new MissileEngine(reg) const aScanner = new AuraScanner(reg) // Summon 3 Ravens at different slvls: // Raven 1 (slvl 1): 11 + 1 = 12 hits // Raven 2 (slvl 5): 11 + 5 = 16 hits // Raven 3 (slvl 10): 11 + 10 = 21 hits const r1 = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 1 }).pet! const r2 = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 5 }).pet! const r3 = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 10 }).pet! expect(r1.shotsRemaining).toBe(12) expect(r2.shotsRemaining).toBe(16) expect(r3.shotsRemaining).toBe(21) expect(sm.getActivePets().length).toBe(3) // Step 120 ticks: 12 attack opportunities (ticks 10, 20, ..., 120) for (let tick = 1; tick <= 120; tick++) { sm.tickPets({ currentTick: tick, owner, enemies: [enemy], enemyPositions, missileEngine: mEngine, auraScanner: aScanner, }) } // Raven 1 (12 hits) is exhausted and dismissed! // Raven 2 (16 - 12 = 4 hits left) // Raven 3 (21 - 12 = 9 hits left) expect(r2.shotsRemaining).toBe(4) expect(r3.shotsRemaining).toBe(9) expect(sm.getActivePets().length).toBe(2) expect(sm.getActivePets().some(p => p.uid === r1.uid)).toBe(false) // Step 40 more ticks (ticks 121..160: 4 more attacks) for (let tick = 121; tick <= 160; tick++) { sm.tickPets({ currentTick: tick, owner, enemies: [enemy], enemyPositions, missileEngine: mEngine, auraScanner: aScanner, }) } // Raven 2 is exhausted and dismissed! // Raven 3 (9 - 4 = 5 hits left) expect(r3.shotsRemaining).toBe(5) expect(sm.getActivePets().length).toBe(1) expect(sm.getActivePets().some(p => p.uid === r2.uid)).toBe(false) // Step 50 more ticks (ticks 161..210: 5 more attacks) for (let tick = 161; tick <= 210; tick++) { sm.tickPets({ currentTick: tick, owner, enemies: [enemy], enemyPositions, missileEngine: mEngine, auraScanner: aScanner, }) } // All ravens now exhausted and dismissed! expect(sm.getActivePets().length).toBe(0) }) it('2.6 Raven idling invariant: shotsRemaining does not decrement without combat targets', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } const mEngine = new MissileEngine(reg) const aScanner = new AuraScanner(reg) const out = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl: 7 }) const raven = out.pet! expect(raven.shotsRemaining).toBe(18) // 11 + 7 // Tick 200 times with no enemies present for (let tick = 1; tick <= 200; tick++) { sm.tickPets({ currentTick: tick, owner, enemies: [], enemyPositions: new Map(), missileEngine: mEngine, auraScanner: aScanner, }) } // Shots remaining remains perfectly intact expect(raven.shotsRemaining).toBe(18) expect(sm.getActivePets().length).toBe(1) }) it('2.7 Raven parametric sweep across slvl 1..99 matches 11 + slvl', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } for (let slvl = 1; slvl <= 99; slvl++) { const pure = calculateRavenStats(slvl) expect(pure.hitsRemaining).toBe(11 + slvl) const out = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.RAVEN, slvl }) expect(out.pet!.shotsRemaining).toBe(11 + slvl) } }) }) // ========================================================================= // 3. Wolf & Bear Cross-Species Passive Synergies // ========================================================================= describe('3. Wolf & Bear Cross-Species Passive Synergies', () => { it('3.1 Formulas: Spirit Wolf gives AR (+25%/lvl) and Defense (+10%/lvl), Dire Wolf gives Life (+25%/lvl), Grizzly gives Damage (+10%/lvl)', () => { // Spirit Wolf slvl 10: +275% AR (50 + 9*25), +140% Def (50 + 9*10) const sw10 = calculateSpiritWolfStats(10) expect(sw10.passiveArBonusPct).toBe(275) expect(sw10.passiveDefBonusPct).toBe(140) // Dire Wolf slvl 10: +275% Life (50 + 9*25) const dw10 = calculateDireWolfStats(10) expect(dw10.passiveLifeBonusPct).toBe(275) // Grizzly slvl 10: +115% Damage (25 + 9*10) const gz10 = calculateGrizzlyStats(10) expect(gz10.passiveDmgBonusPct).toBe(115) // Cross-synergy wiring into Dire Wolf: const dwWithSyn = calculateDireWolfStats(10, { spiritWolfBlvl: 10, grizzlyBlvl: 10 }) expect(dwWithSyn.passiveArBonusPct).toBe(275) expect(dwWithSyn.passiveDefBonusPct).toBe(140) expect(dwWithSyn.passiveDmgBonusPct).toBe(115) // Cross-synergy wiring into Grizzly: const gzWithSyn = calculateGrizzlyStats(10, { spiritWolfBlvl: 10, direWolfBlvl: 10 }) expect(gzWithSyn.passiveArBonusPct).toBe(275) expect(gzWithSyn.passiveDefBonusPct).toBe(140) expect(gzWithSyn.passiveLifeBonusPct).toBe(275) }) it('3.2 calculateDruidSummonSynergyProfile provides separated AR and Defense bonuses', () => { const syn = calculateDruidSummonSynergyProfile({ spiritWolf: 10, direWolf: 10, grizzly: 10, }) expect(syn.spiritWolfArBonusPct).toBe(275) expect(syn.spiritWolfDefBonusPct).toBe(140) expect(syn.direWolfLifeBonusPct).toBe(275) expect(syn.grizzlyDmgBonusPct).toBe(115) }) it('3.3 Runtime SummonManager.createPet applies cross-species synergies to summoned pets', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) // Player has 20 hard points in all 3 skills stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, 20) stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_DIRE_WOLF, 20) stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, 20) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } const resGz = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, slvl: 20 }) const gz = resGz.pet! // In 1.13c, Grizzly at slvl 20 with 20 Spirit Wolf & 20 Dire Wolf: // - Spirit Wolf synergy: +525% AR (50 + 19*25), +240% Def (50 + 19*10) // - Grizzly innate passive: +215% Damage (25 + 19*10) expect(gz.statList.getAccruedStat('item_armor_percent')).toBe(240) expect(gz.statList.getAccruedStat('item_tohit_percent')).toBe(525) expect(gz.statList.getAccruedStat('damagepercent')).toBe(215) }) it('3.4 Full differential matrix sweep (sw 0..30, dw 0..30, gz 0..30) verifying AR and Def divergence', () => { for (let sw = 0; sw <= 30; sw++) { const expectedAr = sw > 0 ? 50 + (sw - 1) * 25 : 0 const expectedDef = sw > 0 ? 50 + (sw - 1) * 10 : 0 const syn = calculateDruidSummonSynergyProfile({ spiritWolf: sw, direWolf: sw, grizzly: sw, }) expect(syn.spiritWolfArBonusPct).toBe(expectedAr) expect(syn.spiritWolfDefBonusPct).toBe(expectedDef) if (sw >= 2) { // Verify that AR and Def bonuses strictly diverge (AR scaling is +25%/lvl, Def is +10%/lvl) expect(syn.spiritWolfArBonusPct).toBeGreaterThan(syn.spiritWolfDefBonusPct) } } }) it('3.5 Hard points vs soft points (+skills) isolation in SummonManager cross-species passives', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) // Set base skill level (hard points) = 0, but simulate +15 all skills on gear stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, 0) stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_DIRE_WOLF, 0) stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, 0) // Gear bonus stats.addStat('allskills', 15) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } // Grizzly summoned at slvl 20 (with 0 hard points in Spirit Wolf and Dire Wolf): const resGz = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, slvl: 20 }) const gz = resGz.pet! // Because Spirit Wolf hard points = 0, Grizzly receives ZERO AR% and ZERO Def% synergy bonus: expect(gz.statList.getAccruedStat('item_armor_percent')).toBe(0) expect(gz.statList.getAccruedStat('item_tohit_percent')).toBe(0) // Only Grizzly's own innate slvl 20 passive damage is active (25 + 19*10 = 215%) expect(gz.statList.getAccruedStat('damagepercent')).toBe(215) }) it('3.6 Cross-species synergies populated across all 3 species (Spirit Wolf, Dire Wolf, Grizzly)', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) // Allocate hard points: SW = 12, DW = 8, GZ = 6 stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, 12) stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_DIRE_WOLF, 8) stats.setBaseSkillLevel(DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, 6) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } // SW synergies: AR = 50 + 11*25 = 325%, Def = 50 + 11*10 = 160% // GZ hard points = 6 -> Dmg = 25 + 5*10 = 75% // DW hard points = 8 -> Life = 50 + 7*25 = 225% // 1. Test Spirit Wolf (gets own AR 325%, Def 160%, GZ Dmg 75%, DW Life 225%) const pSW = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, slvl: 12 }).pet! expect(pSW.statList.getAccruedStat('item_tohit_percent')).toBe(325) expect(pSW.statList.getAccruedStat('item_armor_percent')).toBe(160) expect(pSW.statList.getAccruedStat('damagepercent')).toBe(75) // 2. Test Dire Wolf (evicts Spirit Wolf, gets SW AR 325%, Def 160%, GZ Dmg 75%, own Life 225%) const pDW = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_DIRE_WOLF, slvl: 8 }).pet! expect(pDW.statList.getAccruedStat('item_tohit_percent')).toBe(325) expect(pDW.statList.getAccruedStat('item_armor_percent')).toBe(160) expect(pDW.statList.getAccruedStat('damagepercent')).toBe(75) // 3. Test Grizzly (evicts Dire Wolf, gets SW AR 325%, Def 160%, own Dmg 75%, DW Life 225%) const pGZ = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, slvl: 6 }).pet! expect(pGZ.statList.getAccruedStat('item_tohit_percent')).toBe(325) expect(pGZ.statList.getAccruedStat('item_armor_percent')).toBe(160) expect(pGZ.statList.getAccruedStat('damagepercent')).toBe(75) }) }) // ========================================================================= // 4. Vine Corpse Consumption & Healing // ========================================================================= describe('4. Vine Corpse Consumption & Healing', () => { it('4.1 Carrion Vine consumes corpse to heal caster % Max Life', () => { const owner = { maxHp: 1000, maxMana: 500 } const corpse = { consumed: false, isBoss: false } // Carrion Vine slvl 1: 4% max HP (40 HP) const res1 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 1, corpse, owner) expect(res1.corpseConsumed).toBe(true) expect(res1.lifeHealed).toBe(40) expect(corpse.consumed).toBe(true) // Carrion Vine slvl 10: 9% max HP (90 HP) const corpse10 = { consumed: false } const res10 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, corpse10, owner) expect(res10.lifeHealed).toBe(90) // Carrion Vine slvl 20: 10% max HP (100 HP) const corpse20 = { consumed: false } const res20 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 20, corpse20, owner) expect(res20.lifeHealed).toBe(100) }) it('4.2 Solar Creeper consumes corpse to restore caster % Max Mana', () => { const owner = { maxHp: 1000, maxMana: 600 } const corpse = { consumed: false } // Solar Creeper slvl 1: 2% max Mana (12 Mana) const res1 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER, 1, corpse, owner) expect(res1.corpseConsumed).toBe(true) expect(res1.manaRestored).toBe(12) expect(corpse.consumed).toBe(true) // Solar Creeper slvl 10: 5% max Mana (30 Mana) const corpse10 = { consumed: false } const res10 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER, 10, corpse10, owner) expect(res10.manaRestored).toBe(30) // Solar Creeper slvl 20: 6% max Mana (36 Mana) const corpse20 = { consumed: false } const res20 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.SOLAR_CREEPER, 20, corpse20, owner) expect(res20.manaRestored).toBe(36) }) it('4.3 Rejection: Already consumed corpses and Act Boss corpses are rejected', () => { const owner = { maxHp: 1000, maxMana: 500 } // Already consumed corpse const cConsumed = { consumed: true, isBoss: false } const r1 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, cConsumed, owner) expect(r1.corpseConsumed).toBe(false) expect(r1.lifeHealed).toBe(0) // Boss corpse const cBoss = { consumed: false, isBoss: true } const r2 = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, cBoss, owner) expect(r2.corpseConsumed).toBe(false) expect(cBoss.consumed).toBe(false) }) it('4.4 simulateVineConsumption safely rejects shattered (cold/freeze death) corpses', () => { const owner = { maxHp: 1000, maxMana: 500 } const cShattered = { consumed: false, shattered: true } as any const res = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, cShattered, owner) // In 1.13c, monsters that shatter into ice leave NO usable corpse (STATE_CORPSE_NOSELECT). expect(res.corpseConsumed).toBe(false) expect(cShattered.consumed).toBe(false) expect(res.lifeHealed).toBe(0) }) it('4.5 simulateVineConsumption safely rejects non-corpse entities', () => { const owner = { maxHp: 1000, maxMana: 500 } const cNonCorpse = { consumed: false, isCorpse: false } as any const res = simulateVineConsumption(DRUID_SUMMON_SKILL_IDS.CARRION_VINE, 10, cNonCorpse, owner) // In 1.13c, non-corpse entities cannot be selected (SKILLS_SrvSt63_Corpse_VineCycler). expect(res.corpseConsumed).toBe(false) expect(cNonCorpse.consumed).toBe(false) expect(res.lifeHealed).toBe(0) }) it('4.6 Comprehensive pathological corpse array rejection in SummonManager.tickPets', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg, { maxhp: 1000 * FIXED_ONE, hitpoints: 500 * FIXED_ONE, maxmana: 500 * FIXED_ONE, mana: 250 * FIXED_ONE }) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus } const mEngine = new MissileEngine(reg) const aScanner = new AuraScanner(reg) // Create Carrion Vine sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.CARRION_VINE, slvl: 10 }) const adversarialCorpses: any[] = [ null, undefined, { id: 'shattered_ice', consumed: false, shattered: true }, { id: 'non_corpse_barrel', consumed: false, isCorpse: false }, { id: 'already_eaten', consumed: true, isBoss: false }, { id: 'act_boss_diablo', consumed: false, isBoss: true }, { id: 'valid_corpse_1', consumed: false, isBoss: false, shattered: false, isCorpse: true }, { id: 'shattered_ice_2', consumed: false, shattered: true }, ] // Tick pets with adversarial corpse array const outcome = sm.tickPets({ currentTick: 1, owner, enemies: [], enemyPositions: new Map(), corpses: adversarialCorpses, missileEngine: mEngine, auraScanner: aScanner, }) expect(outcome.corpsesConsumedByPets).toBe(1) expect(adversarialCorpses[6].consumed).toBe(true) // valid_corpse_1 consumed // Shattered and non-corpse remain unconsumed! expect(adversarialCorpses[2].consumed).toBe(false) expect(adversarialCorpses[3].consumed).toBe(false) expect(adversarialCorpses[5].consumed).toBe(false) expect(adversarialCorpses[7].consumed).toBe(false) // Subsequent tick with remaining corpses: none are valid, so 0 consumed const outcome2 = sm.tickPets({ currentTick: 2, owner, enemies: [], enemyPositions: new Map(), corpses: adversarialCorpses, missileEngine: mEngine, auraScanner: aScanner, }) expect(outcome2.corpsesConsumedByPets).toBe(0) }) it('4.7 simulateDireWolfCorpseFrenzy safely rejects shattered, non-corpse, and null/undefined corpses', () => { expect(simulateDireWolfCorpseFrenzy(null).rageActivated).toBe(false) expect(simulateDireWolfCorpseFrenzy(undefined).rageActivated).toBe(false) const cShattered = { consumed: false, shattered: true } expect(simulateDireWolfCorpseFrenzy(cShattered).rageActivated).toBe(false) expect(cShattered.consumed).toBe(false) const cNonCorpse = { consumed: false, isCorpse: false } expect(simulateDireWolfCorpseFrenzy(cNonCorpse).rageActivated).toBe(false) expect(cNonCorpse.consumed).toBe(false) const cBoss = { consumed: false, isBoss: true } expect(simulateDireWolfCorpseFrenzy(cBoss).rageActivated).toBe(false) expect(cBoss.consumed).toBe(false) const cValid = { consumed: false, isBoss: false, shattered: false, isCorpse: true } const resValid = simulateDireWolfCorpseFrenzy(cValid) expect(resValid.rageActivated).toBe(true) expect(resValid.damageMultiplier).toBe(2.0) expect(cValid.consumed).toBe(true) }) }) // ========================================================================= // 5. Spirit Aura Dissemination // ========================================================================= describe('5. Spirit Aura Dissemination', () => { it('5.1 AuraScanner pulses Oak Sage (298) % Max Life to party members within range', async () => { const reg = await getSharedDataRegistry() const scanner = new AuraScanner(reg) const playerStats = new UnitStatList(reg, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const playerBus = new StateBus(playerStats, reg) const player: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: playerStats, stateBus: playerBus, x: 0, y: 0 } const allyStats = new UnitStatList(reg, { maxhp: 800 * FIXED_ONE, hitpoints: 800 * FIXED_ONE }) const allyBus = new StateBus(allyStats, reg) const ally: CombatUnitContext = { id: 'ally_1', name: 'Mercenary', statList: allyStats, stateBus: allyBus, x: 20, y: 20 } // Oak Sage slvl 10: 25 + 5 * 10 = 75% max life const auraSrc = scanner.setActiveAura(player, 298, 10, 0)! expect(auraSrc).toBeDefined() const outcome = scanner.pulseAura(auraSrc, 0, [player, ally], []) expect(outcome.alliesBuffed).toBe(2) // Ally received oaksage state with item_maxhp_percent: 75 const allyState = allyBus.getState('oaksage') expect(allyState).toBeDefined() expect(allyState!.stats['item_maxhp_percent']).toBe(75) // Ally max HP scaled from 800 to 1400 (800 * 1.75) expect(allyStats.getMaxHp256() / FIXED_ONE).toBe(1400) }) it('5.2 AuraScanner pulses Heart of Wolverine (297) % Damage and % AR to party members within range', async () => { const reg = await getSharedDataRegistry() const scanner = new AuraScanner(reg) const playerStats = new UnitStatList(reg) const playerBus = new StateBus(playerStats, reg) const player: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: playerStats, stateBus: playerBus, x: 0, y: 0 } const allyStats = new UnitStatList(reg, { tohit: 1000 }) const allyBus = new StateBus(allyStats, reg) const ally: CombatUnitContext = { id: 'ally_1', name: 'Mercenary', statList: allyStats, stateBus: allyBus, x: 20, y: 20 } // Heart of Wolverine slvl 10: 18 + 7*10 = 88% AR, 13 + 7*10 = 83% ED const auraSrc = scanner.setActiveAura(player, 297, 10, 0)! expect(auraSrc).toBeDefined() const outcome = scanner.pulseAura(auraSrc, 0, [player, ally], []) expect(outcome.alliesBuffed).toBe(2) const allyState = allyBus.getState('wolverine') expect(allyState).toBeDefined() expect(allyState!.stats['item_tohit_percent']).toBe(88) expect(allyState!.stats['damagepercent']).toBe(83) expect(allyStats.getAccruedStat('item_tohit_percent')).toBe(88) expect(allyStats.getAccruedStat('damagepercent')).toBe(83) }) it('5.3 SummonManager.tickPets disseminates Spirit Auras to party members and allies', async () => { const reg = await getSharedDataRegistry() const arena = new WorldArena({ registry: reg, classCode: 'dru', skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE, slvl: 10, }) // Spawn an ally in arena.allies const allyStats = new UnitStatList(reg, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE }) const allyBus = new StateBus(allyStats, reg) const ally: CombatUnitContext = { id: 'ally_merc', name: 'Act2_Desert_Merc', statList: allyStats, stateBus: allyBus, x: 10, y: 10, } arena.allies.push(ally) // Cast Oak Sage in arena arena.triggerCast() expect(arena.summonManager.getActivePets().length).toBe(1) // Step 60 ticks for pet tick & aura pulse arena.stepTicks(60) // Player receives control state: expect(arena.player.stateBus.hasState('oaksagecontrol')).toBe(true) // Oak Sage aura pulses to party allies within range: expect(ally.stateBus.hasState('oaksage')).toBe(true) expect(allyStats.getAccruedStat('item_maxhp_percent')).toBe(75) }) it('5.4 Heart of Wolverine and Spirit of Barbs disseminate to party allies AND other active pets', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus, x: 0, y: 0 } const allyStats = new UnitStatList(reg, { tohit: 500 }) const allyBus = new StateBus(allyStats, reg) const ally: CombatUnitContext = { id: 'ally_rogue', name: 'Act1_Rogue', statList: allyStats, stateBus: allyBus, x: 20, y: 20 } const mEngine = new MissileEngine(reg) const aScanner = new AuraScanner(reg) // 1. Create Grizzly combat pet const resGz = sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_GRIZZLY, slvl: 10, x: 10, y: 10 }) const grizzly = resGz.pet! // 2. Create Heart of Wolverine totem sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.HEART_OF_WOLVERINE, slvl: 10, x: 0, y: 0 }) // Step tick sm.tickPets({ currentTick: 10, owner, allies: [ally], enemies: [], enemyPositions: new Map(), missileEngine: mEngine, auraScanner: aScanner, }) // Owner receives wolverine control state expect(owner.stateBus.hasState('wolverinecontrol')).toBe(true) // Ally receives wolverine target buff state expect(ally.stateBus.hasState('wolverine')).toBe(true) expect(ally.statList.getAccruedStat('damagepercent')).toBe(83) // 13 + 7*10 expect(ally.statList.getAccruedStat('item_tohit_percent')).toBe(88) // 18 + 7*10 // Other pet (Grizzly) also receives wolverine buff! expect(grizzly.stateBus.hasState('wolverine')).toBe(true) expect(grizzly.statList.getAccruedStat('damagepercent')).toBe(83 + 25 + 9 * 10) // 83 from aura + 115 from passive = 198 expect(grizzly.statList.getAccruedStat('item_tohit_percent')).toBe(88) // Test Spirit of Barbs: const smBarbs = new SummonManager(reg) const bOwnerStats = new UnitStatList(reg) const bOwnerBus = new StateBus(bOwnerStats, reg) const bOwner: CombatUnitContext = { id: 'player_b', name: 'Hero_Druid', statList: bOwnerStats, stateBus: bOwnerBus, x: 0, y: 0 } const bAllyStats = new UnitStatList(reg) const bAllyBus = new StateBus(bAllyStats, reg) const bAlly: CombatUnitContext = { id: 'ally_merc', name: 'Act2_Merc', statList: bAllyStats, stateBus: bAllyBus, x: 15, y: 15 } const bWolf = smBarbs.createPet({ owner: bOwner, skillId: DRUID_SUMMON_SKILL_IDS.SUMMON_SPIRIT_WOLF, slvl: 5, x: 10, y: 10 }).pet! smBarbs.createPet({ owner: bOwner, skillId: DRUID_SUMMON_SKILL_IDS.SPIRIT_OF_BARBS, slvl: 10, x: 0, y: 0 }) smBarbs.tickPets({ currentTick: 10, owner: bOwner, allies: [bAlly], enemies: [], enemyPositions: new Map(), missileEngine: mEngine, auraScanner: aScanner, }) expect(bOwner.stateBus.hasState('barbscontrol')).toBe(true) expect(bAlly.stateBus.hasState('barbs')).toBe(true) expect(bWolf.stateBus.hasState('barbs')).toBe(true) // Thorns return percent: 50 + 9 * 10 = 140% expect(bAlly.statList.getAccruedStat('thorns_percent')).toBe(140) expect(bWolf.statList.getAccruedStat('thorns_percent')).toBe(140) }) it('5.5 Spatial distance cutoff: distant allies are excluded while nearby allies and pets are buffed', async () => { const reg = await getSharedDataRegistry() const sm = new SummonManager(reg) const stats = new UnitStatList(reg, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const bus = new StateBus(stats, reg) const owner: CombatUnitContext = { id: 'player', name: 'Hero_Druid', statList: stats, stateBus: bus, x: 0, y: 0 } // Near ally (within radius) const nearAllyStats = new UnitStatList(reg, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE }) const nearAllyBus = new StateBus(nearAllyStats, reg) const nearAlly: CombatUnitContext = { id: 'ally_near', name: 'Near_Merc', statList: nearAllyStats, stateBus: nearAllyBus, x: 30, y: 30 } // Far ally (far out of radius: 3000px away) const farAllyStats = new UnitStatList(reg, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE }) const farAllyBus = new StateBus(farAllyStats, reg) const farAlly: CombatUnitContext = { id: 'ally_far', name: 'Far_Merc', statList: farAllyStats, stateBus: farAllyBus, x: 3000, y: 3000 } const mEngine = new MissileEngine(reg) const aScanner = new AuraScanner(reg) // Create Oak Sage sm.createPet({ owner, skillId: DRUID_SUMMON_SKILL_IDS.OAK_SAGE, slvl: 10, x: 0, y: 0 }) sm.tickPets({ currentTick: 10, owner, allies: [nearAlly, farAlly], enemies: [], enemyPositions: new Map(), missileEngine: mEngine, auraScanner: aScanner, }) // Near ally is buffed expect(nearAlly.stateBus.hasState('oaksage')).toBe(true) // Far ally is NOT buffed expect(farAlly.stateBus.hasState('oaksage')).toBe(false) }) }) })