diablo2-web/tests/skills/pal/adv-pal-auras-stress.test.ts

381 lines
16 KiB
TypeScript

/**
* Adversarial Stress & Edge Case Verification Suite — Paladin Offensive Auras (10 Skills).
*
* 5 Critical Edge Scenarios:
* 1. Conviction Immunity-Breaking Thresholds (100%, 110%, 140%, 160% resistance; zero poison/magic pierce)
* 2. Sanctuary vs Physical Immune Undead (0% effective phys res on Undead; living/demons untouched)
* 3. Thorns Overkill & PvP 1/4th Penalty (10,000 dmg, monster mitigation, PvP 75% cut, 0 on immune)
* 4. Holy Freeze CBF Bypass & 2:1 Isometric Geometry (dx=100, dy=50 in range vs dx=100, dy=100 out)
* 5. Fanaticism Asymmetric Party Scaling & Rapid Aura Switching (1 Pal + 3 allies, 100 frame swap loop)
*/
import { describe, expect, it } from 'vitest'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { FIXED_ONE, UnitStatList } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
import { AuraScanner } from '../../../src/game/engine/aura-scanner.ts'
import {
computeEffectiveResistance,
executeSUnitDmg,
type CombatUnitContext,
} from '../../../src/game/engine/combat-pipeline.ts'
describe('Paladin Offensive Auras — Adversarial Stress & Edge Cases', () => {
// --------------------------------------------------------------------------
// Scenario 1: Conviction Immunity-Breaking Thresholds
// --------------------------------------------------------------------------
it('Scenario 1: Conviction breaking thresholds (100%, 110%, 140%, 160%) and Poison/Magic exclusion', async () => {
const registry = await getSharedDataRegistry()
const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const paladin: CombatUnitContext = {
id: 'paladin',
name: 'Paladin',
statList: palStats,
stateBus: new StateBus(palStats, registry),
unitType: 'player',
}
// Slvl 25 Conviction has -150% uncapped elemental resistance reduction
// Against immunities (baseRes >= 100%), applies at 1/5th effectiveness = -30%
const pierce25 = 150
// 1. Monster with 100% Fire Resistance
const res100 = computeEffectiveResistance({ baseRes: 100, convictionPierce: pierce25 })
expect(res100.isImmune).toBe(false)
expect(res100.effectiveRes).toBe(70) // 100 - 30 = 70%
// 2. Monster with 110% Lightning Resistance
const res110 = computeEffectiveResistance({ baseRes: 110, convictionPierce: pierce25 })
expect(res110.isImmune).toBe(false)
expect(res110.effectiveRes).toBe(80) // 110 - 30 = 80%
// 3. Monster with 140% Cold Resistance (Unbreakable!)
const res140 = computeEffectiveResistance({ baseRes: 140, convictionPierce: pierce25 })
expect(res140.isImmune).toBe(true)
expect(res140.effectiveRes).toBe(100) // 140 - 30 = 110% >= 100 -> Remains Immune!
// 4. Monster with 160% Fire Resistance (Unbreakable!)
const res160 = computeEffectiveResistance({ baseRes: 160, convictionPierce: pierce25 })
expect(res160.isImmune).toBe(true)
expect(res160.effectiveRes).toBe(100)
// 5. Conviction NEVER impacts Poison or Magic Resistance
const monsterStats = new UnitStatList(registry, {
maxhp: 1000 * FIXED_ONE,
hitpoints: 1000 * FIXED_ONE,
poisonresist: 50,
magicresist: 50,
})
const monsterBus = new StateBus(monsterStats, registry)
monsterBus.applyState({
stateNameOrId: 'conviction',
slvl: 25,
stats: { conviction_pierce: 150, item_armor_percent: -90 },
})
const monster: CombatUnitContext = {
id: 'monster_pois_mag',
name: 'Target',
statList: monsterStats,
stateBus: monsterBus,
unitType: 'monster',
}
// Poison attack with Conviction on defender
const poisOut = executeSUnitDmg(paladin, monster, {
skillId: 0,
attackKind: 'melee',
elemType: 'pois',
elemMin256: 100 * FIXED_ONE,
elemMax256: 100 * FIXED_ONE,
autoHit: true,
})
// 50% poison resistance applies in full -> 50 damage taken (NOT pierced by Conviction!)
expect(poisOut.elemDamage256).toBe(50 * FIXED_ONE)
// Magic attack with Conviction on defender
const magOut = executeSUnitDmg(paladin, monster, {
skillId: 0,
attackKind: 'spell',
elemType: 'mag',
elemMin256: 100 * FIXED_ONE,
elemMax256: 100 * FIXED_ONE,
autoHit: true,
})
// 50% magic resistance applies in full -> 50 damage taken (NOT pierced by Conviction!)
expect(magOut.elemDamage256).toBe(50 * FIXED_ONE)
})
// --------------------------------------------------------------------------
// Scenario 2: Sanctuary vs Physical Immune Undead
// --------------------------------------------------------------------------
it('Scenario 2: Sanctuary pierces 100% and 150% Physical Immune Undead while leaving living/demons immune', async () => {
const registry = await getSharedDataRegistry()
const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const palBus = new StateBus(palStats, registry)
palBus.applyState({ stateNameOrId: 'sanctuary', slvl: 10 }) // +420% ED vs Undead
const paladin: CombatUnitContext = {
id: 'paladin',
name: 'Paladin',
statList: palStats,
stateBus: palBus,
unitType: 'player',
weaponMinPhys: 100,
weaponMaxPhys: 100,
}
// 1. Ghost (100% Physical Immune Undead)
const ghostStats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, damageresist: 100 })
const ghost: CombatUnitContext = {
id: 'ghost_100',
name: 'Ghost (100% Phys Res)',
statList: ghostStats,
stateBus: new StateBus(ghostStats, registry),
unitType: 'monster',
isUndead: true,
}
const outGhost100 = executeSUnitDmg(paladin, ghost, { skillId: 0, attackKind: 'melee', autoHit: true })
expect(outGhost100.immuneToPhys).toBe(false)
// 100 base * (100 + 420)% = 520 physical damage dealt with 0% resistance!
expect(outGhost100.physDamage256).toBe(520 * FIXED_ONE)
// 2. Stone Skin Ghost (150% Physical Immune Undead)
const stoneSkinGhostStats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, damageresist: 150 })
const stoneSkinGhost: CombatUnitContext = {
id: 'ghost_150',
name: 'Stone Skin Ghost (150% Phys Res)',
statList: stoneSkinGhostStats,
stateBus: new StateBus(stoneSkinGhostStats, registry),
unitType: 'monster',
isUndead: true,
}
const outGhost150 = executeSUnitDmg(paladin, stoneSkinGhost, { skillId: 0, attackKind: 'melee', autoHit: true })
expect(outGhost150.immuneToPhys).toBe(false)
expect(outGhost150.physDamage256).toBe(520 * FIXED_ONE)
// 3. Stone Skin Demon (100% Phys Res, isDemon: true, isUndead: false) -> Sanctuary does NOT pierce!
const demonStats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, damageresist: 100 })
const demon: CombatUnitContext = {
id: 'demon_100',
name: 'Demon (100% Phys Res)',
statList: demonStats,
stateBus: new StateBus(demonStats, registry),
unitType: 'monster',
isDemon: true,
isUndead: false,
}
const outDemon = executeSUnitDmg(paladin, demon, { skillId: 0, attackKind: 'melee', autoHit: true })
expect(outDemon.immuneToPhys).toBe(true)
expect(outDemon.physDamage256).toBe(0)
})
// --------------------------------------------------------------------------
// Scenario 3: Thorns Overkill & PvP 1/4th Penalty
// --------------------------------------------------------------------------
it('Scenario 3: Thorns massive overkill reflection, attacker physical resistance, and PvP 1/4th penalty', async () => {
const registry = await getSharedDataRegistry()
// Defender with Slvl 20 Thorns (1010% reflection)
const defStats = new UnitStatList(registry, { maxhp: 50000 * FIXED_ONE, hitpoints: 50000 * FIXED_ONE, damageresist: 0 })
const defBus = new StateBus(defStats, registry)
defBus.applyState({ stateNameOrId: 'thorns', slvl: 20 })
const paladinDef: CombatUnitContext = {
id: 'paladin_thorns',
name: 'Thorns Paladin',
statList: defStats,
stateBus: defBus,
unitType: 'player',
}
// 1. Monster attacks for 10,000 melee physical damage
const atkStats = new UnitStatList(registry, { maxhp: 200000 * FIXED_ONE, hitpoints: 200000 * FIXED_ONE, damageresist: 0 })
const monsterAtk: CombatUnitContext = {
id: 'massive_monster',
name: 'Massive Monster',
statList: atkStats,
stateBus: new StateBus(atkStats, registry),
unitType: 'monster',
weaponMinPhys: 10000,
weaponMaxPhys: 10000,
}
const outMassive = executeSUnitDmg(monsterAtk, paladinDef, { skillId: 0, attackKind: 'melee', autoHit: true })
// 10,000 * 1010% = 101,000 physical damage reflected!
expect(outMassive.reflectedPhys256).toBe(101000 * FIXED_ONE)
expect(monsterAtk.statList.getHp256()).toBe((200000 - 101000) * FIXED_ONE)
// 2. PvP Penalty: Player attacker attacks for 10,000 damage
const playerAtkStats = new UnitStatList(registry, { maxhp: 200000 * FIXED_ONE, hitpoints: 200000 * FIXED_ONE, damageresist: 0 })
const playerAtk: CombatUnitContext = {
id: 'player_atk',
name: 'Player Attacker',
statList: playerAtkStats,
stateBus: new StateBus(playerAtkStats, registry),
unitType: 'player',
weaponMinPhys: 10000,
weaponMaxPhys: 10000,
}
const outPvP = executeSUnitDmg(playerAtk, paladinDef, { skillId: 0, attackKind: 'melee', autoHit: true })
// In PvP, 1/4th penalty applies: Math.trunc(101,000 / 4) = 25,250 reflected!
expect(outPvP.reflectedPhys256).toBe(25250 * FIXED_ONE)
// 3. Physical Immune Attacker: Takes strictly 0 reflected damage!
const immuneAtkStats = new UnitStatList(registry, { maxhp: 50000 * FIXED_ONE, hitpoints: 50000 * FIXED_ONE, damageresist: 100 })
const immuneAtk: CombatUnitContext = {
id: 'immune_atk',
name: 'Physical Immune Attacker',
statList: immuneAtkStats,
stateBus: new StateBus(immuneAtkStats, registry),
unitType: 'monster',
weaponMinPhys: 1000,
weaponMaxPhys: 1000,
}
const outImmune = executeSUnitDmg(immuneAtk, paladinDef, { skillId: 0, attackKind: 'melee', autoHit: true })
expect(outImmune.reflectedPhys256).toBeUndefined()
expect(immuneAtk.statList.getHp256()).toBe(50000 * FIXED_ONE)
})
// --------------------------------------------------------------------------
// Scenario 4: Holy Freeze CBF Bypass & 2:1 Isometric Geometry
// --------------------------------------------------------------------------
it('Scenario 4: Holy Freeze CBF bypass and 2:1 isometric ground space range check', async () => {
const registry = await getSharedDataRegistry()
const scanner = new AuraScanner(registry)
// Paladin at origin (0, 0)
const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const paladin: CombatUnitContext = {
id: 'paladin',
name: 'Paladin',
statList: palStats,
stateBus: new StateBus(palStats, registry),
unitType: 'player',
x: 0,
y: 0,
}
// Enemy 1: In range in 2:1 iso ground space: (dx=100, dy=50)
// Math.hypot(100, 50 * 2) = Math.hypot(100, 100) = 141.42 <= 200
const e1Stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE, cannot_be_frozen: 1 })
const enemy1: CombatUnitContext = {
id: 'enemy_in_range',
name: 'In Range CBF Enemy',
statList: e1Stats,
stateBus: new StateBus(e1Stats, registry),
unitType: 'monster',
x: 100,
y: 50,
}
// Enemy 2: Out of range in 2:1 iso ground space: (dx=100, dy=100)
// Math.hypot(100, 100 * 2) = Math.hypot(100, 200) = 223.6 > 200
const e2Stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE, cannot_be_frozen: 1 })
const enemy2: CombatUnitContext = {
id: 'enemy_out_of_range',
name: 'Out of Range CBF Enemy',
statList: e2Stats,
stateBus: new StateBus(e2Stats, registry),
unitType: 'monster',
x: 100,
y: 100,
}
// Slvl 10 Holy Freeze: radiusSubtiles = 15 -> radiusPx = Math.round(15 * 40/3) = 200 px
const auraSource = scanner.setActiveAura(paladin, 114, 10)!
expect(auraSource).toBeDefined()
scanner.pulseAura(auraSource, 0, [paladin], [enemy1, enemy2])
// Enemy 1 is in range and slowed (bypassing CBF!)
expect(enemy1.stateBus.hasState('holywindcold')).toBe(true)
expect(enemy1.statList.getModifierBonus('item_slow')).toBe(46)
// Enemy 2 is out of range and NOT affected
expect(enemy2.stateBus.hasState('holywindcold')).toBe(false)
expect(enemy2.statList.getModifierBonus('item_slow')).toBe(0)
})
// --------------------------------------------------------------------------
// Scenario 5: Fanaticism Asymmetric Party Scaling & Rapid Aura Switching
// --------------------------------------------------------------------------
it('Scenario 5: Fanaticism asymmetric party scaling and rapid aura switching stability', async () => {
const registry = await getSharedDataRegistry()
const scanner = new AuraScanner(registry)
// Paladin Caster
const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const paladin: CombatUnitContext = {
id: 'paladin',
name: 'Paladin',
statList: palStats,
stateBus: new StateBus(palStats, registry),
unitType: 'player',
weaponMinPhys: 100,
weaponMaxPhys: 100,
}
// 3 Allies
const allies: CombatUnitContext[] = [1, 2, 3].map(i => {
const s = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
return {
id: `ally_${i}`,
name: `Ally ${i}`,
statList: s,
stateBus: new StateBus(s, registry),
unitType: 'party',
weaponMinPhys: 100,
weaponMaxPhys: 100,
}
})
// Activate Slvl 20 Fanaticism (Caster: 373% ED, Allies: 186% ED)
const fanatAura = scanner.setActiveAura(paladin, 122, 20)!
scanner.pulseAura(fanatAura, 0, [paladin, ...allies], [])
// Asserts Paladin receives full %ED (373%) and allies receive exactly half %ED (186%)
expect(paladin.stateBus.hasState('fanaticism')).toBe(true)
for (const ally of allies) {
expect(ally.stateBus.hasState('fanaticism')).toBe(true)
expect(ally.statList.getModifierBonus('damagepercent')).toBe(186)
}
// Simulate switching between Fanaticism (122), Conviction (123), and Might (98) over 100 consecutive frames
const auraCycle = [122, 123, 98]
for (let frame = 1; frame <= 100; frame++) {
const nextAuraId = auraCycle[frame % 3]!
scanner.clearUnitAuras(paladin.id)
paladin.stateBus.removeState('fanaticism')
paladin.stateBus.removeState('conviction')
paladin.stateBus.removeState('might')
for (const ally of allies) {
ally.stateBus.removeState('fanaticism')
ally.stateBus.removeState('conviction')
ally.stateBus.removeState('might')
}
const active = scanner.setActiveAura(paladin, nextAuraId, 10, frame)!
scanner.pulseAura(active, frame, [paladin, ...allies], [])
}
// Final state check: last frame (100 % 3 = 1 -> Conviction 123)
// Paladin must have active Conviction visual without debuff
expect(paladin.stateBus.hasState('conviction')).toBe(true)
expect(paladin.statList.getModifierBonus('item_armor_percent')).toBe(0)
expect(paladin.statList.getModifierBonus('conviction_pierce')).toBe(0)
// No lingering Fanaticism or Might modifiers remain
expect(paladin.stateBus.hasState('fanaticism')).toBe(false)
expect(paladin.stateBus.hasState('might')).toBe(false)
for (const ally of allies) {
expect(ally.stateBus.hasState('fanaticism')).toBe(false)
expect(ally.stateBus.hasState('might')).toBe(false)
expect(ally.statList.getModifierBonus('damagepercent')).toBe(0)
}
})
})