import { describe, expect, it } from 'vitest' import { tickProjectiles, spawnNovaRing, calculateNovaDamage, calculateStaticFieldDamage, calculateStaticFieldRadiusYards, calculateStaticFieldRadiusPx, hasLineOfSight, getSkillManaCost, getSkillCooldownTicks, ISO_GROUND_ASPECT_RATIO, type Projectile, } from '../../../src/server/skills/skills.ts' import { MissileEngine, spawnNovaRing as spawnEngineNovaRing, executeStaticField, } from '../../../src/server/engine/missile-engine.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { UnitStatList, FIXED_ONE } from '../../../src/server/engine/stat-list.ts' import { StateBus } from '../../../src/server/engine/state-bus.ts' import type { CombatUnitContext } from '../../../src/server/engine/combat-pipeline.ts' import { WorldArena } from '../../../src/server/engine/world-arena.ts' import { executeSkillCore113c } from '../../../src/server/skills/registry.ts' function makeUnit(registry: any, id: string, name: string, hp = 10000): CombatUnitContext { const statList = new UnitStatList(registry, { level: 80, maxhp: hp * FIXED_ONE, hitpoints: hp * FIXED_ONE, }) const stateBus = new StateBus(statList, registry) return { id, name, statList, stateBus, isDualClaw: false, hasShield: false, weaponMinPhys: 10, weaponMaxPhys: 20, } } describe('Empirical Challenger: Nova (48) Adversarial Stress Tests', () => { // ------------------------------------------------------------------------- // 1. NextDelay 4 Single-Hit Protection under Overlapping Bolt Clusters // ------------------------------------------------------------------------- describe('1. NextDelay 4 Single-Hit Protection under Overlapping Bolt Clusters', () => { it('MissileEngine: 64 overlapping bolts at point-blank hit target exactly once across 13 frames', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const owner = makeUnit(registry, 'player', 'Sorceress') const target = makeUnit(registry, 'dummy-point-blank', 'Target Dummy', 10000) // Point blank target at (5, 0) const targetPositions = new Map([ ['dummy-point-blank', { x: 5, y: 0 }], ]) const bolts = engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'lightning', elemMin256: 131 * FIXED_ONE, elemMax256: 188 * FIXED_ONE, autoHit: true, }, }) expect(bolts.length).toBe(64) let totalHits = 0 const hitTicks: number[] = [] for (let tick = 1; tick <= 15; tick++) { const step = engine.tick(tick, [target], targetPositions) for (const h of step.hits) { if (h.targetId === 'dummy-point-blank') { totalHits += 1 hitTicks.push(tick) } } } // Must be hit EXACTLY once, never multiple times from a single Nova ring expect(totalHits).toBe(1) expect(hitTicks).toEqual([1]) }) it('MissileEngine: Large radius boss monster is hit exactly once, not multiple times by adjacent bolts', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const owner = makeUnit(registry, 'player', 'Sorceress') const boss = makeUnit(registry, 'boss-duriel', 'Duriel', 50000) // Boss at (40, 0), where multiple radial bolts spread out and pass near/through the boss const targetPositions = new Map([ ['boss-duriel', { x: 40, y: 0 }], ]) engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'lightning', elemMin256: 150 * FIXED_ONE, elemMax256: 150 * FIXED_ONE, autoHit: true, }, }) let totalHits = 0 const hitTicks: number[] = [] for (let tick = 1; tick <= 15; tick++) { const step = engine.tick(tick, [boss], targetPositions) for (const h of step.hits) { if (h.targetId === 'boss-duriel') { totalHits += 1 hitTicks.push(tick) } } } expect(totalHits).toBe(1) }) it('VULNERABILITY: tickProjectiles allows multi-hit across ticks if options.nextDelayByTarget is not passed (as in engine.ts:428)', () => { // Point-blank target at (15, 0) with radius 25 const target = { index: 1, x: 15, y: 0, radius: 25, alive: true } const bolts = spawnNovaRing(0, 0, 100) const terrain = { overlap: () => 0 } // Tick 1 (as invoked by src/server/engine/engine.ts line 428: tickProjectiles(this.projectiles, targets, this.terrain)) const res1 = tickProjectiles(bolts, [target], terrain) expect(res1.hits.length).toBe(1) expect(res1.hits[0]!.targetIndex).toBe(1) // Tick 2: Without nextDelayByTarget, the target takes damage AGAIN on tick 2! const res2 = tickProjectiles(res1.alive, [target], terrain) // Empirically confirms that if nextDelayByTarget is omitted by caller, NextDelay is violated across frames! expect(res2.hits.length).toBe(1) // Tick 3: bolt has moved out of collision range (48px > 25px radius + 8px missile radius) const res3 = tickProjectiles(res2.alive, [target], terrain) expect(res3.hits.length).toBe(0) }) }) // ------------------------------------------------------------------------- // 2. Rapid Casting & NextDelay Lockout Window // ------------------------------------------------------------------------- describe('2. Rapid Casting & NextDelay Lockout Window', () => { it('MissileEngine: Second Nova cast within 4 frames of hit is locked out; cast after 4 frames hits', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const owner = makeUnit(registry, 'player', 'Sorceress') const target = makeUnit(registry, 'dummy-rapid', 'Dummy Rapid', 20000) // Target at distance 24 px (x=24, y=0) const targetPositions = new Map([ ['dummy-rapid', { x: 24, y: 0 }], ]) const dmgPacket = { skillId: 48, attackKind: 'spell' as const, elemType: 'lightning' as const, elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, } // Cast 1 at tick 1: Nova travels 24 px at tick 1 and hits target at tick 1! engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket }) const step1 = engine.tick(1, [target], targetPositions) expect(step1.hits.length).toBe(1) expect(step1.hits[0]!.targetId).toBe('dummy-rapid') // Target entered NextDelay lockout at tick 1: NextDelay = 4 frames -> expires at tick 1 + 4 = 5. // Frames 2, 3, 4 are LOCKED OUT! Frame 5: exp is 5, and exp > currentTick (5 > 5) is false, so tick 5 is allowed. // Rapid Cast 2 at tick 2: Target is at distance 24 px, Nova 2 reaches target at tick 2! engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket }) const step2 = engine.tick(2, [target], targetPositions) // Must be locked out by NextDelay! expect(step2.hits.filter(h => h.targetId === 'dummy-rapid').length).toBe(0) // Tick 3: still locked out const step3 = engine.tick(3, [target], targetPositions) expect(step3.hits.filter(h => h.targetId === 'dummy-rapid').length).toBe(0) // Tick 4: still locked out const step4 = engine.tick(4, [target], targetPositions) expect(step4.hits.filter(h => h.targetId === 'dummy-rapid').length).toBe(0) // Cast 3 at tick 5: Nova 3 spawned at tick 5, reaches target at tick 5! engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket }) const step5 = engine.tick(5, [target], targetPositions) // Lockout expired, hit must register! expect(step5.hits.filter(h => h.targetId === 'dummy-rapid').length).toBe(1) }) }) // ------------------------------------------------------------------------- // 3. Multi-Monster Collision Deduping // ------------------------------------------------------------------------- describe('3. Multi-Monster Collision Deduping', () => { it('MissileEngine: 8 enemies encircling the player each take exactly 1 hit from the single Nova ring', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const owner = makeUnit(registry, 'player', 'Sorceress') const enemies: CombatUnitContext[] = [] const enemyPositions = new Map() // 8 enemies distributed at angle k * pi / 4, distance 50 px (taking into account 2:1 aspect ratio) for (let i = 0; i < 8; i++) { const id = `encircling-enemy-${i}` const angle = (i / 8) * 2 * Math.PI const x = Math.cos(angle) * 50 const y = Math.sin(angle) * 50 * ISO_GROUND_ASPECT_RATIO enemies.push(makeUnit(registry, id, `Enemy ${i}`, 10000)) enemyPositions.set(id, { x, y }) } engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'lightning', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, }, }) const hitsPerEnemy = new Map() for (let tick = 1; tick <= 15; tick++) { const step = engine.tick(tick, enemies, enemyPositions) for (const h of step.hits) { hitsPerEnemy.set(h.targetId, (hitsPerEnemy.get(h.targetId) ?? 0) + 1) } } // Every single one of the 8 enemies must be hit EXACTLY once! expect(hitsPerEnemy.size).toBe(8) for (let i = 0; i < 8; i++) { expect(hitsPerEnemy.get(`encircling-enemy-${i}`)).toBe(1) } }) it('MissileEngine: Two enemies placed close together along the same ray (front and back)', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const owner = makeUnit(registry, 'player', 'Sorceress') // Front enemy at (24, 0), Back enemy at (72, 0) const front = makeUnit(registry, 'front-mob', 'Front Mob', 5000) const back = makeUnit(registry, 'back-mob', 'Back Mob', 5000) const posMap = new Map([ ['front-mob', { x: 24, y: 0 }], ['back-mob', { x: 72, y: 0 }], ]) engine.spawnNovaRing({ owner, startX: 0, startY: 0, slvl: 20, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'lightning', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, }, }) const hitsPerEnemy = new Map() for (let tick = 1; tick <= 15; tick++) { const step = engine.tick(tick, [front, back], posMap) for (const h of step.hits) { hitsPerEnemy.set(h.targetId, (hitsPerEnemy.get(h.targetId) ?? 0) + 1) } } // Front mob is hit by bolt directly at (24, 0) expect(hitsPerEnemy.get('front-mob')).toBe(1) // Since nova has 64 bolts with angle spacing 5.625 degrees, does back-mob get hit by adjacent bolt or does front absorb? // Adjacent bolts spread out as distance increases: at x=72, angle 5.625 deg gives y = 72 * tan(5.625) = 7.1 px, // which is well within hitRadius (28 px)! expect(hitsPerEnemy.get('back-mob')).toBe(1) }) }) // ------------------------------------------------------------------------- // 4. 2:1 Isometric Aspect Ratio 0.5 // ------------------------------------------------------------------------- describe('4. 2:1 Isometric Aspect Ratio 0.5', () => { it('verifies exact 2:1 ground aspect ratio kinematics: vy is always vx * 0.5 at equal angles', () => { const bolts = spawnNovaRing(0, 0, 50) for (const b of bolts) { // Vy / Vx ratio must match tan(theta) * 0.5 const theta = Math.atan2(b.vy / ISO_GROUND_ASPECT_RATIO, b.vx) const expectedVx = Math.cos(theta) * 24 const expectedVy = Math.sin(theta) * 24 * ISO_GROUND_ASPECT_RATIO expect(b.vx).toBeCloseTo(expectedVx, 5) expect(b.vy).toBeCloseTo(expectedVy, 5) } }) it('verifies max vertical displacement is exactly half of max horizontal displacement', () => { const bolts = spawnNovaRing(0, 0, 50) const maxX = Math.max(...bolts.map(b => b.vx)) * 13 // ttl 13 const maxY = Math.max(...bolts.map(b => b.vy)) * 13 expect(maxX).toBeCloseTo(24 * 13, 2) // 312 px expect(maxY).toBeCloseTo(12 * 13, 2) // 156 px expect(maxY / maxX).toBeCloseTo(0.5, 4) }) }) }) describe('Empirical Challenger: Static Field (42) Adversarial Stress Tests', () => { // ------------------------------------------------------------------------- // 5. Integer Division Truncation (2 HP, 3 HP, 4 HP, 5 HP, etc.) // ------------------------------------------------------------------------- describe('5. Integer Division Truncation', () => { it('truncates 25% damage to 0 at 2 HP and 3 HP, never reducing HP below 1', () => { // 1 HP: Pre-check invariant returns 0 damage immediately const r1 = calculateStaticFieldDamage(1, 100, 'normal', 0) expect(r1.damage).toBe(0) expect(r1.newHp).toBe(1) expect(r1.blockedByFloor).toBe(true) // 2 HP: floor(2 * 0.25) = 0 damage. Target remains at 2 HP const r2 = calculateStaticFieldDamage(2, 100, 'normal', 0) expect(r2.damage).toBe(0) expect(r2.newHp).toBe(2) expect(r2.blockedByFloor).toBe(false) // 3 HP: floor(3 * 0.25) = 0 damage. Target remains at 3 HP const r3 = calculateStaticFieldDamage(3, 100, 'normal', 0) expect(r3.damage).toBe(0) expect(r3.newHp).toBe(3) expect(r3.blockedByFloor).toBe(false) // 4 HP: floor(4 * 0.25) = 1 damage. Target reduced to 3 HP const r4 = calculateStaticFieldDamage(4, 100, 'normal', 0) expect(r4.damage).toBe(1) expect(r4.newHp).toBe(3) expect(r4.blockedByFloor).toBe(false) // Subsequent cast on 3 HP deals 0 damage const r4_next = calculateStaticFieldDamage(r4.newHp, 100, 'normal', 0) expect(r4_next.damage).toBe(0) expect(r4_next.newHp).toBe(3) // 5 HP: floor(5 * 0.25) = 1 damage. Target reduced to 4 HP const r5 = calculateStaticFieldDamage(5, 100, 'normal', 0) expect(r5.damage).toBe(1) expect(r5.newHp).toBe(4) // 7 HP: floor(7 * 0.25) = 1 damage. Target reduced to 6 HP const r7 = calculateStaticFieldDamage(7, 100, 'normal', 0) expect(r7.damage).toBe(1) expect(r7.newHp).toBe(6) // 8 HP: floor(8 * 0.25) = 2 damage. Target reduced to 6 HP const r8 = calculateStaticFieldDamage(8, 100, 'normal', 0) expect(r8.damage).toBe(2) expect(r8.newHp).toBe(6) }) }) // ------------------------------------------------------------------------- // 6. Difficulty Floors under Nightmare (33%) and Hell (50%) // ------------------------------------------------------------------------- describe('6. Difficulty Floors under Nightmare (33%) and Hell (50%)', () => { it('Nightmare difficulty: floor is floor(maxHp * 33 / 100)', () => { const maxHp = 1000 const floor = Math.floor((maxHp * 33) / 100) // 330 // Case A: HP is at 330 (exactly at floor) -> 0 damage, blockedByFloor const rAtFloor = calculateStaticFieldDamage(330, maxHp, 'nightmare', 0) expect(rAtFloor.damage).toBe(0) expect(rAtFloor.newHp).toBe(330) expect(rAtFloor.blockedByFloor).toBe(true) // Case B: HP is at 329 (below floor) -> 0 damage, blockedByFloor const rBelow = calculateStaticFieldDamage(329, maxHp, 'nightmare', 0) expect(rBelow.damage).toBe(0) expect(rBelow.newHp).toBe(329) expect(rBelow.blockedByFloor).toBe(true) // Case C: HP is at 331 (above floor) -> cuts 25% of 331 = 82 -> 249 HP const rAbove = calculateStaticFieldDamage(331, maxHp, 'nightmare', 0) expect(rAbove.damage).toBe(82) expect(rAbove.newHp).toBe(249) expect(rAbove.blockedByFloor).toBe(false) // Follow-up cast on 249 HP -> blocked by floor! const rNext = calculateStaticFieldDamage(rAbove.newHp, maxHp, 'nightmare', 0) expect(rNext.damage).toBe(0) expect(rNext.newHp).toBe(249) expect(rNext.blockedByFloor).toBe(true) }) it('Hell difficulty: floor is floor(maxHp * 50 / 100)', () => { const maxHp = 1000 const floor = Math.floor((maxHp * 50) / 100) // 500 // Case A: HP is at 500 (exactly at floor) -> 0 damage, blockedByFloor const rAtFloor = calculateStaticFieldDamage(500, maxHp, 'hell', 0) expect(rAtFloor.damage).toBe(0) expect(rAtFloor.newHp).toBe(500) expect(rAtFloor.blockedByFloor).toBe(true) // Case B: HP is at 499 (below floor) -> 0 damage, blockedByFloor const rBelow = calculateStaticFieldDamage(499, maxHp, 'hell', 0) expect(rBelow.damage).toBe(0) expect(rBelow.newHp).toBe(499) expect(rBelow.blockedByFloor).toBe(true) // Case C: HP is at 501 (above floor) -> cuts 25% of 501 = 125 -> 376 HP const rAbove = calculateStaticFieldDamage(501, maxHp, 'hell', 0) expect(rAbove.damage).toBe(125) expect(rAbove.newHp).toBe(376) expect(rAbove.blockedByFloor).toBe(false) // Follow-up cast on 376 HP -> blocked by floor! const rNext = calculateStaticFieldDamage(rAbove.newHp, maxHp, 'hell', 0) expect(rNext.damage).toBe(0) expect(rNext.newHp).toBe(376) expect(rNext.blockedByFloor).toBe(true) }) it('Odd Max HP values in Nightmare and Hell calculate floor correctly with integer truncation', () => { // maxHp = 333: NM floor is floor(333 * 33 / 100) = floor(109.89) = 109 expect(calculateStaticFieldDamage(109, 333, 'nightmare', 0).blockedByFloor).toBe(true) expect(calculateStaticFieldDamage(110, 333, 'nightmare', 0).blockedByFloor).toBe(false) // maxHp = 777: Hell floor is floor(777 * 50 / 100) = floor(388.5) = 388 expect(calculateStaticFieldDamage(388, 777, 'hell', 0).blockedByFloor).toBe(true) expect(calculateStaticFieldDamage(389, 777, 'hell', 0).blockedByFloor).toBe(false) }) }) // ------------------------------------------------------------------------- // 7. Immunity Check at >= 100% Lightning Resistance // ------------------------------------------------------------------------- describe('7. Immunity Check at >= 100% Lightning Resistance', () => { it('returns 0 damage and immune=true for resistance 100%, 110%, 150%', () => { for (const res of [100, 105, 110, 150, 200]) { const out = calculateStaticFieldDamage(1000, 1000, 'normal', res) expect(out.damage).toBe(0) expect(out.newHp).toBe(1000) expect(out.immune).toBe(true) expect(out.blockedByFloor).toBe(false) } }) it('deals damage at resistance 99% (floor(250 * 1 / 100) = 2 damage)', () => { const out = calculateStaticFieldDamage(1000, 1000, 'normal', 99) expect(out.damage).toBe(2) expect(out.newHp).toBe(998) expect(out.immune).toBe(false) }) it('floor pre-check takes precedence over immunity check when target is already below floor', () => { // Hell difficulty: target has 400 HP / 1000 max HP (below 500 floor), and 110% light resist const out = calculateStaticFieldDamage(400, 1000, 'hell', 110) expect(out.damage).toBe(0) expect(out.newHp).toBe(400) expect(out.blockedByFloor).toBe(true) expect(out.immune).toBe(false) }) }) // ------------------------------------------------------------------------- // 8. Line-of-Sight Terrain Blocking // ------------------------------------------------------------------------- describe('8. Line-of-Sight Terrain Blocking', () => { it('blocks damage to targets behind solid walls across multiple ray angles', () => { // Solid wall from x=30 to x=50, y from -100 to 100 const isBlocked = (x: number, y: number) => x >= 30 && x <= 50 && y >= -100 && y <= 100 const targets = [ { id: 'behind-wall-1', x: 60, y: 0, currentHp: 1000, maxHp: 1000 }, { id: 'behind-wall-2', x: 70, y: 20, currentHp: 1000, maxHp: 1000 }, { id: 'behind-wall-3', x: 80, y: -30, currentHp: 1000, maxHp: 1000 }, { id: 'open-field-1', x: -60, y: 0, currentHp: 1000, maxHp: 1000 }, { id: 'open-field-2', x: 0, y: 80, currentHp: 1000, maxHp: 1000 }, ] const res = executeStaticField({ casterX: 0, casterY: 0, slvl: 5, // radius: 9 yards = 360 px targets, difficulty: 'normal', isBlocked, }) const behind1 = res.hits.find(h => h.targetId === 'behind-wall-1')! const behind2 = res.hits.find(h => h.targetId === 'behind-wall-2')! const behind3 = res.hits.find(h => h.targetId === 'behind-wall-3')! const open1 = res.hits.find(h => h.targetId === 'open-field-1')! const open2 = res.hits.find(h => h.targetId === 'open-field-2')! expect(behind1.blockedByLos).toBe(true) expect(behind1.damage).toBe(0) expect(behind2.blockedByLos).toBe(true) expect(behind2.damage).toBe(0) expect(behind3.blockedByLos).toBe(true) expect(behind3.damage).toBe(0) expect(open1.blockedByLos).toBe(false) expect(open1.damage).toBe(250) expect(open2.blockedByLos).toBe(false) expect(open2.damage).toBe(250) }) it('hasLineOfSight correctly handles step intervals and edge-touching obstacles', () => { // Wall between x=15 and x=17 const thinWall = (x: number) => x >= 15 && x <= 17 expect(hasLineOfSight(0, 0, 30, 0, thinWall)).toBe(false) expect(hasLineOfSight(0, 0, 10, 0, thinWall)).toBe(true) expect(hasLineOfSight(20, 0, 30, 0, thinWall)).toBe(true) }) }) // ------------------------------------------------------------------------- // 9. WorldArena & executeSkillCore113c Architecture Alignment // ------------------------------------------------------------------------- describe('9. WorldArena & executeSkillCore113c Architecture Alignment', () => { it('audits WorldArena behavior for Static Field (Skill 42)', async () => { const registry = await getSharedDataRegistry() const arena = new WorldArena({ registry, classCode: 'sor', skillId: 42, slvl: 20, }) const dummyNormal = arena.targets.find(t => t.id === 'dummy-normal')! const initialHp = dummyNormal.statList.getHp() expect(initialHp).toBe(10000) arena.triggerCast() arena.stepTicks(15) const finalHp = dummyNormal.statList.getHp() expect(finalHp).toBe(7500) // Cut by 25% via combat-pipeline executeSUnitDmg // However, combat-pipeline executeSUnitDmg lacks difficulty floors and LOS checking! }) it('audits WorldArena behavior for Nova (Skill 48) — verifies 64-bolt ring and deferred missile damage', async () => { const registry = await getSharedDataRegistry() const arena = new WorldArena({ registry, classCode: 'sor', skillId: 48, slvl: 20, }) const outcome = arena.triggerCast() // executeSkillCore113c spawns authentic 64-bolt circular ring expect(outcome?.missilesSpawned.length).toBe(64) expect(outcome?.missilesSpawned[0]).toBe('nova') // Frame 0 cast deals 0 direct damage; damage is delivered as missiles travel expect(outcome?.totalDamageDealt).toBe(0) // Step simulation for 15 frames: Nova bolts expand and strike dummies arena.stepTicks(15) const dummyNormal = arena.targets.find(t => t.id === 'dummy-normal')! expect(dummyNormal.statList.getHp()).toBeLessThan(10000) }) }) })