/** * Diablo II: Lord of Destruction v1.13c — Amazon Passive & Magic Stress & Adversarial Suite * * Comprehensive adversarial and E2E kinematics integration verification: * 1. Defensive Evasion Triad: * - Dodge: melee avoidance while standing still / attacking; triggers GH animation lock. * - Avoid: missile avoidance while standing still / attacking; triggers GH animation lock. * - Evade: avoidance while moving against both melee and missile; zero animation lock (uninterrupted). * 2. Inner Sight: * - Flat defense reduction applied directly before percentage modifiers. * - 200px radius, 700 + 150*(slvl-1) duration, 5 mana. * - Monster defense reduced, to-hit chance dramatically increased. * 3. Slow Missiles: * - 200px radius, 300 + 60*(slvl-1) duration, 5 mana. * - Hostile missile projectile velocity reduced to 33% (67% speed damper) in 2:1 isometric space. * 4. Critical Strike & Deadly Strike: * - Sequential evaluation: CS roll first, DS roll second if CS fails; never quadrupling (max 2x physical). * - Does not double elemental/magic damage. * 5. Penetrate: * - Linear +35% + 10%*(slvl-1) Attack Rating bonus correctly accrued to statList. * 6. Decoy (Dopplezon): * - Stationary posture (NU mode), cannot move or attack. * - Draws threat, scales HP from Amazon life (50% + 10%*slvl), up to 85% elemental resistance. * - Inherits Avoid and Evade passives from Amazon. * 7. Valkyrie: * - 150-frame (6.0s) casting delay. * - Base HP 440 * (1 + 0.2*(slvl-1)), +20% life per Decoy hard point synergy. * - Equipment progression tiers across slvl 1, 7, 11, 17, 27+. * - Inherits Critical Strike, Dodge, Avoid, Evade, Penetrate at Amazon's slvl. * 8. Pierce & Guided Arrow Invariant: * - 1..4 pierces (up to 5 targets hit). * - Guided Arrow (skillId === 22) strict 0% pierce override invariant. * 9. System Invariant: * - BATCH1_SKILLS length strictly equals 38; zero mocks (vi.mock strictly forbidden). * * Ground Truth: * - Blizzard v1.13c: D2Common.dll, D2Game.dll, Skills.txt, Missiles.txt */ import { describe, expect, it } from 'vitest' import { BATCH1_SKILLS, getSkillManaCost, getSkillCooldownTicks, calculateSkillDamage, } from '../../../src/server/skills/skills.ts' import { calculateInnerSightStats, calculateCriticalStrikeStats, calculateDodgeStats, calculateSlowMissilesStats, calculateAvoidStats, calculatePenetrateStats, calculateDecoyStats, calculateEvadeStats, calculateValkyrieStats, calculatePierceStats, } from '../../../src/common/skills/amazon-passive.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { MissileEngine, ISO_GROUND_ASPECT_RATIO } from '../../../src/server/engine/missile-engine.ts' import { AuraScanner } from '../../../src/server/engine/aura-scanner.ts' import { SummonManager } from '../../../src/server/ai/summon-manager.ts' import { WorldArena } from '../../../src/server/engine/world-arena.ts' import { UnitStatList, FIXED_ONE } from '../../../src/server/engine/stat-list.ts' import { StateBus } from '../../../src/server/engine/state-bus.ts' import { executeSUnitDmg, evaluateAvoidanceAndBlock, computeToHitChance, type CombatUnitContext, type SUnitDmgPacket, } from '../../../src/server/engine/combat-pipeline.ts' import { evaluateCriticalAndDeadlyStrike, calculateCriticalChance } from '../../../src/common/skills/formulas.ts' function createTestCombatUnit( id: string, pos: { x: number; y: number }, registry: any, opts?: { hp?: number isMoving?: boolean def?: number ar?: number level?: number isBoss?: boolean } ): CombatUnitContext { const statList = new UnitStatList(registry, { level: opts?.level ?? 80, hitpoints: (opts?.hp ?? 10000) * FIXED_ONE, maxhp: (opts?.hp ?? 10000) * FIXED_ONE, armorclass: opts?.def ?? 400, tohit: opts?.ar ?? 1000, fireresist: 0, coldresist: 0, lightresist: 0, poisonresist: 0, damageresist: 0, magicresist: 0, x: pos.x, y: pos.y, }) const stateBus = new StateBus(statList, registry) return { id, name: id, statList, stateBus, isMoving: opts?.isMoving ?? false, isBoss: opts?.isBoss ?? false, x: pos.x, y: pos.y, weaponMinPhys: 50, weaponMaxPhys: 100, } } describe('Amazon Passive & Magic Skills Adversarial Stress Suite (1.13c Ground Truth)', () => { it('enforces System Invariants: BATCH1_SKILLS length strictly equals 38 and zero mocks utilized', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) expect(BATCH1_SKILLS[6]).toBeDefined() // Magic Arrow expect(BATCH1_SKILLS[7]).toBeDefined() // Fire Arrow expect(BATCH1_SKILLS[10]).toBeDefined() // Jab expect(BATCH1_SKILLS[11]).toBeDefined() // Cold Arrow expect(BATCH1_SKILLS[14]).toBeDefined() // Power Strike }) describe('Defensive Evasion Triad: Dodge, Avoid, Evade & Animation Lock Invariants', () => { it('verifies Dodge: triggers only against melee while stationary/attacking and applies GH animation lock', async () => { const registry = await getSharedDataRegistry() // Amazon standing still with slvl 20 Dodge (56% chance) const amazonStationary = createTestCombatUnit('amazon-still', { x: 0, y: 0 }, registry, { isMoving: false }) const dodgeStats20 = calculateDodgeStats(20) amazonStationary.statList.setBaseSkillLevel(13, 20) amazonStationary.statList.addStat('passive_dodge', dodgeStats20.chancePct) // 1. Melee attack while stationary: roll 30 < 56% -> avoided via dodge const resMeleeStill = evaluateAvoidanceAndBlock({ defender: amazonStationary, attackKind: 'melee', roll100: 30, }) expect(resMeleeStill.avoided).toBe(true) expect(resMeleeStill.reason).toBe('dodge') expect(dodgeStats20.triggersAnimationLock).toBe(true) expect(dodgeStats20.animationCode).toBe('GH') // 2. Melee attack while stationary: roll 70 >= 56% -> not avoided const resMeleeStillFail = evaluateAvoidanceAndBlock({ defender: amazonStationary, attackKind: 'melee', roll100: 70, }) expect(resMeleeStillFail.avoided).toBe(false) expect(resMeleeStillFail.reason).toBe('none') // 3. Melee attack while moving: Dodge must NOT trigger when moving const amazonMoving = createTestCombatUnit('amazon-run', { x: 0, y: 0 }, registry, { isMoving: true }) amazonMoving.statList.addStat('passive_dodge', dodgeStats20.chancePct) const resMeleeMoving = evaluateAvoidanceAndBlock({ defender: amazonMoving, attackKind: 'melee', roll100: 30, }) // Dodge does not activate while moving! expect(resMeleeMoving.reason).not.toBe('dodge') }) it('verifies Avoid: triggers only against missiles while stationary/attacking and applies GH animation lock', async () => { const registry = await getSharedDataRegistry() // Amazon standing still with slvl 20 Avoid (65% chance) const amazonStill = createTestCombatUnit('amazon-still', { x: 0, y: 0 }, registry, { isMoving: false }) const avoidStats20 = calculateAvoidStats(20) amazonStill.statList.setBaseSkillLevel(18, 20) amazonStill.statList.addStat('passive_avoid', avoidStats20.chancePct) // 1. Ranged missile while stationary: roll 40 < 65% -> avoided via avoid const resMissileStill = evaluateAvoidanceAndBlock({ defender: amazonStill, attackKind: 'missile', roll100: 40, }) expect(resMissileStill.avoided).toBe(true) expect(resMissileStill.reason).toBe('avoid') expect(avoidStats20.triggersAnimationLock).toBe(true) expect(avoidStats20.animationCode).toBe('GH') // 2. Melee attack: Avoid must NOT trigger against melee attacks const resMelee = evaluateAvoidanceAndBlock({ defender: amazonStill, attackKind: 'melee', roll100: 40, }) expect(resMelee.reason).not.toBe('avoid') // 3. Missile attack while moving: Avoid must NOT trigger when moving const amazonMoving = createTestCombatUnit('amazon-run', { x: 0, y: 0 }, registry, { isMoving: true }) amazonMoving.statList.addStat('passive_avoid', avoidStats20.chancePct) const resMissileMoving = evaluateAvoidanceAndBlock({ defender: amazonMoving, attackKind: 'missile', roll100: 40, }) expect(resMissileMoving.reason).not.toBe('avoid') }) it('verifies Evade: triggers while moving against BOTH melee and missiles with ZERO animation lock', async () => { const registry = await getSharedDataRegistry() // Amazon moving with slvl 20 Evade (56% chance) const amazonMoving = createTestCombatUnit('amazon-moving', { x: 0, y: 0 }, registry, { isMoving: true }) const evadeStats20 = calculateEvadeStats(20) amazonMoving.statList.setBaseSkillLevel(29, 20) amazonMoving.statList.addStat('passive_evade', evadeStats20.chancePct) // 1. Evade against melee while moving: roll 30 < 56% -> avoided via evade const resMelee = evaluateAvoidanceAndBlock({ defender: amazonMoving, attackKind: 'melee', roll100: 30, }) expect(resMelee.avoided).toBe(true) expect(resMelee.reason).toBe('evade') // Zero animation lock: uninterrupted motion expect(evadeStats20.triggersAnimationLock).toBe(false) expect(evadeStats20.uninterrupted).toBe(true) // 2. Evade against missile while moving: roll 30 < 56% -> avoided via evade const resMissile = evaluateAvoidanceAndBlock({ defender: amazonMoving, attackKind: 'missile', roll100: 30, }) expect(resMissile.avoided).toBe(true) expect(resMissile.reason).toBe('evade') // 3. Evade while stationary: must NOT trigger when stationary const amazonStill = createTestCombatUnit('amazon-still', { x: 0, y: 0 }, registry, { isMoving: false }) amazonStill.statList.addStat('passive_evade', evadeStats20.chancePct) const resStill = evaluateAvoidanceAndBlock({ defender: amazonStill, attackKind: 'melee', roll100: 30, }) expect(resStill.reason).not.toBe('evade') }) }) describe('Inner Sight: Flat Defense Reduction & Illumination Radius', () => { it('applies flat defense reduction directly before percentage defense modifiers', async () => { const isStats1 = calculateInnerSightStats(1) expect(isStats1.flatDefenseReduction).toBe(-40) expect(isStats1.durationFrames).toBe(700) // 28.0s expect(isStats1.radiusPx).toBe(200) expect(isStats1.manaCost).toBe(5) const isStats20 = calculateInnerSightStats(20) expect(isStats20.flatDefenseReduction).toBe(-515) // -40 - 25*19 = -515 expect(isStats20.durationFrames).toBe(3550) // 700 + 150*19 = 3550 frames (142.0s) // Verification of defense reduction impact on hit chance: // Base Monster: 600 Defense, Attacker: 800 AR, both level 80 const baseHitChance = computeToHitChance({ attackerAr: 800, defenderDef: 600, attackerLvl: 80, defenderLvl: 80, }) // Raw: (2 * 800 * 100 * 80) / ((800 + 600) * 160) = 12800000 / 224000 = 57% expect(baseHitChance).toBe(57) // Afflicted by slvl 20 Inner Sight: 600 - 515 = 85 defense remaining const debuffedDef = Math.max(0, 600 + isStats20.flatDefenseReduction) expect(debuffedDef).toBe(85) const afflictedHitChance = computeToHitChance({ attackerAr: 800, defenderDef: debuffedDef, attackerLvl: 80, defenderLvl: 80, }) // Raw: (2 * 800 * 100 * 80) / ((800 + 85) * 160) = 12800000 / 141600 = 90% expect(afflictedHitChance).toBe(90) expect(afflictedHitChance).toBeGreaterThan(baseHitChance) }) }) describe('Slow Missiles: Projectile Velocity Kinematics Dampening', () => { it('scales hostile projectile velocity by 33% (67% speed reduction) in 2:1 isometric space', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const enemy = createTestCombatUnit('monster-caster', { x: 0, y: 0 }, registry) const dmgPacket: SUnitDmgPacket = { skillId: 0, attackKind: 'missile', flatPhysMin256: 50 * FIXED_ONE, flatPhysMax256: 50 * FIXED_ONE, } // 1. Baseline normal projectile const normalMsl = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner: enemy, startX: 0, startY: 0, targetX: 300, targetY: 0, dmgPacket, })! expect(normalMsl).not.toBeNull() const baseVx = normalMsl.vx const baseVy = normalMsl.vy // 2. Slowed projectile under Slow Missiles aura const slowedMsl = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner: enemy, startX: 0, startY: 0, targetX: 300, targetY: 0, dmgPacket, isSlowed: true, slowMultiplier: 0.33, })! expect(slowedMsl).not.toBeNull() // Step both missiles for 5 ticks for (let t = 0; t < 5; t++) { missileEngine.tick(t, [], new Map()) } // Under Slow Missiles, distance traveled must be ~33% of baseline expect(slowedMsl.x).toBeCloseTo(normalMsl.x * 0.33, 0) }) }) describe('Critical Strike & Deadly Strike: Sequential Doubling Invariant', () => { it('rolls Critical Strike before Deadly Strike and strictly never quadruples physical damage', () => { const csStats20 = calculateCriticalStrikeStats(20) expect(csStats20.chancePct).toBe(68) expect(csStats20.damageMultiplier).toBe(2.0) // Test sequential doubling with mock PRNG rolls: // Case 1: CS succeeds (roll 10 < 68%) -> critical_strike, DS skipped const res1 = evaluateCriticalAndDeadlyStrike(68, 50, () => 0.10) expect(res1.isCritical).toBe(true) expect(res1.triggeredBy).toBe('critical_strike') // Case 2: CS fails (roll 80 >= 68%), DS succeeds (roll 20 < 50%) -> deadly_strike let rollCount = 0 const rolls = [0.80, 0.20] const res2 = evaluateCriticalAndDeadlyStrike(68, 50, () => rolls[rollCount++] ?? 0.5) expect(res2.isCritical).toBe(true) expect(res2.triggeredBy).toBe('deadly_strike') // Case 3: Combined chance formula check: P = CS + DS * (1 - CS) // CS=68%, DS=50% -> 0.68 + 0.50 * 0.32 = 0.68 + 0.16 = 0.84 (84%) const combined = calculateCriticalChance(68, 50) expect(combined).toBe(84) // Invariant: In combat pipeline, physical damage is doubled ONCE, never multiplied by 4 const baseDamage = 100 const finalCritDamage = baseDamage * csStats20.damageMultiplier expect(finalCritDamage).toBe(200) expect(finalCritDamage).not.toBe(400) }) }) describe('Penetrate: Attack Rating Scaling & Stat Accrual', () => { it('linearly accrues +35% + 10%*(slvl-1) Attack Rating to UnitStatList', () => { const p1 = calculatePenetrateStats(1) expect(p1.attackRatingBonusPct).toBe(35) expect(p1.manaCost).toBe(0) const p10 = calculatePenetrateStats(10) expect(p10.attackRatingBonusPct).toBe(125) const p20 = calculatePenetrateStats(20) expect(p20.attackRatingBonusPct).toBe(225) // StatList verification: base AR 1000 with +225% Penetrate -> 1000 * 3.25 = 3250 const statList = new UnitStatList(undefined, { tohit: 1000 }) statList.addStat('item_tohit_percent', p20.attackRatingBonusPct) expect(statList.getAccruedStat('tohit')).toBe(3250) }) }) describe('Decoy (Dopplezon): Stationary Posture, Threat Drawing & Passive Inheritance', () => { it('creates stationary companion with life scaling, resistance cap, and inherited passives', async () => { const registry = await getSharedDataRegistry() const summonManager = new SummonManager(registry) const amazon = createTestCombatUnit('amazon-caster', { x: 100, y: 100 }, registry, { hp: 1000, def: 500, }) // Allocate Avoid (slvl 10) and Evade (slvl 8) hard points on Amazon amazon.statList.setBaseSkillLevel(18, 10) amazon.statList.setBaseSkillLevel(29, 8) const outcome = summonManager.createPet({ owner: amazon, skillId: 28, slvl: 10, x: 150, y: 120, }) expect(outcome.created).toBe(true) const decoy = outcome.pet! expect(decoy).toBeDefined() expect(decoy.kind).toBe('dopplezon') expect(decoy.isStationary).toBe(true) // Life scaling: 1000 * (0.5 + 0.1 * 10) = 1500 HP expect(decoy.hp).toBe(1500) expect(decoy.maxHp).toBe(1500) // Defense matches Amazon's defense expect(decoy.statList.getAccruedStat('armorclass')).toBe(500) // Resistances: min(85, 4 * 10) = 40% expect(decoy.statList.getAccruedStat('fireresist')).toBe(40) expect(decoy.statList.getAccruedStat('lightresist')).toBe(40) // Inherited Avoid & Evade passives expect(decoy.statList.getAccruedStat('passive_avoid')).toBe(calculateAvoidStats(10).chancePct) expect(decoy.statList.getAccruedStat('passive_evade')).toBe(calculateEvadeStats(8).chancePct) // Ticking Decoy in combat: stationary Decoy does NOT attack const enemy = createTestCombatUnit('enemy-target', { x: 150, y: 120 }, registry) const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const tickRes = summonManager.tickPets({ currentTick: 10, owner: amazon, enemies: [enemy], enemyPositions: new Map([['enemy-target', { x: 150, y: 120 }]]), missileEngine, auraScanner, }) expect(tickRes.petAttacks).toBe(0) }) }) describe('Valkyrie: 150-Frame Cooldown, Decoy Synergies & Equipment Tier Progression', () => { it('enforces 150-frame casting delay, +20% life per Decoy point, and 5 equipment generation tiers', async () => { const registry = await getSharedDataRegistry() const summonManager = new SummonManager(registry) // 1. Casting Delay Invariant: 150 frames (6.0s) expect(getSkillCooldownTicks(32)).toBe(150) expect(calculateValkyrieStats(1).cooldownFrames).toBe(150) expect(calculateValkyrieStats(1).cooldownSeconds).toBe(6.0) // 2. Decoy Synergy: +20% HP per hard point in Decoy const amazon = createTestCombatUnit('amazon-valk-caster', { x: 0, y: 0 }, registry) amazon.statList.setBaseSkillLevel(28, 5) // 5 Decoy hard points = +100% life amazon.statList.setBaseSkillLevel(9, 10) // CS slvl 10 amazon.statList.setBaseSkillLevel(13, 10) // Dodge slvl 10 amazon.statList.setBaseSkillLevel(18, 10) // Avoid slvl 10 amazon.statList.setBaseSkillLevel(29, 10) // Evade slvl 10 amazon.statList.setBaseSkillLevel(23, 10) // Penetrate slvl 10 const outcome = summonManager.createPet({ owner: amazon, skillId: 32, slvl: 10, }) expect(outcome.created).toBe(true) const valk = outcome.pet! expect(valk.kind).toBe('valkyrie') // Base HP at slvl 10: 440 * (1 + 0.2*9) = 440 * 2.8 = 1232 // Decoy Synergy (+100%): 1232 * 2.0 = 2464 HP expect(valk.hp).toBe(2464) // Resistances: min(75, 2 * 10) = 20% expect(valk.statList.getAccruedStat('fireresist')).toBe(20) // Inherited Amazon passives expect(valk.statList.getAccruedStat('passive_critical_strike')).toBe(calculateCriticalStrikeStats(10).chancePct) expect(valk.statList.getAccruedStat('passive_dodge')).toBe(calculateDodgeStats(10).chancePct) expect(valk.statList.getAccruedStat('passive_avoid')).toBe(calculateAvoidStats(10).chancePct) expect(valk.statList.getAccruedStat('passive_evade')).toBe(calculateEvadeStats(10).chancePct) expect(valk.statList.getAccruedStat('item_tohit_percent')).toBe(calculatePenetrateStats(10).attackRatingBonusPct) // 3. Equipment generation tiers expect(calculateValkyrieStats(1).equipmentTier).toBe('tier1_basic') expect(calculateValkyrieStats(7).equipmentTier).toBe('tier2_boots') expect(calculateValkyrieStats(11).equipmentTier).toBe('tier3_gloves') expect(calculateValkyrieStats(17).equipmentTier).toBe('tier4_war_pike') expect(calculateValkyrieStats(27).equipmentTier).toBe('tier5_rare_tiara') }) }) describe('Pierce: Penetration Limit & Guided Arrow Zero-Pierce Invariant', () => { it('caps pierce at 4 iterations (5 targets hit max) and strictly clamps Guided Arrow to 0%', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) // 1. Pierce stats check const pierceStats20 = calculatePierceStats(20) expect(pierceStats20.chancePct).toBe(86) expect(pierceStats20.maxPierceCount).toBe(4) expect(pierceStats20.maxHitsPerMissile).toBe(5) // 2. Guided Arrow strict 0% pierce override const gaStats = calculatePierceStats(20, 22) expect(gaStats.isGuidedArrow).toBe(true) expect(gaStats.chancePct).toBe(0) // 3. Runtime verification in MissileEngine: Guided Arrow NEVER pierces const owner = createTestCombatUnit('amazon-ga', { x: 0, y: 0 }, registry) const dmgPacket: SUnitDmgPacket = { skillId: 22, attackKind: 'missile', srcDam: 128, } const gaMissile = missileEngine.spawnMissile({ missileNameOrId: 'guidedarrow', sourceSkillId: 22, slvl: 20, owner, startX: 0, startY: 0, targetX: 100, targetY: 0, dmgPacket, pierceChancePct: 100, // Adversarial input: attempt to force 100% pierce }) expect(gaMissile!.canPierce).toBe(false) expect(gaMissile!.pierceChancePct).toBe(0) // 4. Normal arrow with Pierce skill penetrates enemies const normalArrow = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner, startX: 0, startY: 0, targetX: 200, targetY: 0, dmgPacket, pierceChancePct: 86, }) expect(normalArrow!.canPierce).toBe(true) expect(normalArrow!.pierceChancePct).toBe(86) }) }) })