/** * Diablo II: Lord of Destruction v1.13c — Amazon Milestone M15 Class Gate & End-to-End Suite * * Comprehensive cross-tree integration and architectural verification across all 30 Amazon skills: * * Skill Trees Covered: * 1. Bow and Crossbow: * - Magic Arrow (006), Fire Arrow (007), Cold Arrow (011), Multiple Shot (012), * Exploding Arrow (016), Ice Arrow (021), Guided Arrow (022), Strafe (026), * Immolation Arrow (027), Freezing Arrow (031). * 2. Passive and Magic: * - Inner Sight (008), Critical Strike (009), Dodge (013), Slow Missiles (017), * Avoid (018), Penetrate (023), Decoy (028), Evade (029), Valkyrie (032), Pierce (033). * 3. Javelin and Spear: * - Jab (010), Power Strike (014), Poison Javelin (015), Impale (019), * Lightning Bolt (020), Charged Strike (024), Plague Javelin (025), Fend (030), * Lightning Strike (034), Lightning Fury (035). * * Core Verification Scenarios: * Scenario 1: Guided Arrow + Pierce + Critical Strike Interaction: * - 1.13c Invariant: Guided Arrow never pierces even under 100% Pierce chance. * - Critical Strike 5%..68% proc probability correctly doubles physical damage packet. * * Scenario 2: Lightning Fury + Pierce Screen-Clearing Bursts: * - Piercing thrown javelin triggers an independent cluster of `slvl` bolts on EVERY hit. * - Synergy from Charged Strike, Lightning Strike, Power Strike. * * Scenario 3: Valkyrie High-Tier Minion Scaling & Synergies: * - Decoy synergy: +20% life per hard point in Decoy. * - Inherits passive skills (Critical Strike, Dodge, Avoid, Evade) from Amazon skill tree. * * Scenario 4: Tactical Crowd Control & Monster Debuffs: * - Slow Missiles projectile velocity reduction (-33%..-66% projectile speed). * - Inner Sight flat defense reduction (-40..-515 defense) eliminating monster evasion. * * Scenario 5: Fend Multi-Target Thrash & Authentic 1.13c Evasion Interrupt Bug: * - Multi-target melee strikes against up to 8 enemies. * - Entering Dodge/Avoid/Evade animation halts and cancels remaining Fend strikes. * * Scenario 6: Freezing Arrow AoE Explosion & Cold Arrow Synergies: * - 80 sub-pixel AoE radius with guaranteed solid freezing. * - Cold Arrow synergy (+12% cold damage per level). * * Scenario 7: Full 30-Skill Amazon Registry & Cooldown Audit: * - All 30 skills registered with valid mana curves, cooldowns, and damage scaling. * * Scenario 8: Architectural Invariants: * - BATCH1_SKILLS length strictly equals 38; zero mocks; zero runtime MPQ queries. */ import { describe, expect, it } from 'vitest' import { BATCH1_SKILLS, getSkillManaCost, getSkillCooldownTicks, calculateSkillDamage, } from '../../../src/server/skills/skills.ts' import { calculateMagicArrowStats, calculateFireArrowStats, calculateColdArrowStats, calculateMultipleShotStats, calculateExplodingArrowStats, calculateIceArrowStats, calculateGuidedArrowStats, calculateStrafeStats, calculateImmolationArrowStats, calculateFreezingArrowStats, } from '../../../src/common/skills/amazon-bow.ts' import { calculateInnerSightStats, calculateCriticalStrikeStats, calculateDodgeStats, calculateSlowMissilesStats, calculateAvoidStats, calculatePenetrateStats, calculateDecoyStats, calculateEvadeStats, calculateValkyrieStats, calculatePierceStats, } from '../../../src/common/skills/amazon-passive.ts' import { calculateJabStats, calculatePowerStrikeStats, calculatePoisonJavelinStats, calculateImpaleStats, calculateLightningBoltStats, calculateChargedStrikeStats, calculatePlagueJavelinStats, calculateFendStats, calculateLightningStrikeStats, calculateLightningFuryStats, } from '../../../src/common/skills/amazon-javelin-spear.ts' import { ISO_GROUND_ASPECT_RATIO } from '../../../src/server/engine/missile-engine.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { WorldArena } from '../../../src/server/engine/world-arena.ts' describe('Milestone M15: Amazon Class Gate & Full Class Verification (All 30 Skills 6–35)', () => { it('Architecture Invariants: BATCH1_SKILLS length strictly 38 and ISO_GROUND_ASPECT_RATIO === 0.5', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) expect(ISO_GROUND_ASPECT_RATIO).toBe(0.5) }) describe('Scenario 1: Cross-Tree Guided Arrow + Pierce + Critical Strike Parity', () => { it('enforces 1.13c zero-pierce invariant on Guided Arrow even under 100% Pierce skill', () => { const pierceStats = calculatePierceStats(20) expect(pierceStats.chancePct).toBeGreaterThanOrEqual(69) // Guided Arrow specifically disallows pierce in 1.13c (Patch 1.09 bugfix) const gaStats = calculateGuidedArrowStats(20) expect(gaStats.autoHit).toBe(true) expect(gaStats.canPierce).toBe(false) expect(gaStats.pierceChancePct).toBe(0) }) it('integrates Critical Strike chance doubling physical weapon damage packet', () => { const csStats = calculateCriticalStrikeStats(20) expect(csStats.chancePct).toBe(68) // 68% chance at slvl 20 const weapon = { min: 100, max: 200 } const gaStats = calculateGuidedArrowStats(20) const edMult = 1 + gaStats.enhancedDamagePct / 100 // +95% ED -> 1.95 const normalDamage = { min: Math.floor(weapon.min * edMult), max: Math.floor(weapon.max * edMult), } const critDamage = { min: normalDamage.min * 2, max: normalDamage.max * 2, } expect(normalDamage.min).toBe(195) expect(normalDamage.max).toBe(390) expect(critDamage.min).toBe(390) expect(critDamage.max).toBe(780) }) }) describe('Scenario 2: Lightning Fury + Pierce Screen-Clearing Bursts', () => { it('triggers per-pierce bolt bursts on EVERY penetrated target under high Pierce skill', () => { const pierceStats = calculatePierceStats(20) expect(pierceStats.chancePct).toBeGreaterThan(65) const lfStats = calculateLightningFuryStats(20, { chargedStrike: 20, lightningStrike: 20, lightningBolt: 20, powerStrike: 20, }) expect(lfStats.releaseBoltsCount).toBe(20) // 20 bolts per target hit/pierce expect(lfStats.piercingBursts).toBe(true) // Unleashes bolts on EACH pierce expect(lfStats.searchRadiusPx).toBe(200) expect(lfStats.synergyBonusPct).toBe(80) // 80 * 1% = +80% expect(lfStats.synergyMultiplier).toBe(1.8) // When penetrating 4 aligned monsters, total bolts spawned = 4 * 20 = 80 bolts const totalBoltsSpawnedOn4Pierces = 4 * lfStats.releaseBoltsCount expect(totalBoltsSpawnedOn4Pierces).toBe(80) }) }) describe('Scenario 3: Valkyrie High-Tier Minion Scaling & Synergies', () => { it('scales Valkyrie base stats, Decoy +20% life synergy, and inherited passives', () => { // Valkyrie slvl 20 without Decoy synergy const valkBase = calculateValkyrieStats(20) expect(valkBase.baseHp).toBeGreaterThan(1500) expect(valkBase.finalHp).toBe(valkBase.baseHp) expect(valkBase.cooldownFrames).toBe(150) expect(valkBase.manaCost).toBe(44) // 25 + 1*19 // Valkyrie slvl 20 with 20 hard points in Decoy (+20% life per point = +400% life) const valkSynergized = calculateValkyrieStats(20, { decoyHardPoints: 20, dodgeLvl: 10, avoidLvl: 10, evadeLvl: 10, }) expect(valkSynergized.decoySynergyBonusPct).toBe(400) expect(valkSynergized.finalHp).toBe(valkBase.baseHp * 5.0) // Verifies inherited defensive passives const dodge = calculateDodgeStats(valkSynergized.inheritedDodgeLvl) const avoid = calculateAvoidStats(valkSynergized.inheritedAvoidLvl) const evade = calculateEvadeStats(valkSynergized.inheritedEvadeLvl) expect(dodge.chancePct).toBeGreaterThanOrEqual(18) expect(avoid.chancePct).toBeGreaterThanOrEqual(24) expect(evade.chancePct).toBeGreaterThanOrEqual(13) }) }) describe('Scenario 4: Tactical Crowd Control & Monster Debuffs', () => { it('verifies Slow Missiles projectile velocity reduction and Inner Sight defense strip', () => { const smStats = calculateSlowMissilesStats(10) expect(smStats.velocityMultiplier).toBe(0.33) // Projectiles slowed to 33% speed expect(smStats.velocityReductionPct).toBe(67) expect(smStats.durationFrames).toBeGreaterThan(300) const isStats = calculateInnerSightStats(20) expect(isStats.flatDefenseReduction).toBe(-515) // Strips 515 flat defense expect(isStats.radiusPx).toBe(200) // Target with 500 defense gets reduced to 0 defense (guaranteed hit cap) const monsterBaseDef = 500 const effectiveDef = Math.max(0, monsterBaseDef + isStats.flatDefenseReduction) expect(effectiveDef).toBe(0) }) }) describe('Scenario 5: Fend Multi-Target Thrash & 1.13c Evasion Interrupt Bug', () => { it('simulates Fend striking up to 8 targets and handling D/A/E evasion lock bug', () => { const fendStats = calculateFendStats(20, 6) expect(fendStats.strikesCount).toBe(6) expect(fendStats.enhancedDamagePct).toBe(260) // 70 + 10*19 = 260% expect(fendStats.attackRatingBonusPct).toBe(230) // 40 + 10*19 = 230% expect(fendStats.evasionInterruptible).toBe(true) expect(fendStats.fendBugActive).toBe(true) // Simulated combat tick: if an evasion state triggers while Fend is executing, // remaining strikes are aborted (1.13c ground truth Fend bug). let currentStrike = 1 let wasInterrupted = false for (let s = 1; s <= fendStats.strikesCount; s++) { if (s === 3) { // Monster attacks back and triggers Dodge (13) wasInterrupted = true break } currentStrike++ } expect(wasInterrupted).toBe(true) expect(currentStrike).toBe(3) }) }) describe('Scenario 6: Freezing Arrow AoE Explosion & Cold Arrow Synergies', () => { it('evaluates 80-pixel AoE radius, freeze duration, and Cold Arrow synergy (+12%/lvl)', () => { const faBase = calculateFreezingArrowStats(20) expect(faBase.radiusPx).toBe(80) expect(faBase.freezeFrames).toBe(50) // 2.0 seconds (50 frames) expect(faBase.minColdDamage).toBeGreaterThan(250) expect(faBase.maxColdDamage).toBeGreaterThan(260) // With 20 points in Cold Arrow: +12% cold damage per point = +240% (multiplier 3.4) const faSynergized = calculateFreezingArrowStats(20, { coldArrow: 20 }) expect(faSynergized.coldSynergyMultiplier).toBe(3.4) expect(faSynergized.minColdDamage).toBe(Math.floor(faBase.baseMinColdDamage * 3.4)) expect(faSynergized.maxColdDamage).toBe(Math.floor(faBase.baseMaxColdDamage * 3.4)) }) }) describe('Scenario 7: Full 30-Skill Amazon Registry & Cooldown Audit', () => { it('verifies casting delays and cooldowns across all 30 Amazon skills', () => { // Poison Javelin (15): 12 frames (0.5s) expect(getSkillCooldownTicks(15)).toBe(12) // Plague Javelin (25): 100 frames (4.0s) expect(getSkillCooldownTicks(25)).toBe(100) // Immolation Arrow (27): 25 frames (1.0s) expect(getSkillCooldownTicks(27)).toBe(25) // Valkyrie (32): 150 frames (6.0s) expect(getSkillCooldownTicks(32)).toBe(150) // All other 26 Amazon skills have 0 cooldown ticks const zeroCooldownSkills = [ 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 29, 30, 31, 33, 34, 35, ] for (const id of zeroCooldownSkills) { expect(getSkillCooldownTicks(id)).toBe(0) } }) it('verifies mana cost evaluation across all 30 Amazon skills at slvl 1 and 20', () => { for (let skillId = 6; skillId <= 35; skillId++) { const mana1 = getSkillManaCost(skillId, 1) const mana20 = getSkillManaCost(skillId, 20) expect(Number.isFinite(mana1)).toBe(true) expect(Number.isFinite(mana20)).toBe(true) expect(mana1).toBeGreaterThanOrEqual(0) expect(mana20).toBeGreaterThanOrEqual(0) } }) it('verifies damage evaluation across all 30 Amazon skills with test weapon', () => { const testWeapon = { min: 30, max: 60 } for (let skillId = 6; skillId <= 35; skillId++) { const dmg = calculateSkillDamage(skillId, 10, 1.0, testWeapon) expect(Number.isFinite(dmg.min)).toBe(true) expect(Number.isFinite(dmg.max)).toBe(true) expect(dmg.min).toBeGreaterThanOrEqual(0) expect(dmg.max).toBeGreaterThanOrEqual(dmg.min) } }) }) describe('Scenario 8: WorldArena Runtime Simulation Across All 3 Skill Trees', () => { it('executes representative skill casts across Bow, Passive, and Javelin trees', async () => { const registry = await getSharedDataRegistry() // 1. Bow Tree: Freezing Arrow (31) const bowArena = new WorldArena({ registry, classCode: 'ama', skillId: 31, slvl: 20, }) const bowOutcome = bowArena.triggerCast() bowArena.stepTicks(15) expect(bowOutcome?.executed).toBe(true) expect(bowArena.getDebugState().actionFrameTriggered).toBe(true) // 2. Passive Tree: Valkyrie (32) const valkArena = new WorldArena({ registry, classCode: 'ama', skillId: 32, slvl: 20, }) const valkOutcome = valkArena.triggerCast() valkArena.stepTicks(15) expect(valkOutcome?.executed).toBe(true) expect(valkArena.getDebugState().actionFrameTriggered).toBe(true) // 3. Javelin Tree: Lightning Fury (35) const javArena = new WorldArena({ registry, classCode: 'ama', skillId: 35, slvl: 20, }) const javOutcome = javArena.triggerCast() javArena.stepTicks(15) expect(javOutcome?.executed).toBe(true) expect(javArena.getDebugState().actionFrameTriggered).toBe(true) }) }) })