diablo2-web/tests/skills/ama/adv-ama-e2e-gate.test.ts

340 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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/game/skills.ts'
import {
calculateMagicArrowStats,
calculateFireArrowStats,
calculateColdArrowStats,
calculateMultipleShotStats,
calculateExplodingArrowStats,
calculateIceArrowStats,
calculateGuidedArrowStats,
calculateStrafeStats,
calculateImmolationArrowStats,
calculateFreezingArrowStats,
} from '../../../src/game/skills/amazon-bow.ts'
import {
calculateInnerSightStats,
calculateCriticalStrikeStats,
calculateDodgeStats,
calculateSlowMissilesStats,
calculateAvoidStats,
calculatePenetrateStats,
calculateDecoyStats,
calculateEvadeStats,
calculateValkyrieStats,
calculatePierceStats,
} from '../../../src/game/skills/amazon-passive.ts'
import {
calculateJabStats,
calculatePowerStrikeStats,
calculatePoisonJavelinStats,
calculateImpaleStats,
calculateLightningBoltStats,
calculateChargedStrikeStats,
calculatePlagueJavelinStats,
calculateFendStats,
calculateLightningStrikeStats,
calculateLightningFuryStats,
} from '../../../src/game/skills/amazon-javelin-spear.ts'
import { ISO_GROUND_ASPECT_RATIO } from '../../../src/game/engine/missile-engine.ts'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { WorldArena } from '../../../src/game/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)
})
})
})