diablo2-web/tests/skills/dru/adv-dru-adversarial-stress-...

689 lines
28 KiB
TypeScript

/**
* Diablo II: Lord of Destruction v1.13c — Druid Skills Adversarial Stress Testing Harness
*
* Dedicated Challenger Suite for Cohort 6 Iteration 2:
* 1. Cyclone Armor Poison Leak & Multi-Elemental Stress Testing
* 2. Shapeshifting Form Exclusivity & Pre-cast Guards Exhaustive Matrix
* 3. Armageddon & Hurricane Persistent Aura States & Duration Scaling
* 4. Melee Charge Progression & Contagion Mechanics
*/
import { describe, expect, it } from 'vitest'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { UnitStatList } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
import { MissileEngine } from '../../../src/game/engine/missile-engine.ts'
import { SummonManager } from '../../../src/game/engine/summon-manager.ts'
import { AuraScanner } from '../../../src/game/engine/aura-scanner.ts'
import { executeSUnitDmg, type CombatUnitContext } from '../../../src/game/engine/combat-pipeline.ts'
import { executeSkill113c, evaluateSkill113c } from '../../../src/game/skills/registry.ts'
import {
canCastDruidSkill,
calculateFeralRageStats,
calculateMaulStats,
calculateRabiesStats,
advanceChargeOrb,
simulateRabiesContagion,
DRUID_SHAPESHIFT_SKILL_IDS,
} from '../../../src/game/skills/druid-shapeshift.ts'
import {
calculateArmageddonStats,
calculateHurricaneStats,
} from '../../../src/game/skills/druid-elemental.ts'
function createTestUnit(
id: string,
name: string,
unitType: 'player' | 'monster',
maxHp = 2000,
): CombatUnitContext {
const statList = new UnitStatList()
statList.setBaseStat('maxhp', maxHp << 8)
statList.setHp256(maxHp << 8)
const stateBus = new StateBus(statList)
return {
id,
name,
unitType,
statList,
stateBus,
}
}
describe('Challenger Adversarial Stress Testing — Druid Elemental & Shapeshifting Invariants', () => {
// =========================================================================
// Dimension 1: Cyclone Armor Poison Leak & Multi-Elemental Stress Testing
// =========================================================================
describe('Dimension 1: Cyclone Armor Invariants & Differential Fuzzing', () => {
it('1.1 Cyclone Armor Poison Leak: Poison completely bypasses pool across 50 random damage/pool configurations', () => {
// Differential property test:
// For any incoming poison damage and any shield pool size:
// - Shield pool MUST remain completely untouched.
// - Defender HP MUST decrease by the exact poison damage amount (net of resistance).
for (let seed = 1; seed <= 50; seed++) {
const initialHp = 5000
const defender = createTestUnit(`def_${seed}`, 'DruidDefender', 'player', initialHp)
const attacker = createTestUnit(`att_${seed}`, 'Attacker', 'monster')
const poolSize = 50 + ((seed * 37) % 500) // 50 .. 549
const poisonDmg = 10 + ((seed * 61) % 400) // 10 .. 409
defender.stateBus.applyState({
stateNameOrId: 'cyclonearmor',
slvl: 10,
stats: { magicarmor: poolSize },
})
const outcome = executeSUnitDmg(attacker, defender, {
skillId: 92, // Poison Nova
elemType: 'pois',
elemMin256: poisonDmg << 8,
elemMax256: poisonDmg << 8,
isPoison: true,
autoHit: true,
})
const armorState = defender.stateBus.getState('cyclonearmor')
expect(armorState, `Cyclone Armor should still be active for seed ${seed}`).toBeDefined()
expect(
armorState?.stats['magicarmor'],
`Cyclone Armor pool must NOT be depleted by poison damage (seed ${seed})`,
).toBe(poolSize)
const hpLost = initialHp - (defender.statList.getHp256() >> 8)
expect(hpLost, `Defender HP must take direct poison damage (seed ${seed})`).toBe(poisonDmg)
expect(outcome.totalDamage).toBe(poisonDmg)
}
})
it('1.2 Elemental Absorption: Fire, Cold, Lightning damage are absorbed; Physical & Magic bypass', () => {
const elements: Array<{
elem: 'fire' | 'cold' | 'ltng' | 'lightning' | 'phys' | 'mag'
isPoison?: boolean
shouldAbsorb: boolean
}> = [
{ elem: 'fire', shouldAbsorb: true },
{ elem: 'cold', shouldAbsorb: true },
{ elem: 'ltng', shouldAbsorb: true },
{ elem: 'lightning', shouldAbsorb: true },
{ elem: 'phys', shouldAbsorb: false },
{ elem: 'mag', shouldAbsorb: false },
{ elem: 'fire', isPoison: true, shouldAbsorb: false }, // if flagged poison, bypass
]
for (const tc of elements) {
const initialHp = 2000
const defender = createTestUnit('def_el', 'DruidDefender', 'player', initialHp)
const attacker = createTestUnit('att_el', 'Attacker', 'monster')
const initialPool = 300
const incomingDmg = 120
defender.stateBus.applyState({
stateNameOrId: 'cyclonearmor',
slvl: 10,
stats: { magicarmor: initialPool },
})
const outcome = executeSUnitDmg(attacker, defender, {
skillId: tc.elem === 'phys' ? 0 : 50,
...(tc.elem === 'phys'
? { flatPhysMin256: incomingDmg << 8, flatPhysMax256: incomingDmg << 8 }
: {
elemType: tc.elem,
elemMin256: incomingDmg << 8,
elemMax256: incomingDmg << 8,
...(tc.isPoison ? { isPoison: true } : {}),
}),
autoHit: true,
})
const armorState = defender.stateBus.getState('cyclonearmor')
if (tc.shouldAbsorb) {
expect(
armorState?.stats['magicarmor'],
`${tc.elem} must reduce Cyclone Armor pool`,
).toBe(initialPool - incomingDmg)
expect(defender.statList.getHp256() >> 8, `${tc.elem} should NOT reduce HP when fully absorbed`).toBe(initialHp)
expect(outcome.totalDamage).toBe(0)
} else {
expect(
armorState?.stats['magicarmor'],
`${tc.elem} must NOT reduce Cyclone Armor pool`,
).toBe(initialPool)
expect(defender.statList.getHp256() >> 8, `${tc.elem} must deal direct damage to defender HP`).toBe(initialHp - incomingDmg)
expect(outcome.totalDamage).toBe(incomingDmg)
}
}
})
it('1.3 Overflow Absorption: Elemental damage exceeding pool dissipates shield and leaks overflow to HP', () => {
const initialHp = 1000
const defender = createTestUnit('def_ov', 'DruidDefender', 'player', initialHp)
const attacker = createTestUnit('att_ov', 'Attacker', 'monster')
const initialPool = 100
const incomingFireDmg = 250 // 100 absorbed, 150 overflow
defender.stateBus.applyState({
stateNameOrId: 'cyclonearmor',
slvl: 10,
stats: { magicarmor: initialPool },
})
const outcome = executeSUnitDmg(attacker, defender, {
skillId: 50,
elemType: 'fire',
elemMin256: incomingFireDmg << 8,
elemMax256: incomingFireDmg << 8,
autoHit: true,
})
// Armor pool depleted to 0 -> state removed!
expect(defender.stateBus.hasState('cyclonearmor')).toBe(false)
// Overflow of 150 damage deals HP damage (1000 - 150 = 850)
expect(defender.statList.getHp256() >> 8).toBe(850)
expect(outcome.totalDamage).toBe(150)
})
})
// =========================================================================
// Dimension 2: Shapeshifting Form Exclusivity & Pre-cast Guards
// =========================================================================
describe('Dimension 2: Shapeshifting Form Exclusivity & Pre-cast Guards Matrix', () => {
it('2.1 Exhaustive Matrix: all 18 human-only Druid skills fail-fast in Wolf and Bear forms with invalid_form', async () => {
const reg = await getSharedDataRegistry()
const missileEngine = new MissileEngine(reg)
const summonManager = new SummonManager(reg)
const auraScanner = new AuraScanner(reg)
const humanOnlySkills = [
// 10 Summons
221, 222, 226, 227, 231, 236, 237, 241, 246, 247,
// 8 Elemental Spells
225, 229, 230, 234, 235, 240, 244, 245,
]
for (const skillId of humanOnlySkills) {
const skill = reg.getSkillById(skillId)!
// Wolf form test
const casterWolf = createTestUnit(`wolf_${skillId}`, 'WolfDruid', 'player')
casterWolf.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
const evalWolf = evaluateSkill113c({
registry: reg,
skill,
slvl: 10,
blvl: 10,
statList: casterWolf.statList,
stateBus: casterWolf.stateBus,
})
const outWolf = executeSkill113c({
registry: reg,
skill,
evalResult: evalWolf,
caster: casterWolf,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine,
auraScanner,
summonManager,
currentTick: 0,
targetX: 10,
targetY: 10,
})
expect(outWolf.executed, `Skill ${skillId} (${skill.name}) must NOT execute in wolf form`).toBe(false)
expect(outWolf.error, `Skill ${skillId} (${skill.name}) must return error: 'invalid_form' in wolf form`).toBe('invalid_form')
// Bear form test
const casterBear = createTestUnit(`bear_${skillId}`, 'BearDruid', 'player')
casterBear.stateBus.applyState({ stateNameOrId: 'bear', slvl: 10, stats: {} })
const evalBear = evaluateSkill113c({
registry: reg,
skill,
slvl: 10,
blvl: 10,
statList: casterBear.statList,
stateBus: casterBear.stateBus,
})
const outBear = executeSkill113c({
registry: reg,
skill,
evalResult: evalBear,
caster: casterBear,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine,
auraScanner,
summonManager,
currentTick: 0,
targetX: 10,
targetY: 10,
})
expect(outBear.executed, `Skill ${skillId} (${skill.name}) must NOT execute in bear form`).toBe(false)
expect(outBear.error, `Skill ${skillId} (${skill.name}) must return error: 'invalid_form' in bear form`).toBe('invalid_form')
// Human form test: should execute without invalid_form error
const casterHuman = createTestUnit(`hum_${skillId}`, 'HumanDruid', 'player')
const evalHuman = evaluateSkill113c({
registry: reg,
skill,
slvl: 10,
blvl: 10,
statList: casterHuman.statList,
stateBus: casterHuman.stateBus,
})
const outHuman = executeSkill113c({
registry: reg,
skill,
evalResult: evalHuman,
caster: casterHuman,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine,
auraScanner,
summonManager,
currentTick: 0,
targetX: 10,
targetY: 10,
})
expect(outHuman.error, `Skill ${skillId} (${skill.name}) in human form must NOT return invalid_form`).toBeUndefined()
}
})
it('2.2 Armageddon (#249) and Hurricane (#250) execute successfully in Wolf, Bear, and Human forms', async () => {
const reg = await getSharedDataRegistry()
const missileEngine = new MissileEngine(reg)
const auraScanner = new AuraScanner(reg)
const stormSkills = [249, 250]
const forms: Array<'human' | 'wolf' | 'bear'> = ['human', 'wolf', 'bear']
for (const skillId of stormSkills) {
const skill = reg.getSkillById(skillId)!
for (const form of forms) {
const caster = createTestUnit(`caster_${skillId}_${form}`, `Druid_${form}`, 'player')
if (form !== 'human') {
caster.stateBus.applyState({ stateNameOrId: form, slvl: 10, stats: {} })
}
const evalRes = evaluateSkill113c({
registry: reg,
skill,
slvl: 10,
blvl: 10,
statList: caster.statList,
stateBus: caster.stateBus,
})
const out = executeSkill113c({
registry: reg,
skill,
evalResult: evalRes,
caster,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine,
auraScanner,
summonManager: null as any,
currentTick: 0,
targetX: 0,
targetY: 0,
})
expect(out.executed, `Skill ${skillId} must execute in ${form} form`).toBe(true)
expect(out.error).toBeUndefined()
const expectedState = skillId === 249 ? 'armageddon' : 'hurricane'
expect(caster.stateBus.hasState(expectedState)).toBe(true)
}
}
})
it('2.3 Form-exclusive attacks: Werewolf attacks blocked in Bear/Human; Werebear attacks blocked in Wolf/Human', async () => {
const reg = await getSharedDataRegistry()
const missileEngine = new MissileEngine(reg)
const target = createTestUnit('target', 'Enemy', 'monster')
const wolfOnlySkills = [
DRUID_SHAPESHIFT_SKILL_IDS.FERAL_RAGE, // 232
DRUID_SHAPESHIFT_SKILL_IDS.RABIES, // 238
DRUID_SHAPESHIFT_SKILL_IDS.FURY, // 248
]
const bearOnlySkills = [
DRUID_SHAPESHIFT_SKILL_IDS.MAUL, // 233
DRUID_SHAPESHIFT_SKILL_IDS.SHOCK_WAVE, // 243
]
// Test wolf-only skills
for (const skillId of wolfOnlySkills) {
const skill = reg.getSkillById(skillId)!
// Blocked in Human
const humanCaster = createTestUnit('h', 'Human', 'player')
const evalH = evaluateSkill113c({ registry: reg, skill, slvl: 10, blvl: 10, statList: humanCaster.statList, stateBus: humanCaster.stateBus })
const outH = executeSkill113c({
registry: reg, skill, evalResult: evalH, caster: humanCaster, targets: [target], targetPositions: new Map(), corpses: [],
missileEngine, auraScanner: null as any, summonManager: null as any, currentTick: 0, targetX: 0, targetY: 0,
})
expect(outH.executed).toBe(false)
expect(outH.error).toBe('invalid_form')
// Blocked in Bear
const bearCaster = createTestUnit('b', 'Bear', 'player')
bearCaster.stateBus.applyState({ stateNameOrId: 'bear', slvl: 10, stats: {} })
const evalB = evaluateSkill113c({ registry: reg, skill, slvl: 10, blvl: 10, statList: bearCaster.statList, stateBus: bearCaster.stateBus })
const outB = executeSkill113c({
registry: reg, skill, evalResult: evalB, caster: bearCaster, targets: [target], targetPositions: new Map(), corpses: [],
missileEngine, auraScanner: null as any, summonManager: null as any, currentTick: 0, targetX: 0, targetY: 0,
})
expect(outB.executed).toBe(false)
expect(outB.error).toBe('invalid_form')
}
// Test bear-only skills
for (const skillId of bearOnlySkills) {
const skill = reg.getSkillById(skillId)!
// Blocked in Human
const humanCaster = createTestUnit('h', 'Human', 'player')
const evalH = evaluateSkill113c({ registry: reg, skill, slvl: 10, blvl: 10, statList: humanCaster.statList, stateBus: humanCaster.stateBus })
const outH = executeSkill113c({
registry: reg, skill, evalResult: evalH, caster: humanCaster, targets: [target], targetPositions: new Map(), corpses: [],
missileEngine, auraScanner: null as any, summonManager: null as any, currentTick: 0, targetX: 0, targetY: 0,
})
expect(outH.executed).toBe(false)
expect(outH.error).toBe('invalid_form')
// Blocked in Wolf
const wolfCaster = createTestUnit('w', 'Wolf', 'player')
wolfCaster.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
const evalW = evaluateSkill113c({ registry: reg, skill, slvl: 10, blvl: 10, statList: wolfCaster.statList, stateBus: wolfCaster.stateBus })
const outW = executeSkill113c({
registry: reg, skill, evalResult: evalW, caster: wolfCaster, targets: [target], targetPositions: new Map(), corpses: [],
missileEngine, auraScanner: null as any, summonManager: null as any, currentTick: 0, targetX: 0, targetY: 0,
})
expect(outW.executed).toBe(false)
expect(outW.error).toBe('invalid_form')
}
})
it('2.4 Cross-morphing: Werewolf to Werebear cleanly evicts wolf and applies bear state with bear buffs', async () => {
const reg = await getSharedDataRegistry()
const caster = createTestUnit('dru_morph', 'MorphDruid', 'player')
// Start in Werewolf
const skillWolf = reg.getSkillById(223)!
const evalWolf = evaluateSkill113c({ registry: reg, skill: skillWolf, slvl: 5, blvl: 5, statList: caster.statList, stateBus: caster.stateBus })
executeSkill113c({
registry: reg, skill: skillWolf, evalResult: evalWolf, caster, targets: [], targetPositions: new Map(), corpses: [],
missileEngine: null as any, auraScanner: null as any, summonManager: null as any, currentTick: 0, targetX: 0, targetY: 0,
})
expect(caster.stateBus.hasState('wolf')).toBe(true)
expect(caster.stateBus.hasState('bear')).toBe(false)
// Cross-morph directly to Werebear (#228)
const skillBear = reg.getSkillById(228)!
const evalBear = evaluateSkill113c({ registry: reg, skill: skillBear, slvl: 10, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
const outBear = executeSkill113c({
registry: reg, skill: skillBear, evalResult: evalBear, caster, targets: [], targetPositions: new Map(), corpses: [],
missileEngine: null as any, auraScanner: null as any, summonManager: null as any, currentTick: 100, targetX: 0, targetY: 0,
})
expect(outBear.executed).toBe(true)
expect(caster.stateBus.hasState('wolf'), 'Wolf state must be evicted').toBe(false)
expect(caster.stateBus.hasState('bear'), 'Bear state must be active').toBe(true)
const bearState = caster.stateBus.getState('bear')
expect(bearState?.stats['damagepercent']).toBe(55 + (10 - 1) * 8)
expect(bearState?.stats['item_armor_percent']).toBe(25 + (10 - 1) * 6)
// Morph back to Werewolf (#223)
const outWolf2 = executeSkill113c({
registry: reg, skill: skillWolf, evalResult: evalWolf, caster, targets: [], targetPositions: new Map(), corpses: [],
missileEngine: null as any, auraScanner: null as any, summonManager: null as any, currentTick: 200, targetX: 0, targetY: 0,
})
expect(outWolf2.executed).toBe(true)
expect(caster.stateBus.hasState('bear'), 'Bear state must be evicted').toBe(false)
expect(caster.stateBus.hasState('wolf'), 'Wolf state must be active').toBe(true)
})
})
// =========================================================================
// Dimension 3: Armageddon & Hurricane Persistent Aura States
// =========================================================================
describe('Dimension 3: Armageddon & Hurricane Persistent Aura States', () => {
it('3.1 Armageddon (#249) attaches armageddon state with scaling duration and records in statesApplied', async () => {
const reg = await getSharedDataRegistry()
const missileEngine = new MissileEngine(reg)
const auraScanner = new AuraScanner(reg)
const caster = createTestUnit('dru_arma', 'ArmaDruid', 'player')
const skill249 = reg.getSkillById(249)!
// Test at slvl 15
const eval249 = evaluateSkill113c({
registry: reg,
skill: skill249,
slvl: 15,
blvl: 15,
statList: caster.statList,
stateBus: caster.stateBus,
})
const out = executeSkill113c({
registry: reg,
skill: skill249,
evalResult: eval249,
caster,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine,
auraScanner,
summonManager: null as any,
currentTick: 50,
targetX: 0,
targetY: 0,
})
expect(out.executed).toBe(true)
expect(out.statesApplied).toContain('armageddon')
expect(caster.stateBus.hasState('armageddon')).toBe(true)
const armaState = caster.stateBus.getState('armageddon')!
expect(armaState.sourceSkillId).toBe(249)
expect(armaState.durationFrames).toBeGreaterThanOrEqual(250)
})
it('3.2 Hurricane (#250) attaches hurricane state with scaling duration and records in statesApplied', async () => {
const reg = await getSharedDataRegistry()
const missileEngine = new MissileEngine(reg)
const auraScanner = new AuraScanner(reg)
const caster = createTestUnit('dru_hurr', 'HurrDruid', 'player')
const skill250 = reg.getSkillById(250)!
const eval250 = evaluateSkill113c({
registry: reg,
skill: skill250,
slvl: 20,
blvl: 20,
statList: caster.statList,
stateBus: caster.stateBus,
})
const out = executeSkill113c({
registry: reg,
skill: skill250,
evalResult: eval250,
caster,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine,
auraScanner,
summonManager: null as any,
currentTick: 100,
targetX: 0,
targetY: 0,
})
expect(out.executed).toBe(true)
expect(out.statesApplied).toContain('hurricane')
expect(caster.stateBus.hasState('hurricane')).toBe(true)
const hurrState = caster.stateBus.getState('hurricane')!
expect(hurrState.sourceSkillId).toBe(250)
expect(hurrState.durationFrames).toBeGreaterThanOrEqual(250)
})
})
// =========================================================================
// Dimension 4: Melee Charge Progression & Contagion
// =========================================================================
describe('Dimension 4: Melee Charge Progression & Contagion Mechanics', () => {
it('4.1 Feral Rage (#232): successive hits advance charges 1..3 with stats and 20.0s decay timer; capped at 3', async () => {
const reg = await getSharedDataRegistry()
const caster = createTestUnit('dru_fr', 'WolfDruid', 'player')
caster.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
const target = createTestUnit('target_fr', 'TargetMonster', 'monster', 5000)
const skill232 = reg.getSkillById(232)!
const slvl = 10
const eval232 = evaluateSkill113c({ registry: reg, skill: skill232, slvl, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
const frStats = calculateFeralRageStats(slvl)
// Expected progression:
// Hit 1 -> stage 1
// Hit 2 -> stage 2
// Hit 3 -> stage 3
// Hit 4 -> stage 3 (capped)
for (let hitIndex = 1; hitIndex <= 4; hitIndex++) {
const tick = hitIndex * 25
const out = executeSkill113c({
registry: reg,
skill: skill232,
evalResult: eval232,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: null as any,
auraScanner: null as any,
summonManager: null as any,
currentTick: tick,
targetX: 0,
targetY: 0,
})
expect(out.executed).toBe(true)
expect(out.statesApplied).toContain('feralrage')
const expectedCharge = Math.min(hitIndex, 3)
const expectedLeech = Math.round((frStats.lifeLeechPct * expectedCharge) / 3)
const expectedVelocity = Math.round((frStats.velocityBonusPct * expectedCharge) / 3)
const state = caster.stateBus.getState('feralrage')
expect(state, `Feral rage state must exist after hit ${hitIndex}`).toBeDefined()
expect(state?.stats['charge']).toBe(expectedCharge)
expect(state?.stats['progressive_charges']).toBe(expectedCharge)
expect(state?.stats['life_leech']).toBe(expectedLeech)
expect(state?.stats['life_leech_pct']).toBe(expectedLeech)
expect(state?.stats['velocitypercent']).toBe(expectedVelocity)
expect(state?.durationFrames).toBe(500) // 20.0 seconds
expect(state?.expireFrame).toBe(tick + 500) // decay timer refreshed
}
})
it('4.2 Maul (#233): successive hits advance charges 1..5 with damage % and stun duration; capped at 5', async () => {
const reg = await getSharedDataRegistry()
const caster = createTestUnit('dru_maul', 'BearDruid', 'player')
caster.stateBus.applyState({ stateNameOrId: 'bear', slvl: 10, stats: {} })
const target = createTestUnit('target_maul', 'TargetMonster', 'monster', 10000)
const skill233 = reg.getSkillById(233)!
const slvl = 10
const eval233 = evaluateSkill113c({ registry: reg, skill: skill233, slvl, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
const maulStats = calculateMaulStats(slvl)
for (let hitIndex = 1; hitIndex <= 6; hitIndex++) {
const tick = hitIndex * 25
const out = executeSkill113c({
registry: reg,
skill: skill233,
evalResult: eval233,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: null as any,
auraScanner: null as any,
summonManager: null as any,
currentTick: tick,
targetX: 0,
targetY: 0,
})
expect(out.executed).toBe(true)
expect(out.statesApplied).toContain('maul')
const expectedCharge = Math.min(hitIndex, 5)
const expectedDmgBonus = expectedCharge * maulStats.damageBonusPerChargePct
const state = caster.stateBus.getState('maul')
expect(state, `Maul state must exist after hit ${hitIndex}`).toBeDefined()
expect(state?.stats['charge']).toBe(expectedCharge)
expect(state?.stats['progressive_charges']).toBe(expectedCharge)
expect(state?.stats['damagepercent']).toBe(expectedDmgBonus)
expect(state?.stats['stunDurationFrames']).toBe(maulStats.stunDurationFrames)
expect(state?.durationFrames).toBe(500) // 20.0 seconds
expect(state?.expireFrame).toBe(tick + 500)
}
})
it('4.3 Rabies (#238): successful hit infects target with authentic rabies contagion state', async () => {
const reg = await getSharedDataRegistry()
const caster = createTestUnit('dru_rab', 'WolfDruid', 'player')
caster.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
const target = createTestUnit('target_rab', 'TargetMonster', 'monster', 2000)
const skill238 = reg.getSkillById(238)!
const slvl = 10
const eval238 = evaluateSkill113c({ registry: reg, skill: skill238, slvl, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
const rabiesStats = calculateRabiesStats(slvl, 0)
const out = executeSkill113c({
registry: reg,
skill: skill238,
evalResult: eval238,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: null as any,
auraScanner: null as any,
summonManager: null as any,
currentTick: 0,
targetX: 0,
targetY: 0,
})
expect(out.executed).toBe(true)
expect(out.statesApplied).toContain('rabies')
expect(target.stateBus.hasState('rabies')).toBe(true)
const rState = target.stateBus.getState('rabies')!
expect(rState.sourceSkillId).toBe(238)
expect(rState.durationFrames).toBe(rabiesStats.poisonDurationFrames)
expect(rState.stats['min_poison_damage']).toBe(rabiesStats.minPoisonDamage)
expect(rState.stats['max_poison_damage']).toBe(rabiesStats.maxPoisonDamage)
expect(rState.stats['contagion_radius_subtiles']).toBe(4)
expect(rState.stats['contagion_spread_interval']).toBe(4)
})
})
})