927 lines
39 KiB
TypeScript
927 lines
39 KiB
TypeScript
/**
|
|
* 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/game/skills/druid-summon.ts'
|
|
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
|
|
import { UnitStatList, FIXED_ONE } from '../../../src/game/engine/stat-list.ts'
|
|
import { StateBus } from '../../../src/game/engine/state-bus.ts'
|
|
import { SummonManager } from '../../../src/game/engine/summon-manager.ts'
|
|
import { AuraScanner } from '../../../src/game/engine/aura-scanner.ts'
|
|
import { MissileEngine } from '../../../src/game/engine/missile-engine.ts'
|
|
import { WorldArena } from '../../../src/game/engine/world-arena.ts'
|
|
import type { CombatUnitContext } from '../../../src/game/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)
|
|
})
|
|
})
|
|
})
|