diablo2-web/tests/skills/bar/adv-bar-cohort5-adversarial...

615 lines
25 KiB
TypeScript

/**
* 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/game/skills/barbarian-combat.ts'
import {
calculateSwordMasteryStats,
calculateAxeMasteryStats,
calculateMaceMasteryStats,
calculatePolearmMasteryStats,
calculateThrowingMasteryStats,
calculateSpearMasteryStats,
calculateIronSkinStats,
calculateNaturalResistanceStats,
calculateIncreasedSpeedStats,
} from '../../../src/game/skills/barbarian-masteries.ts'
import { computeDiminishingReturns } from '../../../src/game/engine/calc-ast.ts'
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,
computeToHitChance,
evaluateAvoidanceAndBlock,
type CombatUnitContext,
} from '../../../src/game/engine/combat-pipeline.ts'
import {
evaluateSkill113c,
executeSkill113c,
registerSkillModule,
} from '../../../src/game/skills/registry.ts'
import { skillModule as doubleSwingModule } from '../../../src/game/skills/impl/bar/skill-133-double-swing.ts'
import { skillModule as frenzyModule } from '../../../src/game/skills/impl/bar/skill-147-frenzy.ts'
import { skillModule as berserkModule } from '../../../src/game/skills/impl/bar/skill-152-berserk.ts'
import { skillModule as whirlwindModule } from '../../../src/game/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')
})
})
})