/** * Diablo II: Lord of Destruction v1.13c — Amazon Javelin and Spear Kinematics Stress Suite * * Comprehensive adversarial and E2E kinematics integration verification: * 1. Jab (10): 3 sequential thrusts, negative-to-positive ED% scaling (-15% -> +42%), attack rating progression. * 2. Power Strike (14): 5-band lightning scaling, +10%/lvl synergies (CS, LS, LF, LB). * 3. Poison Javelin (15): 12-frame casting delay, 100..1050 frame duration, trail cloud emissions, +12%/lvl PJ synergy. * 4. Impale (19): Massive ED% (+300%..+775%), durability loss degradation (50% -> 12%, min 10%), fixed 3 mana. * 5. Lightning Bolt (20): 100% physical-to-lightning conversion, auto-hit defense bypass, 1 - (2*slvl+10) bonus. * 6. Charged Strike (25): floor(slvl/5)+3 bolts (3..7+), point-blank shotgunning against bosses, 4x +14%/lvl synergies. * 7. Plague Javelin (29): 100-frame (4.0s) casting delay, 75..550 frame poison cloud explosion, +10%/lvl synergy. * 8. Fend (24): Multi-target melee strikes against up to 8 enemies, 1.13c D/A/E evasion interrupt bug flag. * 9. Lightning Strike (30): Chain hops min(24, slvl+1), 4-frame NextHitDelay, 4x +8%/lvl synergies. * 10. Lightning Fury (35): slvl radial bolts, 200px search radius, per-pierce bolt burst cascades, 4x +1%/lvl synergies. * 11. System Invariant: BATCH1_SKILLS length strictly maintained at 38; zero mocks; zero runtime MPQ reads. * * 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 { calculateJabStats, calculatePowerStrikeStats, calculatePoisonJavelinStats, calculateImpaleStats, calculateLightningBoltStats, calculateChargedStrikeStats, calculatePlagueJavelinStats, calculateFendStats, calculateLightningStrikeStats, calculateLightningFuryStats, } from '../../../src/common/skills/amazon-javelin-spear.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { WorldArena } from '../../../src/server/engine/world-arena.ts' describe('Diablo II v1.13c Amazon Javelin & Spear Adversarial & Kinematics Suite', () => { it('enforces System Invariant: BATCH1_SKILLS length strictly maintained at 38', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) expect(BATCH1_SKILLS[10]).toBeDefined() // Jab expect(BATCH1_SKILLS[14]).toBeDefined() // Power Strike }) describe('Skill 010: Jab — 3-Thrust Rapid Melee Kinematics', () => { it('accurately computes 1.13c 3-thrust count, damage penalty/bonus, and AR progression', () => { const s1 = calculateJabStats(1) expect(s1.strikesCount).toBe(3) expect(s1.enhancedDamagePct).toBe(-15) // -15% at slvl 1 expect(s1.attackRatingBonusPct).toBe(10) // +10% at slvl 1 expect(s1.manaCost).toBe(2.0) const s10 = calculateJabStats(10) expect(s10.strikesCount).toBe(3) expect(s10.enhancedDamagePct).toBe(12) // -15 + 3*9 = 12% expect(s10.attackRatingBonusPct).toBe(91) // 10 + 9*9 = 91% expect(s10.manaCost).toBe(4.25) // 2.0 + 0.25*9 const s20 = calculateJabStats(20) expect(s20.strikesCount).toBe(3) expect(s20.enhancedDamagePct).toBe(42) // -15 + 3*19 = 42% expect(s20.attackRatingBonusPct).toBe(181) // 10 + 9*19 = 181% expect(s20.manaCost).toBe(6.75) // 2.0 + 0.25*19 }) it('integrates with calculateSkillDamage correctly scaling weapon damage', () => { const weapon = { min: 20, max: 40 } const dmg1 = calculateSkillDamage(10, 1, 1.0, weapon) // Jab is defined in BATCH1_SKILLS with srcDamage 128 (100% weapon damage) expect(dmg1.min).toBe(20) expect(dmg1.max).toBe(40) }) }) describe('Skill 014: Power Strike — High-Voltage Lightning Jab', () => { it('evaluates 5-band lightning damage progression and 4-way synergy boosts', () => { const s1 = calculatePowerStrikeStats(1) expect(s1.minDamage).toBe(1) expect(s1.maxDamage).toBe(16) expect(s1.attackRatingBonusPct).toBe(20) expect(s1.manaCost).toBe(2.0) const s10 = calculatePowerStrikeStats(10) expect(s10.minDamage).toBe(1) expect(s10.maxDamage).toBeGreaterThan(150) expect(s10.attackRatingBonusPct).toBe(110) // Test synergies: +10% per level from Charged Strike, Lightning Strike, Lightning Fury, Lightning Bolt const syn = calculatePowerStrikeStats(20, { chargedStrike: 20, lightningStrike: 20, lightningFury: 20, lightningBolt: 20, }) expect(syn.synergyBonusPct).toBe(800) // 80 hard points * 10% = +800% expect(syn.synergyMultiplier).toBe(9.0) expect(syn.maxDamage).toBe(syn.baseMaxDamage * 9) }) }) describe('Skill 015: Poison Javelin — Trail Cloud Mechanics & Cooldown', () => { it('enforces 12-frame casting cooldown and 100..1050 frame duration scaling', () => { expect(getSkillCooldownTicks(15)).toBe(12) const s1 = calculatePoisonJavelinStats(1) expect(s1.durationFrames).toBe(100) // 4 seconds expect(s1.trailCloudMissile).toBe('poisonjavcloud') expect(s1.castingDelayFrames).toBe(12) expect(s1.manaCost).toBe(4.0) const s10 = calculatePoisonJavelinStats(10) expect(s10.durationFrames).toBe(550) // 100 + 50*9 = 550 frames (22s) const s20 = calculatePoisonJavelinStats(20) expect(s20.durationFrames).toBe(1050) // 100 + 50*19 = 1050 frames (42s) expect(s20.manaCost).toBe(8.75) // 4.0 + 0.25*19 // Synergy: +12% per point of Plague Javelin const syn = calculatePoisonJavelinStats(20, { plagueJavelin: 20 }) expect(syn.synergyBonusPct).toBe(240) // 20 * 12% = +240% expect(syn.synergyMultiplier).toBe(3.4) expect(syn.maxDamage).toBe(Math.floor(syn.baseMaxDamage * 3.4)) }) }) describe('Skill 019: Impale — Brutal Uninterruptible Thrust', () => { it('validates massive %ED (+300%..+775%) and durability loss degradation curve', () => { const s1 = calculateImpaleStats(1) expect(s1.enhancedDamagePct).toBe(300) expect(s1.attackRatingBonusPct).toBe(100) expect(s1.durabilityLossChancePct).toBe(50) expect(s1.uninterruptible).toBe(true) expect(s1.manaCost).toBe(3.0) const s10 = calculateImpaleStats(10) expect(s10.enhancedDamagePct).toBe(525) // 300 + 25*9 expect(s10.attackRatingBonusPct).toBe(280) // 100 + 20*9 expect(s10.durabilityLossChancePct).toBe(32) // 50 - 2*9 const s20 = calculateImpaleStats(20) expect(s20.enhancedDamagePct).toBe(775) // 300 + 25*19 expect(s20.attackRatingBonusPct).toBe(480) // 100 + 20*19 expect(s20.durabilityLossChancePct).toBe(12) // 50 - 2*19 // slvl 30 caps durability loss chance at minimum 10% const s30 = calculateImpaleStats(30) expect(s30.durabilityLossChancePct).toBe(10) // max(10, 50 - 2*29 = -8) -> 10% }) }) describe('Skill 020: Lightning Bolt — 100% Conversion & Defense Bypass', () => { it('enforces 100% physical to lightning conversion and auto-hit properties', () => { const s1 = calculateLightningBoltStats(1) expect(s1.conversionPct).toBe(100) expect(s1.srcDam).toBe(0) // Physical component zeroed out expect(s1.autoHit).toBe(true) expect(s1.bypassesDefense).toBe(true) expect(s1.flatBonusLightningMin).toBe(1) expect(s1.flatBonusLightningMax).toBe(12) // 1*2 + 10 = 12 expect(s1.manaCost).toBe(9.0) const s10 = calculateLightningBoltStats(10) expect(s10.flatBonusLightningMax).toBe(30) // 10*2 + 10 = 30 expect(s10.manaCost).toBe(6.75) // 9.0 - 0.25*9 const s20 = calculateLightningBoltStats(20) expect(s20.flatBonusLightningMax).toBe(50) // 20*2 + 10 = 50 expect(s20.manaCost).toBe(4.25) // 9.0 - 0.25*19 = 4.25 (floor) // Synergy: +3%/lvl from Power Strike, Charged Strike, Lightning Strike, Lightning Fury const syn = calculateLightningBoltStats(20, { powerStrike: 20, chargedStrike: 20, lightningStrike: 20, lightningFury: 20, }) expect(syn.synergyBonusPct).toBe(240) // 80 * 3% = 240% expect(syn.synergyMultiplier).toBe(3.4) }) }) describe('Skill 024: Charged Strike — Boss-Melting Multi-Bolt Shotgun', () => { it('scales bolt count floor(slvl/5)+3 and supports shotgunning against dense targets', () => { const s1 = calculateChargedStrikeStats(1) expect(s1.boltsCount).toBe(3) // floor(1/5)+3 = 3 expect(s1.shotgunning).toBe(true) expect(s1.manaCost).toBe(4.0) const s5 = calculateChargedStrikeStats(5) expect(s5.boltsCount).toBe(4) // floor(5/5)+3 = 4 const s10 = calculateChargedStrikeStats(10) expect(s10.boltsCount).toBe(5) // floor(10/5)+3 = 5 const s20 = calculateChargedStrikeStats(20) expect(s20.boltsCount).toBe(7) // floor(20/5)+3 = 7 const s25 = calculateChargedStrikeStats(25) expect(s25.boltsCount).toBe(8) // floor(25/5)+3 = 8 // Synergies: +14% per point from PS, LB, LS, LF const syn = calculateChargedStrikeStats(20, { powerStrike: 20, lightningBolt: 20, lightningStrike: 20, lightningFury: 20, }) expect(syn.synergyBonusPct).toBe(1120) // 80 * 14% = 1120% expect(syn.synergyMultiplier).toBe(12.2) expect(syn.maxDamage).toBe(Math.floor(syn.baseMaxDamage * 12.2)) }) }) describe('Skill 025: Plague Javelin — Lingering Toxic Cloud Explosions', () => { it('enforces 100-frame (4.0s) casting delay and expanding poison clouds', () => { expect(getSkillCooldownTicks(25)).toBe(100) const s1 = calculatePlagueJavelinStats(1) expect(s1.durationFrames).toBe(75) // 3 seconds expect(s1.castingDelayFrames).toBe(100) expect(s1.impactCloudMissile).toBe('plaguejavcloud') expect(s1.manaCost).toBe(8.5) const s10 = calculatePlagueJavelinStats(10) expect(s10.durationFrames).toBe(300) // 75 + 25*9 = 300 frames (12s) expect(s10.manaCost).toBe(13.0) // 8.5 + 0.5*9 const s20 = calculatePlagueJavelinStats(20) expect(s20.durationFrames).toBe(550) // 75 + 25*19 = 550 frames (22s) expect(s20.manaCost).toBe(18.0) // 8.5 + 0.5*19 // Synergy: +10% per point of Poison Javelin const syn = calculatePlagueJavelinStats(20, { poisonJavelin: 20 }) expect(syn.synergyBonusPct).toBe(200) // 20 * 10% = 200% expect(syn.synergyMultiplier).toBe(3.0) expect(syn.maxDamage).toBe(syn.baseMaxDamage * 3.0) }) }) describe('Skill 030: Fend — Multi-Target Thrash & 1.13c Evasion Interrupt Bug', () => { it('validates up to 8 adjacent targets and tracks authentic 1.13c D/A/E evasion lock bug', () => { const s1 = calculateFendStats(1) expect(s1.enhancedDamagePct).toBe(70) expect(s1.attackRatingBonusPct).toBe(40) expect(s1.maxAdjacentTargets).toBe(8) expect(s1.evasionInterruptible).toBe(true) expect(s1.fendBugActive).toBe(true) expect(s1.manaCost).toBe(5.0) const s10 = calculateFendStats(10, 4) expect(s10.enhancedDamagePct).toBe(160) // 70 + 10*9 expect(s10.attackRatingBonusPct).toBe(130) // 40 + 10*9 expect(s10.strikesCount).toBe(4) // Clamped to available enemies const s20 = calculateFendStats(20) expect(s20.enhancedDamagePct).toBe(260) // 70 + 10*19 expect(s20.attackRatingBonusPct).toBe(230) // 40 + 10*19 expect(s20.strikesCount).toBe(8) }) }) describe('Skill 034: Lightning Strike — Chain Arc Cascades', () => { it('verifies chain hops min(24, slvl+1), 4-frame NextHitDelay, and 4x +8%/lvl synergies', () => { const s1 = calculateLightningStrikeStats(1) expect(s1.chainHops).toBe(2) // 1 + 1 = 2 expect(s1.nextHitDelayFrames).toBe(4) expect(s1.manaCost).toBe(9.0) const s10 = calculateLightningStrikeStats(10) expect(s10.chainHops).toBe(11) // 10 + 1 = 11 const s20 = calculateLightningStrikeStats(20) expect(s20.chainHops).toBe(21) // 20 + 1 = 21 const s30 = calculateLightningStrikeStats(30) expect(s30.chainHops).toBe(24) // min(24, 31) = 24 cap // Synergies: +8% per point from PS, LB, CS, LF const syn = calculateLightningStrikeStats(20, { powerStrike: 20, lightningBolt: 20, chargedStrike: 20, lightningFury: 20, }) expect(syn.synergyBonusPct).toBe(640) // 80 * 8% = 640% expect(syn.synergyMultiplier).toBe(7.4) expect(syn.maxDamage).toBe(Math.floor(syn.baseMaxDamage * 7.4)) }) }) describe('Skill 035: Lightning Fury — Screen-Clearing Javelin & Bolt Bursts', () => { it('verifies radial bolt split per target/pierce, 200px search radius, and synergies', () => { const s1 = calculateLightningFuryStats(1) expect(s1.releaseBoltsCount).toBe(1) expect(s1.searchRadiusPx).toBe(200) expect(s1.piercingBursts).toBe(true) expect(s1.subMissileName).toBe('lightningjavelin') expect(s1.manaCost).toBe(10.0) const s10 = calculateLightningFuryStats(10) expect(s10.releaseBoltsCount).toBe(10) expect(s10.manaCost).toBe(14.5) // 10.0 + 0.5*9 const s20 = calculateLightningFuryStats(20) expect(s20.releaseBoltsCount).toBe(20) expect(s20.manaCost).toBe(19.5) // 10.0 + 0.5*19 // Synergies: +1% per point from PS, LB, CS, LS const syn = calculateLightningFuryStats(20, { powerStrike: 20, lightningBolt: 20, chargedStrike: 20, lightningStrike: 20, }) expect(syn.synergyBonusPct).toBe(80) // 80 * 1% = +80% expect(syn.synergyMultiplier).toBe(1.8) expect(syn.maxDamage).toBe(Math.floor(syn.baseMaxDamage * 1.8)) }) }) describe('WorldArena Combat Loop Integration', () => { it('executes javelin and spear attacks inside simulated arena with valid action frame outcomes', async () => { const registry = await getSharedDataRegistry() const arena = new WorldArena({ registry, classCode: 'ama', skillId: 10, slvl: 20, }) const outcome = arena.triggerCast() arena.stepTicks(15) const dbg = arena.getDebugState() expect(dbg.ready).toBe(true) expect(dbg.skillId).toBe(10) expect(dbg.castCount).toBeGreaterThanOrEqual(1) expect(dbg.actionFrameTriggered).toBe(true) expect(outcome?.executed).toBe(true) }) }) })