578 lines
22 KiB
TypeScript
578 lines
22 KiB
TypeScript
/**
|
|
* Diablo II: Lord of Destruction v1.13c — Druid Milestone M25 Adversarial Challenger Suite
|
|
*
|
|
* Empirical verification and adversarial stress-testing across 6 core mechanical dimensions:
|
|
* 1. Shapeshifted Casting Restrictions (Armageddon/Hurricane shift aura attachment; spell/summon fail-fast)
|
|
* 2. Shapeshifting Form Exclusivity (Werewolf <-> Werebear mutual eviction; form-exclusive attack execution)
|
|
* 3. Cyclone Armor Absorption Invariant (Fire/Cold/Ltng absorption; Physical & Poison complete bypass)
|
|
* 4. Tornado NextHitDelay (25-frame NextDelay target immunity window)
|
|
* 5. Feral Rage & Maul Orbs (Formula scaling, charge progression, runtime state application)
|
|
* 6. Rabies Contagion Mechanics (Poison damage, contagion spread simulation, runtime state attachment)
|
|
*/
|
|
|
|
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,
|
|
} from '../../../src/game/skills/druid-shapeshift.ts'
|
|
import {
|
|
simulateCycloneArmorAbsorption,
|
|
} from '../../../src/game/skills/druid-elemental.ts'
|
|
|
|
function createTestUnit(
|
|
id: string,
|
|
name: string,
|
|
unitType: 'player' | 'monster',
|
|
maxHp = 1000,
|
|
): CombatUnitContext {
|
|
const statList = new UnitStatList()
|
|
statList.addStat('maxhp', maxHp)
|
|
statList.setHp256(maxHp << 8)
|
|
const stateBus = new StateBus(statList)
|
|
return {
|
|
id,
|
|
name,
|
|
unitType,
|
|
statList,
|
|
stateBus,
|
|
}
|
|
}
|
|
|
|
describe('Adversarial Stress Testing — Cohort 6: Druid Elemental & Shapeshifting Skills', () => {
|
|
// =========================================================================
|
|
// Dimension 1: Shapeshifted Casting Restrictions
|
|
// =========================================================================
|
|
describe('Dimension 1: Shapeshifted Casting Restrictions', () => {
|
|
it('1.1 canCastDruidSkill predicate correctly permits Armageddon (249) and Hurricane (250) in all forms', () => {
|
|
expect(canCastDruidSkill('human', 249)).toBe(true)
|
|
expect(canCastDruidSkill('wolf', 249)).toBe(true)
|
|
expect(canCastDruidSkill('bear', 249)).toBe(true)
|
|
|
|
expect(canCastDruidSkill('human', 250)).toBe(true)
|
|
expect(canCastDruidSkill('wolf', 250)).toBe(true)
|
|
expect(canCastDruidSkill('bear', 250)).toBe(true)
|
|
})
|
|
|
|
it('1.2 canCastDruidSkill predicate rejects elemental spells and summons in wolf or bear form', () => {
|
|
const humanOnlySpells = [221, 222, 225, 226, 227, 229, 230, 231, 234, 235, 236, 237, 240, 241, 244, 245, 246, 247]
|
|
for (const id of humanOnlySpells) {
|
|
expect(canCastDruidSkill('human', id)).toBe(true)
|
|
expect(canCastDruidSkill('wolf', id)).toBe(false)
|
|
expect(canCastDruidSkill('bear', id)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('1.3 [FAILING / BUG REPRO] executeSkill113c runtime attaches armageddon and hurricane aura states to caster', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const auraScanner = new AuraScanner(reg)
|
|
const missileEngine = new MissileEngine(reg)
|
|
|
|
// In Wolf form, cast Armageddon (249)
|
|
const casterWolf = createTestUnit('dru_w', 'DruidWolf', 'player')
|
|
casterWolf.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
|
|
|
|
const skill249 = reg.getSkillById(249)!
|
|
const eval249 = evaluateSkill113c({ registry: reg, skill: skill249, slvl: 20, blvl: 20, statList: casterWolf.statList, stateBus: casterWolf.stateBus })
|
|
const out249 = executeSkill113c({
|
|
registry: reg,
|
|
skill: skill249,
|
|
evalResult: eval249,
|
|
caster: casterWolf,
|
|
targets: [],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine,
|
|
auraScanner,
|
|
summonManager: null as any,
|
|
currentTick: 0,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
})
|
|
|
|
expect(out249.executed).toBe(true)
|
|
// D2MOO SkillDruid.cpp:957 (SKILLS_SrvDo124_Armageddon_Hurricane) toggles state on caster
|
|
expect(casterWolf.stateBus.hasState('armageddon')).toBe(true)
|
|
|
|
// In Bear form, cast Hurricane (250)
|
|
const casterBear = createTestUnit('dru_b', 'DruidBear', 'player')
|
|
casterBear.stateBus.applyState({ stateNameOrId: 'bear', slvl: 10, stats: {} })
|
|
|
|
const skill250 = reg.getSkillById(250)!
|
|
const eval250 = evaluateSkill113c({ registry: reg, skill: skill250, slvl: 20, blvl: 20, statList: casterBear.statList, stateBus: casterBear.stateBus })
|
|
const out250 = executeSkill113c({
|
|
registry: reg,
|
|
skill: skill250,
|
|
evalResult: eval250,
|
|
caster: casterBear,
|
|
targets: [],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine,
|
|
auraScanner,
|
|
summonManager: null as any,
|
|
currentTick: 0,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
})
|
|
|
|
expect(out250.executed).toBe(true)
|
|
expect(casterBear.stateBus.hasState('hurricane')).toBe(true)
|
|
})
|
|
|
|
it('1.4 [FAILING / BUG REPRO] executeSkill113c runtime fails fast when casting prohibited elemental spells or summons while shapeshifted', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const missileEngine = new MissileEngine(reg)
|
|
const summonManager = new SummonManager(reg)
|
|
|
|
const casterWolf = createTestUnit('dru_w', 'DruidWolf', 'player')
|
|
casterWolf.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
|
|
|
|
// Firestorm (225) attempted in wolf form must fail fast (executed = false)
|
|
const skill225 = reg.getSkillById(225)!
|
|
const eval225 = evaluateSkill113c({ registry: reg, skill: skill225, slvl: 10, blvl: 10, statList: casterWolf.statList, stateBus: casterWolf.stateBus })
|
|
const out225 = executeSkill113c({
|
|
registry: reg,
|
|
skill: skill225,
|
|
evalResult: eval225,
|
|
caster: casterWolf,
|
|
targets: [],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine,
|
|
auraScanner: null as any,
|
|
summonManager,
|
|
currentTick: 0,
|
|
targetX: 100,
|
|
targetY: 100,
|
|
})
|
|
expect(out225.executed).toBe(false)
|
|
|
|
// Raven (221) attempted in wolf form must fail fast (executed = false)
|
|
const skill221 = reg.getSkillById(221)!
|
|
const eval221 = evaluateSkill113c({ registry: reg, skill: skill221, slvl: 10, blvl: 10, statList: casterWolf.statList, stateBus: casterWolf.stateBus })
|
|
const out221 = executeSkill113c({
|
|
registry: reg,
|
|
skill: skill221,
|
|
evalResult: eval221,
|
|
caster: casterWolf,
|
|
targets: [],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine,
|
|
auraScanner: null as any,
|
|
summonManager,
|
|
currentTick: 0,
|
|
targetX: 100,
|
|
targetY: 100,
|
|
})
|
|
expect(out221.executed).toBe(false)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 2: Shapeshifting Form Exclusivity
|
|
// =========================================================================
|
|
describe('Dimension 2: Shapeshifting Form Exclusivity', () => {
|
|
it('2.1 Casting Werebear while Werewolf is active evicts Werewolf state and applies Werebear', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const caster = createTestUnit('dru_1', 'DruidCaster', 'player')
|
|
|
|
// 1. Cast Werewolf
|
|
const skill223 = reg.getSkillById(223)!
|
|
const eval223 = evaluateSkill113c({ registry: reg, skill: skill223, slvl: 10, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
|
|
executeSkill113c({
|
|
registry: reg,
|
|
skill: skill223,
|
|
evalResult: eval223,
|
|
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)
|
|
|
|
// 2. Cast Werebear -> wolf must be evicted
|
|
const skill228 = reg.getSkillById(228)!
|
|
const eval228 = evaluateSkill113c({ registry: reg, skill: skill228, slvl: 10, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
|
|
executeSkill113c({
|
|
registry: reg,
|
|
skill: skill228,
|
|
evalResult: eval228,
|
|
caster,
|
|
targets: [],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine: null as any,
|
|
auraScanner: null as any,
|
|
summonManager: null as any,
|
|
currentTick: 10,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
})
|
|
expect(caster.stateBus.hasState('wolf')).toBe(false)
|
|
expect(caster.stateBus.hasState('bear')).toBe(true)
|
|
})
|
|
|
|
it('2.2 [FAILING / BUG REPRO] executeSkill113c rejects Werewolf attacks in Bear or Human form, and Werebear attacks in Wolf or Human form', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const missileEngine = new MissileEngine(reg)
|
|
const target = createTestUnit('m1', 'Fallen', 'monster')
|
|
|
|
// Human casting Feral Rage (232) must be rejected
|
|
const casterHuman = createTestUnit('dru_h', 'DruidHuman', 'player')
|
|
const skill232 = reg.getSkillById(232)!
|
|
const eval232 = evaluateSkill113c({ registry: reg, skill: skill232, slvl: 10, blvl: 10, statList: casterHuman.statList, stateBus: casterHuman.stateBus })
|
|
const out232 = executeSkill113c({
|
|
registry: reg,
|
|
skill: skill232,
|
|
evalResult: eval232,
|
|
caster: casterHuman,
|
|
targets: [target],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine,
|
|
auraScanner: null as any,
|
|
summonManager: null as any,
|
|
currentTick: 0,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
})
|
|
expect(out232.executed).toBe(false)
|
|
|
|
// Wolf casting Maul (233) must be rejected
|
|
const casterWolf = createTestUnit('dru_w', 'DruidWolf', 'player')
|
|
casterWolf.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
|
|
const skill233 = reg.getSkillById(233)!
|
|
const eval233 = evaluateSkill113c({ registry: reg, skill: skill233, slvl: 10, blvl: 10, statList: casterWolf.statList, stateBus: casterWolf.stateBus })
|
|
const out233 = executeSkill113c({
|
|
registry: reg,
|
|
skill: skill233,
|
|
evalResult: eval233,
|
|
caster: casterWolf,
|
|
targets: [target],
|
|
targetPositions: new Map(),
|
|
corpses: [],
|
|
missileEngine,
|
|
auraScanner: null as any,
|
|
summonManager: null as any,
|
|
currentTick: 0,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
})
|
|
expect(out233.executed).toBe(false)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 3: Cyclone Armor Absorption Invariant
|
|
// =========================================================================
|
|
describe('Dimension 3: Cyclone Armor Absorption Invariant', () => {
|
|
it('3.1 Pure simulator: absorbs Fire, Cold, Lightning, and allows Physical and Poison to bypass', () => {
|
|
const cap = 300
|
|
const fRes = simulateCycloneArmorAbsorption(cap, 100, 'fire')
|
|
expect(fRes.absorbed).toBe(100)
|
|
expect(fRes.remainingArmor).toBe(200)
|
|
|
|
const cRes = simulateCycloneArmorAbsorption(200, 100, 'cold')
|
|
expect(cRes.absorbed).toBe(100)
|
|
expect(cRes.remainingArmor).toBe(100)
|
|
|
|
const lRes = simulateCycloneArmorAbsorption(100, 100, 'lightning')
|
|
expect(lRes.absorbed).toBe(100)
|
|
expect(lRes.remainingArmor).toBe(0)
|
|
expect(lRes.didDissipate).toBe(true)
|
|
|
|
const pRes = simulateCycloneArmorAbsorption(300, 100, 'physical')
|
|
expect(pRes.absorbed).toBe(0)
|
|
expect(pRes.penetrated).toBe(100)
|
|
|
|
const psnRes = simulateCycloneArmorAbsorption(300, 100, 'poison')
|
|
expect(psnRes.absorbed).toBe(0)
|
|
expect(psnRes.penetrated).toBe(100)
|
|
})
|
|
|
|
it('3.2 [FAILING / BUG REPRO] executeSUnitDmg: Poison damage completely bypasses Cyclone Armor and deals direct damage to defender HP', () => {
|
|
const attacker = createTestUnit('attacker_1', 'PoisonCaster', 'monster')
|
|
const defender = createTestUnit('defender_1', 'DruidDefender', 'player', 1000)
|
|
|
|
defender.stateBus.applyState({
|
|
stateNameOrId: 'cyclonearmor',
|
|
slvl: 10,
|
|
stats: { magicarmor: 148 },
|
|
})
|
|
|
|
// 100 Poison damage incoming
|
|
const poisHit = executeSUnitDmg(attacker, defender, {
|
|
skillId: 92, // Poison Nova
|
|
elemType: 'pois',
|
|
elemMin256: 100 << 8,
|
|
elemMax256: 100 << 8,
|
|
isPoison: true,
|
|
autoHit: true,
|
|
})
|
|
|
|
// In authentic 1.13c, Poison damage bypasses Cyclone Armor:
|
|
// - Cyclone Armor magicarmor remains untouched at 148
|
|
// - Defender HP takes full 100 damage (1000 -> 900)
|
|
expect(defender.stateBus.getState('cyclonearmor')?.stats['magicarmor']).toBe(148)
|
|
expect(defender.statList.getHp256() >> 8).toBe(900)
|
|
expect(poisHit.totalDamage).toBe(100)
|
|
})
|
|
|
|
it('3.3 executeSUnitDmg: Physical damage completely bypasses Cyclone Armor', () => {
|
|
const attacker = createTestUnit('attacker_1', 'MeleeMonster', 'monster')
|
|
const defender = createTestUnit('defender_1', 'DruidDefender', 'player', 1000)
|
|
|
|
defender.stateBus.applyState({
|
|
stateNameOrId: 'cyclonearmor',
|
|
slvl: 10,
|
|
stats: { magicarmor: 148 },
|
|
})
|
|
|
|
// 100 Physical damage incoming
|
|
const physHit = executeSUnitDmg(attacker, defender, {
|
|
skillId: 0,
|
|
flatPhysMin256: 100 << 8,
|
|
flatPhysMax256: 100 << 8,
|
|
autoHit: true,
|
|
})
|
|
|
|
// Physical damage bypasses Cyclone Armor:
|
|
expect(defender.stateBus.getState('cyclonearmor')?.stats['magicarmor']).toBe(148)
|
|
expect(defender.statList.getHp256() >> 8).toBe(900)
|
|
expect(physHit.totalDamage).toBe(100)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 4: Tornado NextHitDelay
|
|
// =========================================================================
|
|
describe('Dimension 4: Tornado NextHitDelay', () => {
|
|
it('4.1 Tornado missile has NextHit = 1 and NextDelay = 25 in DataRegistry', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const mRec = reg.getMissileByName('tornado')
|
|
expect(mRec).toBeDefined()
|
|
expect(mRec!.nextHit).toBe(true)
|
|
expect(mRec!.nextDelay).toBe(25)
|
|
})
|
|
|
|
it('4.2 MissileEngine enforces 25-frame NextDelay window preventing multi-hits', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const engine = new MissileEngine(reg)
|
|
|
|
const caster = createTestUnit('dru_1', 'DruidCaster', 'player')
|
|
const target = createTestUnit('target_1', 'MonsterTarget', 'monster', 1000)
|
|
const targetPositions = new Map([['target_1', { x: 100, y: 100 }]])
|
|
|
|
// First tornado hits at tick 0
|
|
engine.spawnMissile({
|
|
missileNameOrId: 'tornado',
|
|
sourceSkillId: 245,
|
|
slvl: 1,
|
|
owner: caster,
|
|
startX: 100,
|
|
startY: 100,
|
|
targetX: 200,
|
|
targetY: 100,
|
|
dmgPacket: { skillId: 245, flatPhysMin256: 100 << 8, flatPhysMax256: 100 << 8 },
|
|
})
|
|
const hit1 = engine.tick(0, [target], targetPositions)
|
|
expect(hit1.hits.length).toBe(1)
|
|
expect(engine.isTargetInNextDelay('target_1', 0)).toBe(true)
|
|
expect(engine.isTargetInNextDelay('target_1', 24)).toBe(true)
|
|
expect(engine.isTargetInNextDelay('target_1', 25)).toBe(false)
|
|
|
|
// Second tornado inside immunity window (tick 10) must be blocked
|
|
engine.spawnMissile({
|
|
missileNameOrId: 'tornado',
|
|
sourceSkillId: 245,
|
|
slvl: 1,
|
|
owner: caster,
|
|
startX: 100,
|
|
startY: 100,
|
|
targetX: 200,
|
|
targetY: 100,
|
|
dmgPacket: { skillId: 245, flatPhysMin256: 100 << 8, flatPhysMax256: 100 << 8 },
|
|
})
|
|
const hit2 = engine.tick(10, [target], targetPositions)
|
|
expect(hit2.hits.length).toBe(0)
|
|
|
|
// Third tornado outside window (tick 26) must hit
|
|
engine.spawnMissile({
|
|
missileNameOrId: 'tornado',
|
|
sourceSkillId: 245,
|
|
slvl: 1,
|
|
owner: caster,
|
|
startX: 100,
|
|
startY: 100,
|
|
targetX: 200,
|
|
targetY: 100,
|
|
dmgPacket: { skillId: 245, flatPhysMin256: 100 << 8, flatPhysMax256: 100 << 8 },
|
|
})
|
|
const hit3 = engine.tick(26, [target], targetPositions)
|
|
expect(hit3.hits.length).toBe(1)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 5: Feral Rage & Maul Orbs
|
|
// =========================================================================
|
|
describe('Dimension 5: Feral Rage & Maul Orbs', () => {
|
|
it('5.1 Feral Rage formulas: 3 stages, 4*slvl% life leech, dm(10, 70, slvl) speed, 20.0s decay timer', () => {
|
|
const fr1 = calculateFeralRageStats(1)
|
|
expect(fr1.maxCharges).toBe(3)
|
|
expect(fr1.lifeLeechPct).toBe(4)
|
|
expect(fr1.velocityBonusPct).toBe(19)
|
|
expect(fr1.durationFrames).toBe(500)
|
|
expect(fr1.durationSeconds).toBe(20.0)
|
|
|
|
const fr10 = calculateFeralRageStats(10)
|
|
expect(fr10.lifeLeechPct).toBe(40)
|
|
expect(fr10.velocityBonusPct).toBe(51)
|
|
|
|
const fr20 = calculateFeralRageStats(20)
|
|
expect(fr20.lifeLeechPct).toBe(80)
|
|
expect(fr20.velocityBonusPct).toBe(60)
|
|
|
|
// Charge progression caps at 3
|
|
let charges = 0
|
|
charges = advanceChargeOrb(charges, 3)
|
|
expect(charges).toBe(1)
|
|
charges = advanceChargeOrb(charges, 3)
|
|
expect(charges).toBe(2)
|
|
charges = advanceChargeOrb(charges, 3)
|
|
expect(charges).toBe(3)
|
|
charges = advanceChargeOrb(charges, 3)
|
|
expect(charges).toBe(3)
|
|
})
|
|
|
|
it('5.2 Maul formulas: 5 stages, slvl*20% damage/charge (slvl*100% max), dm(10, 100, slvl) stun, 20.0s decay timer', () => {
|
|
const m1 = calculateMaulStats(1)
|
|
expect(m1.maxCharges).toBe(5)
|
|
expect(m1.damageBonusPerChargePct).toBe(20)
|
|
expect(m1.maxDamageBonusPct).toBe(100)
|
|
expect(m1.stunDurationFrames).toBe(24)
|
|
expect(m1.durationFrames).toBe(500)
|
|
expect(m1.durationSeconds).toBe(20.0)
|
|
|
|
const m10 = calculateMaulStats(10)
|
|
expect(m10.damageBonusPerChargePct).toBe(200)
|
|
expect(m10.maxDamageBonusPct).toBe(1000)
|
|
expect(m10.stunDurationFrames).toBe(71)
|
|
|
|
const m20 = calculateMaulStats(20)
|
|
expect(m20.damageBonusPerChargePct).toBe(400)
|
|
expect(m20.maxDamageBonusPct).toBe(2000)
|
|
expect(m20.stunDurationFrames).toBe(86)
|
|
})
|
|
|
|
it('5.3 [FAILING / BUG REPRO] executeSkill113c runtime attaches feralrage and maul states to caster on hit', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const caster = createTestUnit('dru_w', 'DruidWolf', 'player')
|
|
caster.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
|
|
const target = createTestUnit('m1', 'TargetMonster', 'monster', 1000)
|
|
|
|
const skill232 = reg.getSkillById(232)!
|
|
const eval232 = evaluateSkill113c({ registry: reg, skill: skill232, slvl: 10, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
|
|
const out232 = 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: 0,
|
|
targetX: 0,
|
|
targetY: 0,
|
|
})
|
|
|
|
// On successful hit, Feral Rage attaches feralrage charge state
|
|
expect(out232.statesApplied).toContain('feralrage')
|
|
expect(caster.stateBus.hasState('feralrage')).toBe(true)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 6: Rabies Contagion Mechanics
|
|
// =========================================================================
|
|
describe('Dimension 6: Rabies Contagion Mechanics', () => {
|
|
it('6.1 simulateRabiesContagion spreads infection to adjacent targets within 128px radius', () => {
|
|
const infected = [{ id: 'target_0', x: 100, y: 100 }]
|
|
const healthy = [
|
|
{ id: 'target_close_1', x: 120, y: 105 },
|
|
{ id: 'target_close_2', x: 80, y: 95 },
|
|
{ id: 'target_far', x: 400, y: 400 },
|
|
]
|
|
|
|
const newlyInfected = simulateRabiesContagion(infected, healthy, 128)
|
|
expect(newlyInfected).toContain('target_close_1')
|
|
expect(newlyInfected).toContain('target_close_2')
|
|
expect(newlyInfected).not.toContain('target_far')
|
|
})
|
|
|
|
it('6.2 Rabies formulas: 5-band poison damage, +18%/blvl Poison Creeper synergy, 4..8s duration', () => {
|
|
const r1 = calculateRabiesStats(1, 0)
|
|
expect(r1.minPoisonDamage).toBe(6)
|
|
expect(r1.maxPoisonDamage).toBe(14)
|
|
expect(r1.poisonDurationFrames).toBe(100)
|
|
expect(r1.poisonDurationSeconds).toBe(4.0)
|
|
|
|
const r20 = calculateRabiesStats(20, 20)
|
|
expect(r20.synergyBonusPct).toBe(360)
|
|
expect(r20.poisonDurationFrames).toBe(290) // 100 + 19*10
|
|
expect(r20.poisonDurationSeconds).toBe(11.6)
|
|
})
|
|
|
|
it('6.3 [FAILING / BUG REPRO] executeSkill113c runtime infects hit target with rabies state', async () => {
|
|
const reg = await getSharedDataRegistry()
|
|
const caster = createTestUnit('dru_w', 'DruidWolf', 'player')
|
|
caster.stateBus.applyState({ stateNameOrId: 'wolf', slvl: 10, stats: {} })
|
|
const target = createTestUnit('m1', 'TargetMonster', 'monster', 1000)
|
|
|
|
const skill238 = reg.getSkillById(238)!
|
|
const eval238 = evaluateSkill113c({ registry: reg, skill: skill238, slvl: 10, blvl: 10, statList: caster.statList, stateBus: caster.stateBus })
|
|
const out238 = 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,
|
|
})
|
|
|
|
// Hit target must receive rabies infection state
|
|
expect(target.stateBus.hasState('rabies')).toBe(true)
|
|
})
|
|
})
|
|
})
|