347 lines
14 KiB
TypeScript
347 lines
14 KiB
TypeScript
/**
|
|
* 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/game/skills.ts'
|
|
import {
|
|
calculateJabStats,
|
|
calculatePowerStrikeStats,
|
|
calculatePoisonJavelinStats,
|
|
calculateImpaleStats,
|
|
calculateLightningBoltStats,
|
|
calculateChargedStrikeStats,
|
|
calculatePlagueJavelinStats,
|
|
calculateFendStats,
|
|
calculateLightningStrikeStats,
|
|
calculateLightningFuryStats,
|
|
} from '../../../src/game/skills/amazon-javelin-spear.ts'
|
|
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
|
|
import { WorldArena } from '../../../src/game/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)
|
|
})
|
|
})
|
|
})
|