diablo2-web/tests/skills/sor/issue-435-charged-bolt-kine...

99 lines
3.0 KiB
TypeScript

/**
* Issue #435: Charged Bolt (#38) Kinematics & Random Walk
*
* 1.13c Ground Truth Invariants Verification Suite:
* 1. Lateral Wander & Heading Drift: Charged Bolt accumulates directional changes
* across steps, resulting in snake-like serpentine wandering across the ground plane.
* 2. Elimination of ±3px Rigid Bound: Maximum lateral excursion significantly exceeds 3px
* over extended flight distance, proving genuine path divergence.
* 3. Speed Preservation: Throughout Cartesian angular drift and isometric mapping,
* the velocity magnitude remains calibrated and normalized.
* 4. Multi-Bolt Divergence: Multiple bolts fired with different seeds or ticks diverge
* into independent wandering paths.
*/
import { describe, expect, it } from 'vitest'
import { tickProjectiles, type Projectile } from '../../../src/game/skills.ts'
describe('Issue #435: Charged Bolt (#38) Kinematics & Random Walk', () => {
it('demonstrates serpentine lateral wander exceeding ±3px bound in skills.ts tickProjectiles', () => {
// Launch a charged bolt moving strictly East (vx = 12, vy = 0)
let p: Projectile = {
name: 'chargedbolt',
sourceSkillId: 38,
x: 500,
y: 500,
vx: 12,
vy: 0,
ttl: 50,
damage: 10,
radius: 8,
jitter: true,
jitterIndex: 0,
seed: 7,
}
const startY = p.y
let maxLateralDisplacement = 0
for (let t = 0; t < 35; t++) {
const state = tickProjectiles([p], [], { overlap: () => 0 })
if (state.alive.length === 0) break
p = state.alive[0]!
const dy = Math.abs(p.y - startY)
if (dy > maxLateralDisplacement) {
maxLateralDisplacement = dy
}
}
// Over 35 ticks, a genuine wandering path MUST exceed the obsolete 3px jitter bound
expect(maxLateralDisplacement).toBeGreaterThan(10)
// The bolt traveled forward along X
expect(p.x).toBeGreaterThan(700)
})
it('ensures distinct random seeds yield divergent trajectories', () => {
let p1: Projectile = {
name: 'chargedbolt',
sourceSkillId: 38,
x: 0,
y: 0,
vx: 10,
vy: 5,
ttl: 30,
damage: 10,
radius: 8,
jitter: true,
jitterIndex: 0,
seed: 1,
}
let p2: Projectile = {
name: 'chargedbolt',
sourceSkillId: 38,
x: 0,
y: 0,
vx: 10,
vy: 5,
ttl: 30,
damage: 10,
radius: 8,
jitter: true,
jitterIndex: 5,
seed: 29,
}
for (let t = 0; t < 25; t++) {
const s1 = tickProjectiles([p1], [], { overlap: () => 0 })
const s2 = tickProjectiles([p2], [], { overlap: () => 0 })
if (s1.alive.length === 0 || s2.alive.length === 0) break
p1 = s1.alive[0]!
p2 = s2.alive[0]!
}
// Both started at identical coordinates and base velocity, but different jitter state
// Their final positions must diverge
const distanceBetween = Math.hypot(p1.x - p2.x, p1.y - p2.y)
expect(distanceBetween).toBeGreaterThan(15)
})
})