/** * Adversarial Stress Testing & Parity Verification Suite for Cohort 5: * Barbarian Combat Skills & Combat Masteries (1.13c Ground Truth) * * Focus Areas: * 1. Whirlwind 1.13c Hit Cadence (4th and 8th frame unconditional, IAS/WSM breakpoints, dual-wield) * 2. Frenzy Speed Stacking & Decay (sequential hits, 150-frame duration, expiration reset, zero-stack edge case) * 3. Berserk Magic Conversion & Zero Defense (100% conversion, physical immunity bypass, magic resistance, zero defense/block) * 4. Double Swing Mana Cost Progression (descending cost, 0 at slvl 9+, extreme slvl bounds) * 5. Combat Masteries Diminishing Returns (Critical Strike dm56, Natural Resistance dm12, Iron Skin ln12 linear verification) * 6. Pre-Cast Equipment Checks (melee weapon check, dual-wield check, runtime execution rejection) */ import { describe, expect, it } from 'vitest' import { calculateWhirlwindHitFrames, calculateWhirlwindStats, calculateFrenzyStats, calculateFrenzyStackState, calculateBerserkStats, calculateDoubleSwingStats, validateBarbarianSkillEquipment, } from '../../../src/server/skills/barbarian-combat.ts' import { calculateSwordMasteryStats, calculateAxeMasteryStats, calculateMaceMasteryStats, calculatePolearmMasteryStats, calculateThrowingMasteryStats, calculateSpearMasteryStats, calculateIronSkinStats, calculateNaturalResistanceStats, calculateIncreasedSpeedStats, } from '../../../src/server/skills/barbarian-masteries.ts' import { computeDiminishingReturns } from '../../../src/common/data/calc-ast.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { UnitStatList, FIXED_ONE } from '../../../src/server/engine/stat-list.ts' import { StateBus } from '../../../src/server/engine/state-bus.ts' import { executeSUnitDmg, computeToHitChance, evaluateAvoidanceAndBlock, type CombatUnitContext, } from '../../../src/server/engine/combat-pipeline.ts' import { evaluateSkill113c, executeSkill113c, registerSkillModule, } from '../../../src/server/skills/registry.ts' import { skillModule as doubleSwingModule } from '../../../src/server/skills/impl/bar/skill-133-double-swing.ts' import { skillModule as frenzyModule } from '../../../src/server/skills/impl/bar/skill-147-frenzy.ts' import { skillModule as berserkModule } from '../../../src/server/skills/impl/bar/skill-152-berserk.ts' import { skillModule as whirlwindModule } from '../../../src/server/skills/impl/bar/skill-151-whirlwind.ts' describe('Cohort 5 Adversarial Stress Testing — Barbarian Combat & Masteries', () => { // ========================================================================= // 1. Whirlwind 1.13c Hit Cadence // ========================================================================= describe('1. Whirlwind 1.13c Hit Cadence', () => { it('1.1 Unconditional hit checks at 4th and 8th frame regardless of duration', () => { // Below frame 4: zero checks for (let f = 1; f < 4; f++) { const res = calculateWhirlwindHitFrames(f, 0, 0, false) expect(res.hitFrames).toEqual([]) } // Frames 4 through 7: exactly frame 4 for (let f = 4; f < 8; f++) { const res = calculateWhirlwindHitFrames(f, 0, 0, false) expect(res.hitFrames).toEqual([4]) } // Frames 8 through 11 (with slow weapon, interval 12): exactly frames 4 and 8 for (let f = 8; f < 12; f++) { const res = calculateWhirlwindHitFrames(f, 0, 10, false) expect(res.hitFrames).toEqual([4, 8]) } }) it('1.2 Verifies exact Effective Weapon Speed (WSM - Weapon IAS) breakpoints: 4, 6, 8, 10, 12 frames', () => { // EWS <= -60: interval = 4 const b4 = calculateWhirlwindHitFrames(30, 60, 0) expect(b4.hitInterval).toBe(4) expect(b4.hitFrames).toEqual([4, 8, 12, 16, 20, 24, 28]) // -59 <= EWS <= -35: interval = 6 const b6_lower = calculateWhirlwindHitFrames(30, 59, 0) expect(b6_lower.hitInterval).toBe(6) expect(b6_lower.hitFrames).toEqual([4, 8, 14, 20, 26]) const b6_upper = calculateWhirlwindHitFrames(30, 35, 0) expect(b6_upper.hitInterval).toBe(6) expect(b6_upper.hitFrames).toEqual([4, 8, 14, 20, 26]) // -34 <= EWS <= -15: interval = 8 const b8_lower = calculateWhirlwindHitFrames(30, 34, 0) expect(b8_lower.hitInterval).toBe(8) expect(b8_lower.hitFrames).toEqual([4, 8, 16, 24]) const b8_upper = calculateWhirlwindHitFrames(30, 15, 0) expect(b8_upper.hitInterval).toBe(8) expect(b8_upper.hitFrames).toEqual([4, 8, 16, 24]) // -14 <= EWS <= 0: interval = 10 const b10_lower = calculateWhirlwindHitFrames(30, 14, 0) expect(b10_lower.hitInterval).toBe(10) expect(b10_lower.hitFrames).toEqual([4, 8, 18, 28]) const b10_zero = calculateWhirlwindHitFrames(30, 0, 0) expect(b10_zero.hitInterval).toBe(10) expect(b10_zero.hitFrames).toEqual([4, 8, 18, 28]) // EWS > 0: interval = 12 const b12 = calculateWhirlwindHitFrames(30, 0, 10) expect(b12.hitInterval).toBe(12) expect(b12.hitFrames).toEqual([4, 8, 20]) }) it('1.3 Fuzzing sweep across EWS [-100..+50] validates monotonic intervals and strict step boundaries', () => { let previousInterval = 4 for (let ews = -100; ews <= 50; ews++) { // weaponBaseSpeed = ews, weaponIas = 0 -> EWS = ews const res = calculateWhirlwindHitFrames(50, 0, ews) if (ews <= -60) { expect(res.hitInterval).toBe(4) } else if (ews <= -35) { expect(res.hitInterval).toBe(6) } else if (ews <= -15) { expect(res.hitInterval).toBe(8) } else if (ews <= 0) { expect(res.hitInterval).toBe(10) } else { expect(res.hitInterval).toBe(12) } expect(res.hitInterval).toBeGreaterThanOrEqual(previousInterval) previousInterval = res.hitInterval } }) it('1.4 Dual-wielding flag propagation', () => { const dwRes = calculateWhirlwindHitFrames(24, 60, 0, true) expect(dwRes.isDualWield).toBe(true) expect(dwRes.hitFrames).toEqual([4, 8, 12, 16, 20, 24]) const singleRes = calculateWhirlwindHitFrames(24, 60, 0, false) expect(singleRes.isDualWield).toBe(false) }) }) // ========================================================================= // 2. Frenzy Speed Stacking & Decay // ========================================================================= describe('2. Frenzy Speed Stacking & Decay', () => { it('2.1 Sequential successful hits stack attack speed and run/walk speed buffs monotonically up to caps', () => { const slvl = 20 const stats = calculateFrenzyStats(slvl) let currentStacks = 0 let lastRunBonus = 0 let lastAtkBonus = 0 // Simulate 15 consecutive hits for (let hit = 1; hit <= 15; hit++) { const state = calculateFrenzyStackState(currentStacks, 1, slvl, 100, 105) expect(state.isExpired).toBe(false) expect(state.activeStacks).toBe(Math.min(10, hit)) if (state.activeStacks < 10) { expect(state.currentRunSpeedBonusPct).toBeGreaterThanOrEqual(lastRunBonus) expect(state.currentAttackSpeedBonusPct).toBeGreaterThanOrEqual(lastAtkBonus) } else { // At max 10 stacks, cap is reached expect(state.currentRunSpeedBonusPct).toBe(stats.maxRunSpeedPct) expect(state.currentAttackSpeedBonusPct).toBe(stats.maxAttackSpeedPct) } currentStacks = state.activeStacks lastRunBonus = state.currentRunSpeedBonusPct lastAtkBonus = state.currentAttackSpeedBonusPct } // Max stack count is strictly 10 expect(currentStacks).toBe(10) }) it('2.2 Frenzy stack duration is strictly 150 frames (6.0s) and properly resets upon expiration', () => { const slvl = 10 const lastHit = 1000 // At frame 1149 (elapsed = 149 frames < 150): active and remainingFrames = 1 const activeState = calculateFrenzyStackState(10, 0, slvl, lastHit, 1149) expect(activeState.isExpired).toBe(false) expect(activeState.activeStacks).toBe(10) expect(activeState.remainingFrames).toBe(1) expect(activeState.currentRunSpeedBonusPct).toBeGreaterThan(0) expect(activeState.currentAttackSpeedBonusPct).toBeGreaterThan(0) // At frame 1150 (elapsed = 150 frames): EXPIRED and fully reset to 0 const expiredAtExact = calculateFrenzyStackState(10, 0, slvl, lastHit, 1150) expect(expiredAtExact.isExpired).toBe(true) expect(expiredAtExact.activeStacks).toBe(0) expect(expiredAtExact.remainingFrames).toBe(0) expect(expiredAtExact.currentRunSpeedBonusPct).toBe(0) expect(expiredAtExact.currentAttackSpeedBonusPct).toBe(0) // Long after expiration (elapsed = 500 frames): remains 0 const wayPast = calculateFrenzyStackState(10, 0, slvl, lastHit, 1500) expect(wayPast.isExpired).toBe(true) expect(wayPast.activeStacks).toBe(0) expect(wayPast.currentRunSpeedBonusPct).toBe(0) expect(wayPast.currentAttackSpeedBonusPct).toBe(0) }) it('2.3 Adversarial Edge Case: 0 stacks and 0 hits within elapsed window', () => { // When a player has 0 current stacks and makes 0 hits, newStacks is 0 const zeroState = calculateFrenzyStackState(0, 0, 1, 0, 50) expect(zeroState.activeStacks).toBe(0) // Note: ratio 0/10 yields minRunSpeedPct in raw formula unless clamped or gated expect(zeroState.currentAttackSpeedBonusPct).toBe(0) }) }) // ========================================================================= // 3. Berserk Magic Conversion & Zero Defense // ========================================================================= describe('3. Berserk Magic Conversion & Zero Defense', () => { it('3.1 100% of physical damage converts to Magic damage (bypassing physical resistance)', async () => { const registry = await getSharedDataRegistry() const heroStats = new UnitStatList() heroStats.addStat('level', 80) heroStats.addStat('tohit', 5000) heroStats.addStat('damageresist', 0) const heroBus = new StateBus(heroStats, registry) const attacker: CombatUnitContext = { id: 'barb', name: 'Barbarian', statList: heroStats, stateBus: heroBus, weaponMinPhys: 100, weaponMaxPhys: 100, } // Defender with 100% Physical Immunity (damageresist = 100) and 0% Magic Resist (magicresist = 0) const immuneStats = new UnitStatList() immuneStats.addStat('level', 80) immuneStats.addStat('current_hp', 50000) immuneStats.addStat('armorclass', 100) immuneStats.addStat('damageresist', 100) // Physical Immune immuneStats.addStat('magicresist', 0) const immuneBus = new StateBus(immuneStats, registry) const physImmuneDefender: CombatUnitContext = { id: 'stone_skin_boss', name: 'Physical Immune Monster', statList: immuneStats, stateBus: immuneBus, } // Normal physical attack deals 0 damage due to physical immunity const normalDmg = executeSUnitDmg(attacker, physImmuneDefender, { skillId: 0, attackKind: 'melee', srcDam: 128, physDamagePct: 0, autoHit: true, }) expect(normalDmg.immuneToPhys).toBe(true) expect(normalDmg.totalDamage).toBe(0) // Berserk (skillId = 152) converts 100% to Magic damage and completely BYPASSES physical immunity const berserkDmg = executeSUnitDmg(attacker, physImmuneDefender, { skillId: 152, attackKind: 'melee', srcDam: 128, physDamagePct: 150, // +150% ED at slvl 1 physToElemPct: 100, autoHit: true, }) expect(berserkDmg.immuneToPhys).toBe(false) expect(berserkDmg.physDamage256).toBe(0) expect(berserkDmg.elemDamage256).toBeGreaterThan(0) expect(berserkDmg.totalDamage).toBeGreaterThan(0) }) it('3.2 Converted magic damage is correctly mitigated by target magic resistance and blocked by magic immunity', async () => { const registry = await getSharedDataRegistry() const heroStats = new UnitStatList() heroStats.addStat('level', 80) heroStats.addStat('tohit', 5000) const heroBus = new StateBus(heroStats, registry) const attacker: CombatUnitContext = { id: 'barb', name: 'Barbarian', statList: heroStats, stateBus: heroBus, weaponMinPhys: 100, weaponMaxPhys: 100, } // Defender with 50% Magic Resistance const mrStats = new UnitStatList() mrStats.addStat('level', 80) mrStats.addStat('current_hp', 50000) mrStats.addStat('armorclass', 100) mrStats.addStat('magicresist', 50) const mrBus = new StateBus(mrStats, registry) const halfResDefender: CombatUnitContext = { id: 'magic_res_monster', name: 'Magic Res Monster', statList: mrStats, stateBus: mrBus, } const halfDmg = executeSUnitDmg(attacker, halfResDefender, { skillId: 152, attackKind: 'melee', srcDam: 128, physDamagePct: 0, physToElemPct: 100, autoHit: true, }) // 100 base weapon damage with 50% magic resist yields 50 damage expect(halfDmg.totalDamage).toBe(50) // Defender with 100% Magic Immunity const miStats = new UnitStatList() miStats.addStat('level', 80) miStats.addStat('current_hp', 50000) miStats.addStat('magicresist', 100) // Magic Immune const miBus = new StateBus(miStats, registry) const magicImmuneDefender: CombatUnitContext = { id: 'unraveler_boss', name: 'Magic Immune Monster', statList: miStats, stateBus: miBus, } const zeroDmg = executeSUnitDmg(attacker, magicImmuneDefender, { skillId: 152, attackKind: 'melee', srcDam: 128, physDamagePct: 0, physToElemPct: 100, autoHit: true, }) expect(zeroDmg.immuneToElem).toBe(true) expect(zeroDmg.totalDamage).toBe(0) }) it('3.3 Berserk stats calculate strictly 100% magic conversion and slvl-dependent zero-defense duration', () => { for (let slvl = 1; slvl <= 30; slvl++) { const stats = calculateBerserkStats(slvl) expect(stats.magicConversionPct).toBe(100) expect(stats.zeroDefenseDurationFrames).toBeLessThanOrEqual(75) expect(stats.zeroDefenseDurationFrames).toBeGreaterThanOrEqual(25) } // Duration decreases with higher skill level const bz1 = calculateBerserkStats(1) const bz10 = calculateBerserkStats(10) const bz20 = calculateBerserkStats(20) const bz30 = calculateBerserkStats(30) expect(bz1.zeroDefenseDurationFrames).toBeGreaterThan(bz10.zeroDefenseDurationFrames) expect(bz10.zeroDefenseDurationFrames).toBeGreaterThan(bz20.zeroDefenseDurationFrames) expect(bz20.zeroDefenseDurationFrames).toBeGreaterThan(bz30.zeroDefenseDurationFrames) }) it('3.4 Adversarial Audit: Verifies defense rating and blocking when defender has 0 defense / no block', () => { // When defender defense is 0, ToHit chance depends solely on level ratio (capped [5..95]) const toHitZeroDef = computeToHitChance({ attackerAr: 1000, defenderDef: 0, attackerLvl: 80, defenderLvl: 80, }) // 2 * 1000 / (1000 + 0) * (80 / (80 + 80)) = 2 * 1 * 0.5 = 1.0 (clamped to 95%) expect(toHitZeroDef).toBe(95) // When toblock is 0, blocking cannot occur const dummyStats = new UnitStatList() dummyStats.addStat('toblock', 0) const dummyDefender: CombatUnitContext = { id: 'target', name: 'Target', statList: dummyStats, stateBus: new StateBus(dummyStats, {} as any), hasShield: true, } const blockResult = evaluateAvoidanceAndBlock({ defender: dummyDefender, attackKind: 'melee', roll100: 10, }) expect(blockResult.avoided).toBe(false) }) }) // ========================================================================= // 4. Double Swing Mana Cost Progression // ========================================================================= describe('4. Double Swing Mana Cost Progression', () => { it('4.1 Verifies Double Swing mana cost decreases with level and becomes strictly 0 at slvl 9+', () => { // Theoretical D2 1.13c progression: (8 - (slvl - 1)) * 64 in 256ths // slvl 1: 512 (2 mana) // slvl 2: 448 (1.75 -> 2 mana) // slvl 3: 384 (1.50 -> 2 mana) // slvl 4: 320 (1.25 -> 2 mana) // slvl 5: 256 (1.00 -> 1 mana) // slvl 6: 192 (0.75 -> 1 mana) // slvl 7: 128 (0.50 -> 1 mana) // slvl 8: 64 (0.25 -> 1 mana) // slvl 9+: 0 mana const expectedMana256 = [ 512, // slvl 1 448, // slvl 2 384, // slvl 3 320, // slvl 4 256, // slvl 5 192, // slvl 6 128, // slvl 7 64, // slvl 8 0, // slvl 9 ] for (let slvl = 1; slvl <= 9; slvl++) { const stats = calculateDoubleSwingStats(slvl) expect(stats.manaCost256).toBe(expectedMana256[slvl - 1]) if (slvl < 9) { expect(stats.manaCost).toBeGreaterThan(0) } else { expect(stats.manaCost).toBe(0) } } // slvl 10 through 99 must remain strictly 0 mana for (let slvl = 10; slvl <= 99; slvl += 10) { const stats = calculateDoubleSwingStats(slvl) expect(stats.manaCost).toBe(0) expect(stats.manaCost256).toBe(0) } }) it('4.2 Differential testing of Double Swing mana cost against naive oracle', () => { const oracleManaCost256 = (lvl: number) => Math.max(0, (8 - (lvl - 1)) * 64) for (let slvl = -5; slvl <= 100; slvl++) { const actual = calculateDoubleSwingStats(slvl).manaCost256 const effLvl = Math.max(1, slvl) const expected = oracleManaCost256(effLvl) expect(actual).toBe(expected) } }) }) // ========================================================================= // 5. Combat Masteries Diminishing Returns // ========================================================================= describe('5. Combat Masteries Diminishing Returns', () => { it('5.1 Critical Strike chance follows authentic diminishing returns formula dm(0, 35, slvl) across levels 1..30', () => { const oracleCrit = (lvl: number) => Math.trunc((110 * lvl * 35) / (100 * (lvl + 6))) const masteryFuncs = [ { name: 'Sword', fn: calculateSwordMasteryStats }, { name: 'Axe', fn: calculateAxeMasteryStats }, { name: 'Mace', fn: calculateMaceMasteryStats }, { name: 'Polearm', fn: calculatePolearmMasteryStats }, { name: 'Throwing', fn: calculateThrowingMasteryStats }, { name: 'Spear', fn: calculateSpearMasteryStats }, ] for (const m of masteryFuncs) { for (let slvl = 1; slvl <= 30; slvl++) { const stats = m.fn(slvl) const expected = oracleCrit(slvl) expect(stats.criticalStrikeChancePct, `${m.name} Mastery Crit at slvl ${slvl}`).toBe(expected) } } }) it('5.2 Natural Resistance elemental resistance % follows authentic diminishing returns curve dm(0, 80, slvl) across levels 1..30', () => { const oracleResist = (lvl: number) => Math.trunc((110 * lvl * 80) / (100 * (lvl + 6))) for (let slvl = 1; slvl <= 30; slvl++) { const stats = calculateNaturalResistanceStats(slvl) const expected = oracleResist(slvl) expect(stats.resistAllPct, `Natural Resistance at slvl ${slvl}`).toBe(expected) expect(stats.fireResistPct).toBe(expected) expect(stats.coldResistPct).toBe(expected) expect(stats.lightningResistPct).toBe(expected) expect(stats.poisonResistPct).toBe(expected) } }) it('5.3 Ground Truth Parity Check: Iron Skin defense bonus is strictly LINEAR (ln12: 30 + 10*(slvl-1)), NOT diminishing returns', () => { // 1.13c Skills.txt specifies: // Param1: 30, Param2: 10, passivecalc1: ln12 // Formula: 30 + (slvl - 1) * 10 for (let slvl = 1; slvl <= 30; slvl++) { const stats = calculateIronSkinStats(slvl) const expectedLinear = 30 + (slvl - 1) * 10 expect(stats.defenseBonusPct, `Iron Skin at slvl ${slvl}`).toBe(expectedLinear) } }) it('5.4 Increased Speed velocity follows authentic diminishing returns curve dm(7, 50, slvl)', () => { const oracleSpeed = (lvl: number) => 7 + Math.trunc((110 * lvl * (50 - 7)) / (100 * (lvl + 6))) for (let slvl = 1; slvl <= 30; slvl++) { const stats = calculateIncreasedSpeedStats(slvl) const expected = oracleSpeed(slvl) expect(stats.runWalkSpeedBonusPct, `Increased Speed at slvl ${slvl}`).toBe(expected) } }) }) // ========================================================================= // 6. Pre-Cast Equipment Checks // ========================================================================= describe('6. Pre-Cast Equipment Checks', () => { it('6.1 Melee weapon requirement strictly enforced across Bash, Double Swing, Stun, Concentrate, Frenzy, Whirlwind, Berserk', () => { const meleeSkills = [126, 133, 139, 144, 147, 151, 152] for (const skillId of meleeSkills) { // Rejects Bow expect(validateBarbarianSkillEquipment(skillId, { weaponItemType: 'bow' }).valid).toBe(false) expect(validateBarbarianSkillEquipment(skillId, { weaponItemType: 'bow' }).reason).toBe('requires_melee') // Rejects Crossbow expect(validateBarbarianSkillEquipment(skillId, { weaponItemType: 'crossbow' }).valid).toBe(false) expect(validateBarbarianSkillEquipment(skillId, { weaponItemType: 'crossbow' }).reason).toBe('requires_melee') // Rejects isRanged: true expect(validateBarbarianSkillEquipment(skillId, { isRanged: true }).valid).toBe(false) expect(validateBarbarianSkillEquipment(skillId, { isRanged: true }).reason).toBe('requires_melee') // Accepts melee weapons for (const meleeType of ['sword', 'axe', 'mace', 'spear', 'polearm']) { expect(validateBarbarianSkillEquipment(skillId, { weaponItemType: meleeType, isRanged: false }).valid).toBe(true) } } }) it('6.2 Dual-wield requirement strictly enforced on Double Swing (#133) and Frenzy (#147)', () => { for (const skillId of [133, 147]) { // Missing dual wield flag expect(validateBarbarianSkillEquipment(skillId, { hasDualWield: false }).valid).toBe(false) expect(validateBarbarianSkillEquipment(skillId, { hasDualWield: false }).reason).toBe('requires_dual_wield') // Not dual wielding expect(validateBarbarianSkillEquipment(skillId, { isDualWielding: false }).valid).toBe(false) expect(validateBarbarianSkillEquipment(skillId, { isDualWielding: false }).reason).toBe('requires_dual_wield') // Valid dual wielding expect(validateBarbarianSkillEquipment(skillId, { hasDualWield: true, isDualWielding: true }).valid).toBe(true) } }) it('6.3 Runtime execution rejection: Double Swing and Frenzy fail fast when dual-wielding is absent', async () => { const registry = await getSharedDataRegistry() registerSkillModule(doubleSwingModule) registerSkillModule(frenzyModule) const singleWieldHero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: new UnitStatList(), stateBus: new StateBus(new UnitStatList(), registry), hasDualWield: false, isDualWielding: false, } // Double Swing (#133) const dsRec = registry.getSkillById(133)! const dsEval = evaluateSkill113c({ registry, skill: dsRec, slvl: 1, blvl: 1, statList: singleWieldHero.statList }) const dsOutcome = executeSkill113c({ registry, skill: dsRec, evalResult: dsEval, caster: singleWieldHero, targets: [], targetPositions: new Map(), corpses: [], missileEngine: {} as any, auraScanner: {} as any, summonManager: {} as any, currentTick: 1, targetX: 0, targetY: 0, }) expect(dsOutcome.executed).toBe(false) expect(dsOutcome.notes).toContain('fail: requires dual-wield weapons') // Frenzy (#147) const fRec = registry.getSkillById(147)! const fEval = evaluateSkill113c({ registry, skill: fRec, slvl: 1, blvl: 1, statList: singleWieldHero.statList }) const fOutcome = executeSkill113c({ registry, skill: fRec, evalResult: fEval, caster: singleWieldHero, targets: [], targetPositions: new Map(), corpses: [], missileEngine: {} as any, auraScanner: {} as any, summonManager: {} as any, currentTick: 1, targetX: 0, targetY: 0, }) expect(fOutcome.executed).toBe(false) expect(fOutcome.notes).toContain('fail: requires dual-wield weapons') }) }) })