diablo2-web/tests/skills/pal/adv-pal-challenger-iter2-st...

696 lines
26 KiB
TypeScript

/**
* Diablo II: Lord of Destruction v1.13c — Adversarial Challenger Verification Suite: Iteration 2
* Focus: Paladin Skills (IDs 96..125) Mechanics Remediation & Ground Truth Verification
*
* Adversarially probes:
* 1. Smite Equipment Leech Suppression:
* - Gear life leech (lifesteal, life_leech, item_parasite, life_leech_pct) is strictly 0.
* - Gear mana leech (manasteal, mana_leech, item_parasitemana, mana_leech_pct) is strictly 0.
* - Contrast: non-smite physical melee attacks correctly leech life and mana.
* 2. Life Tap Curse Healing Preservation:
* - Smite against a target cursed with Life Tap restores exactly 50% of dealt physical damage.
* - Smite with BOTH Life Tap on defender AND 50% gear leech on attacker heals strictly 50% (not 100%).
* - Smite against Physical Immune target with Life Tap heals strictly 0 HP.
* - Smite against 50% Physical Resistance target heals 50% of the mitigated damage.
* 3. Holy Shield Blocking Chance Parity:
* - dm56 formula parity across all levels 1..50: 10 + Math.floor((3300 * lvl) / (100 * (lvl + 6))).
* - slvl 1 evaluates to exactly 14% (not 13%).
* - slvl 20 evaluates to exactly 35% (not 36%).
* - Runtime state application toblock bonus matches across slvls.
* 4. SrvSt Pre-Cast Equipment Guards:
* - hasShield === false rejects Smite (#97) and Holy Shield (#117) with 0 mana spent.
* - weaponItemType === 'bow' or 'crossbow' or isRanged === true rejects Sacrifice (#96) and Zeal (#106).
* - undefined/omitted equipment properties retain permissive fallback for minimal fixtures.
*/
import { describe, expect, it } from 'vitest'
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 {
executeSUnitDmg,
type CombatUnitContext,
} from '../../../src/game/engine/combat-pipeline.ts'
import {
calculateHolyShieldStats,
validatePaladinSkillEquipment,
} from '../../../src/game/skills/paladin-combat.ts'
import { computeDiminishingReturns } from '../../../src/game/engine/calc-ast.ts'
import { executeSkillCore113c, evaluateSkill113c } from '../../../src/game/skills/registry.ts'
import { MissileEngine } from '../../../src/game/engine/missile-engine.ts'
import { AuraScanner } from '../../../src/game/engine/aura-scanner.ts'
import { SummonManager } from '../../../src/game/engine/summon-manager.ts'
import type { SkillExecContext } from '../../../src/game/skills/types.ts'
function createExecContext(
registry: any,
skillId: number,
slvl: number,
caster: CombatUnitContext,
targets: CombatUnitContext[] = [],
): SkillExecContext {
const skill = registry.getSkillById(skillId)!
const evalResult = evaluateSkill113c({
registry,
skill,
slvl,
blvl: slvl,
statList: caster.statList,
})
return {
registry,
skill,
evalResult,
caster,
targets,
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 0,
targetX: 0,
targetY: 0,
}
}
describe('Adversarial Challenger Verification Suite — Paladin Iteration 2', () => {
// ==========================================================================
// Pillar 1: Smite Equipment Leech Suppression
// ==========================================================================
describe('Pillar 1: Smite Equipment Leech Suppression', () => {
it('suppresses all gear life leech and mana leech keys during Smite physical attack', async () => {
const registry = await getSharedDataRegistry()
const initialHp = 500 * FIXED_ONE
const initialMana = 300 * FIXED_ONE
// Test multiple leech keys simultaneously
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 2000 * FIXED_ONE,
mana: initialMana,
maxmana: 1000 * FIXED_ONE,
lifesteal: 20,
life_leech: 15,
item_parasite: 10,
manasteal: 25,
mana_leech: 10,
item_parasitemana: 15,
})
const attacker: CombatUnitContext = {
id: 'pal_smiter',
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
hasShield: true,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 50000 * FIXED_ONE,
maxhp: 50000 * FIXED_ONE,
damageresist: 0,
})
const defender: CombatUnitContext = {
id: 'target_dummy',
name: 'Target Dummy',
statList: defenderStats,
stateBus: new StateBus(defenderStats, registry),
unitType: 'monster',
}
// Smite attack dealing 1000 physical damage (srcDam: 0 for weapon physical damage)
const out = executeSUnitDmg(attacker, defender, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 1000 * FIXED_ONE,
flatPhysMax256: 1000 * FIXED_ONE,
autoHit: true,
unblockable: true,
})
expect(out.hit).toBe(true)
expect(out.physDamage256).toBe(1000 * FIXED_ONE)
expect(out.attackerHealed256 ?? 0).toBe(0)
expect(out.attackerManaLeeched256 ?? 0).toBe(0)
expect(attackerStats.getHp256()).toBe(initialHp)
expect(attackerStats.getMana256()).toBe(initialMana)
})
it('contrast test: regular non-Smite attack leeches life and mana normally with same gear', async () => {
const registry = await getSharedDataRegistry()
const initialHp = 500 * FIXED_ONE
const initialMana = 300 * FIXED_ONE
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 2000 * FIXED_ONE,
mana: initialMana,
maxmana: 1000 * FIXED_ONE,
lifesteal: 20, // 20% life leech
manasteal: 10, // 10% mana leech
})
const attacker: CombatUnitContext = {
id: 'pal_standard_attacker',
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
weaponMinPhys: 0,
weaponMaxPhys: 0,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 50000 * FIXED_ONE,
maxhp: 50000 * FIXED_ONE,
damageresist: 0,
})
const defender: CombatUnitContext = {
id: 'target_dummy',
name: 'Target Dummy',
statList: defenderStats,
stateBus: new StateBus(defenderStats, registry),
unitType: 'monster',
}
// Standard melee attack dealing 500 physical damage
const out = executeSUnitDmg(attacker, defender, {
skillId: 0, // Standard Attack
attackKind: 'melee',
srcDam: 128,
flatPhysMin256: 500 * FIXED_ONE,
flatPhysMax256: 500 * FIXED_ONE,
autoHit: true,
})
expect(out.hit).toBe(true)
expect(out.physDamage256).toBe(500 * FIXED_ONE)
// 20% of 500 = 100 HP healed
expect(out.attackerHealed256).toBe(100 * FIXED_ONE)
// 10% of 500 = 50 Mana leeched
expect(out.attackerManaLeeched256).toBe(50 * FIXED_ONE)
expect(attackerStats.getHp256()).toBe(initialHp + 100 * FIXED_ONE)
expect(attackerStats.getMana256()).toBe(initialMana + 50 * FIXED_ONE)
})
it('suppresses gear leech across an adversarial sweep of high leech percentages (1% to 200%)', async () => {
const registry = await getSharedDataRegistry()
const testPcts = [1, 5, 10, 25, 50, 100, 200]
for (const pct of testPcts) {
const initialHp = 1000 * FIXED_ONE
const initialMana = 500 * FIXED_ONE
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 10000 * FIXED_ONE,
mana: initialMana,
maxmana: 5000 * FIXED_ONE,
lifesteal: pct,
manasteal: pct,
})
const attacker: CombatUnitContext = {
id: `pal_sweep_${pct}`,
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
hasShield: true,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 20000 * FIXED_ONE,
maxhp: 20000 * FIXED_ONE,
damageresist: 0,
})
const defender: CombatUnitContext = {
id: `dummy_${pct}`,
name: 'Dummy',
statList: defenderStats,
stateBus: new StateBus(defenderStats, registry),
unitType: 'monster',
}
const out = executeSUnitDmg(attacker, defender, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 800 * FIXED_ONE,
flatPhysMax256: 800 * FIXED_ONE,
autoHit: true,
})
expect(out.hit).toBe(true)
expect(out.attackerHealed256 ?? 0).toBe(0)
expect(out.attackerManaLeeched256 ?? 0).toBe(0)
expect(attackerStats.getHp256()).toBe(initialHp)
expect(attackerStats.getMana256()).toBe(initialMana)
}
})
})
// ==========================================================================
// Pillar 2: Life Tap Curse Healing Preservation on Smite
// ==========================================================================
describe('Pillar 2: Life Tap Curse Healing Preservation on Smite', () => {
it('restores exactly 50% of physical damage dealt as HP when defender is cursed with Life Tap', async () => {
const registry = await getSharedDataRegistry()
const initialHp = 300 * FIXED_ONE
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 2000 * FIXED_ONE,
})
const attacker: CombatUnitContext = {
id: 'pal_smiter_lt',
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
hasShield: true,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 10000 * FIXED_ONE,
maxhp: 10000 * FIXED_ONE,
damageresist: 0,
})
const defenderBus = new StateBus(defenderStats, registry)
defenderBus.applyState({ stateNameOrId: 'lifetap', slvl: 1, durationFrames: 500 })
const defender: CombatUnitContext = {
id: 'dummy_lt',
name: 'Dummy',
statList: defenderStats,
stateBus: defenderBus,
unitType: 'monster',
}
const out = executeSUnitDmg(attacker, defender, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 600 * FIXED_ONE,
flatPhysMax256: 600 * FIXED_ONE,
autoHit: true,
unblockable: true,
})
expect(out.hit).toBe(true)
expect(out.physDamage256).toBe(600 * FIXED_ONE)
// 50% of 600 = 300 HP healed
expect(out.attackerHealed256).toBe(300 * FIXED_ONE)
expect(attackerStats.getHp256()).toBe(initialHp + 300 * FIXED_ONE)
})
it('when attacker has BOTH gear leech (50%) AND target has Life Tap, heals strictly 50% from Life Tap (gear leech is 0)', async () => {
const registry = await getSharedDataRegistry()
const initialHp = 400 * FIXED_ONE
const initialMana = 200 * FIXED_ONE
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 3000 * FIXED_ONE,
mana: initialMana,
maxmana: 1000 * FIXED_ONE,
lifesteal: 50, // 50% gear life leech
manasteal: 50, // 50% gear mana leech
})
const attacker: CombatUnitContext = {
id: 'pal_smiter_both',
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
hasShield: true,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 10000 * FIXED_ONE,
maxhp: 10000 * FIXED_ONE,
damageresist: 0,
})
const defenderBus = new StateBus(defenderStats, registry)
defenderBus.applyState({ stateNameOrId: 'lifetap', slvl: 5, durationFrames: 500 })
const defender: CombatUnitContext = {
id: 'dummy_both',
name: 'Dummy',
statList: defenderStats,
stateBus: defenderBus,
unitType: 'monster',
}
const out = executeSUnitDmg(attacker, defender, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 800 * FIXED_ONE,
flatPhysMax256: 800 * FIXED_ONE,
autoHit: true,
})
expect(out.hit).toBe(true)
expect(out.physDamage256).toBe(800 * FIXED_ONE)
// Must heal ONLY 50% (400), NOT 50% + 50% (800)
expect(out.attackerHealed256).toBe(400 * FIXED_ONE)
// Mana leech must remain strictly 0
expect(out.attackerManaLeeched256 ?? 0).toBe(0)
expect(attackerStats.getHp256()).toBe(initialHp + 400 * FIXED_ONE)
expect(attackerStats.getMana256()).toBe(initialMana)
})
it('heals 0 HP when striking a Physical Immune target cursed with Life Tap (damage dealt = 0)', async () => {
const registry = await getSharedDataRegistry()
const initialHp = 500 * FIXED_ONE
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 2000 * FIXED_ONE,
})
const attacker: CombatUnitContext = {
id: 'pal_smiter_pi',
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
hasShield: true,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 10000 * FIXED_ONE,
maxhp: 10000 * FIXED_ONE,
damageresist: 100, // Physical Immune
})
const defenderBus = new StateBus(defenderStats, registry)
defenderBus.applyState({ stateNameOrId: 'lifetap', slvl: 1, durationFrames: 500 })
const defender: CombatUnitContext = {
id: 'dummy_pi',
name: 'Dummy PI',
statList: defenderStats,
stateBus: defenderBus,
unitType: 'monster',
}
const out = executeSUnitDmg(attacker, defender, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 1000 * FIXED_ONE,
flatPhysMax256: 1000 * FIXED_ONE,
autoHit: true,
})
expect(out.hit).toBe(true)
expect(out.physDamage256).toBe(0)
expect(out.immuneToPhys).toBe(true)
expect(out.attackerHealed256 ?? 0).toBe(0)
expect(attackerStats.getHp256()).toBe(initialHp)
})
it('heals exactly 50% of reduced physical damage when target has 50% Physical Resistance', async () => {
const registry = await getSharedDataRegistry()
const initialHp = 500 * FIXED_ONE
const attackerStats = new UnitStatList(registry, {
hitpoints: initialHp,
maxhp: 2000 * FIXED_ONE,
})
const attacker: CombatUnitContext = {
id: 'pal_smiter_res',
name: 'Paladin',
statList: attackerStats,
stateBus: new StateBus(attackerStats, registry),
unitType: 'player',
hasShield: true,
}
const defenderStats = new UnitStatList(registry, {
hitpoints: 10000 * FIXED_ONE,
maxhp: 10000 * FIXED_ONE,
damageresist: 50, // 50% Physical Resistance
})
const defenderBus = new StateBus(defenderStats, registry)
defenderBus.applyState({ stateNameOrId: 'lifetap', slvl: 1, durationFrames: 500 })
const defender: CombatUnitContext = {
id: 'dummy_res',
name: 'Dummy 50% DR',
statList: defenderStats,
stateBus: defenderBus,
unitType: 'monster',
}
const out = executeSUnitDmg(attacker, defender, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 1000 * FIXED_ONE,
flatPhysMax256: 1000 * FIXED_ONE,
autoHit: true,
})
expect(out.hit).toBe(true)
// 1000 raw -> 500 final phys damage
expect(out.physDamage256).toBe(500 * FIXED_ONE)
// 50% of 500 = 250 HP healed
expect(out.attackerHealed256).toBe(250 * FIXED_ONE)
expect(attackerStats.getHp256()).toBe(initialHp + 250 * FIXED_ONE)
})
})
// ==========================================================================
// Pillar 3: Holy Shield Blocking Chance Parity (dm56)
// ==========================================================================
describe('Pillar 3: Holy Shield Blocking Chance Parity (dm56)', () => {
// Exact D2 1.13c dm56 oracle
function dm56Oracle(lvl: number): number {
const l = Math.max(1, lvl)
return 10 + Math.floor((3300 * l) / (100 * (l + 6)))
}
it('verifies 100% exact parity with dm56 oracle across slvls 1 through 50', () => {
const mismatches: { slvl: number; hs: number; oracle: number }[] = []
for (let slvl = 1; slvl <= 50; slvl++) {
const hs = calculateHolyShieldStats(slvl).blockChancePct
const oracle = dm56Oracle(slvl)
const calcAst = computeDiminishingReturns(10, 40, slvl)
if (hs !== oracle || hs !== calcAst) {
mismatches.push({ slvl, hs, oracle })
}
}
expect(mismatches).toEqual([])
})
it('specifically validates critical anchor levels: slvl 1 = 14% and slvl 20 = 35%', () => {
// slvl 1: 10 + Math.floor(3300*1 / 700) = 10 + 4 = 14% (NOT 13%)
expect(calculateHolyShieldStats(1).blockChancePct).toBe(14)
// slvl 20: 10 + Math.floor(3300*20 / 2600) = 10 + Math.floor(66000 / 2600) = 10 + 25 = 35% (NOT 36%)
expect(calculateHolyShieldStats(20).blockChancePct).toBe(35)
})
it('verifies runtime executeSkillCore113c applies correct toblock stat to caster', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, {})
const casterBus = new StateBus(stats, registry)
const caster: CombatUnitContext = {
id: 'hs_pal',
name: 'Paladin',
statList: stats,
stateBus: casterBus,
unitType: 'player',
hasShield: true,
}
// Cast Holy Shield slvl 1
const ctx1 = createExecContext(registry, 117, 1, caster)
const res1 = executeSkillCore113c(ctx1)
expect(res1.executed).toBe(true)
expect(stats.getModifierBonus('toblock')).toBe(14)
// Cast Holy Shield slvl 20
const ctx20 = createExecContext(registry, 117, 20, caster)
const res20 = executeSkillCore113c(ctx20)
expect(res20.executed).toBe(true)
expect(stats.getModifierBonus('toblock')).toBe(35)
})
})
// ==========================================================================
// Pillar 4: SrvSt Pre-Cast Equipment Guards & Tri-State Semantics
// ==========================================================================
describe('Pillar 4: SrvSt Pre-Cast Equipment Guards & Tri-State Semantics', () => {
it('Smite (#97) and Holy Shield (#117) fail fast when hasShield === false', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { mana: 500 * FIXED_ONE, maxmana: 500 * FIXED_ONE })
const unshieldedCaster: CombatUnitContext = {
id: 'pal_no_shield',
name: 'Paladin',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'player',
hasShield: false,
}
// 1. Holy Shield (#117)
const hsCtx = createExecContext(registry, 117, 1, unshieldedCaster)
const hsRes = executeSkillCore113c(hsCtx)
expect(hsRes.executed).toBe(false)
expect(hsRes.manaSpent256).toBe(0)
expect(hsRes.notes).toContain('fail: requires equipped shield')
// 2. Smite (#97) via executeSkillCore113c
const smiteCtx = createExecContext(registry, 97, 1, unshieldedCaster)
const smiteCoreRes = executeSkillCore113c(smiteCtx)
expect(smiteCoreRes.executed).toBe(false)
expect(smiteCoreRes.manaSpent256).toBe(0)
expect(smiteCoreRes.notes).toContain('fail: requires equipped shield')
// 3. Smite (#97) via executeSUnitDmg
const dummyDef: CombatUnitContext = {
id: 'dummy',
name: 'Dummy',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'monster',
}
const smiteDmgRes = executeSUnitDmg(unshieldedCaster, dummyDef, {
skillId: 97,
attackKind: 'melee',
srcDam: 0,
flatPhysMin256: 100 * FIXED_ONE,
flatPhysMax256: 100 * FIXED_ONE,
})
expect(smiteDmgRes.hit).toBe(false)
expect(smiteDmgRes.avoidedReason).toBe('miss')
expect(smiteDmgRes.totalDamage256).toBe(0)
})
it('Sacrifice (#96) and Zeal (#106) fail fast when weaponItemType is bow or crossbow or isRanged is true', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { mana: 500 * FIXED_ONE, maxmana: 500 * FIXED_ONE })
const dummyDef: CombatUnitContext = {
id: 'dummy',
name: 'Dummy',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'monster',
}
const invalidRangedAttackers: CombatUnitContext[] = [
{
id: 'bow_pal',
name: 'Paladin Bow',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'player',
weaponItemType: 'bow',
},
{
id: 'xbow_pal',
name: 'Paladin Xbow',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'player',
weaponItemType: 'crossbow',
},
{
id: 'ranged_pal',
name: 'Paladin Ranged Flag',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'player',
isRanged: true,
},
]
for (const attacker of invalidRangedAttackers) {
// Sacrifice (#96) in registry
const sacCtx = createExecContext(registry, 96, 1, attacker)
const sacRes = executeSkillCore113c(sacCtx)
expect(sacRes.executed).toBe(false)
expect(sacRes.manaSpent256).toBe(0)
expect(sacRes.notes).toContain('fail: requires melee weapon')
// Zeal (#106) in registry
const zealCtx = createExecContext(registry, 106, 1, attacker)
const zealRes = executeSkillCore113c(zealCtx)
expect(zealRes.executed).toBe(false)
expect(zealRes.manaSpent256).toBe(0)
expect(zealRes.notes).toContain('fail: requires melee weapon')
// Sacrifice (#96) in combat pipeline
const sacDmgRes = executeSUnitDmg(attacker, dummyDef, {
skillId: 96,
attackKind: 'melee',
srcDam: 128,
flatPhysMin256: 100 * FIXED_ONE,
flatPhysMax256: 100 * FIXED_ONE,
})
expect(sacDmgRes.hit).toBe(false)
expect(sacDmgRes.avoidedReason).toBe('miss')
// Zeal (#106) in combat pipeline
const zealDmgRes = executeSUnitDmg(attacker, dummyDef, {
skillId: 106,
attackKind: 'melee',
srcDam: 128,
flatPhysMin256: 100 * FIXED_ONE,
flatPhysMax256: 100 * FIXED_ONE,
})
expect(zealDmgRes.hit).toBe(false)
expect(zealDmgRes.avoidedReason).toBe('miss')
}
})
it('permissive fallback: undefined/omitted properties permit execution for legacy test fixtures', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { mana: 500 * FIXED_ONE, maxmana: 500 * FIXED_ONE })
const bareAttacker: CombatUnitContext = {
id: 'bare_pal',
name: 'Paladin Bare',
statList: stats,
stateBus: new StateBus(stats, registry),
unitType: 'player',
// hasShield is undefined, weaponItemType is undefined
}
// Smite (#97) executes when hasShield is undefined
const smiteCtx = createExecContext(registry, 97, 1, bareAttacker)
const smiteRes = executeSkillCore113c(smiteCtx)
expect(smiteRes.executed).toBe(true)
// Holy Shield (#117) executes when hasShield is undefined
const hsCtx = createExecContext(registry, 117, 1, bareAttacker)
const hsRes = executeSkillCore113c(hsCtx)
expect(hsRes.executed).toBe(true)
// Sacrifice (#96) executes when weaponItemType is undefined
const sacCtx = createExecContext(registry, 96, 1, bareAttacker)
const sacRes = executeSkillCore113c(sacCtx)
expect(sacRes.executed).toBe(true)
// Zeal (#106) executes when weaponItemType is undefined
const zealCtx = createExecContext(registry, 106, 1, bareAttacker)
const zealRes = executeSkillCore113c(zealCtx)
expect(zealRes.executed).toBe(true)
// validatePaladinSkillEquipment helper truth table
expect(validatePaladinSkillEquipment(97, undefined).valid).toBe(true)
expect(validatePaladinSkillEquipment(117, undefined).valid).toBe(true)
expect(validatePaladinSkillEquipment(96, undefined).valid).toBe(true)
expect(validatePaladinSkillEquipment(106, undefined).valid).toBe(true)
expect(validatePaladinSkillEquipment(97, { hasShield: true }).valid).toBe(true)
expect(validatePaladinSkillEquipment(97, { hasShield: false }).valid).toBe(false)
expect(validatePaladinSkillEquipment(117, { hasShield: true }).valid).toBe(true)
expect(validatePaladinSkillEquipment(117, { hasShield: false }).valid).toBe(false)
expect(validatePaladinSkillEquipment(96, { weaponItemType: 'sword' }).valid).toBe(true)
expect(validatePaladinSkillEquipment(96, { weaponItemType: 'bow' }).valid).toBe(false)
expect(validatePaladinSkillEquipment(106, { weaponItemType: 'axe' }).valid).toBe(true)
expect(validatePaladinSkillEquipment(106, { weaponItemType: 'bow' }).valid).toBe(false)
})
})
})