import { describe, expect, it } from 'vitest' import { calculateStaticFieldDamage, calculateStaticFieldRadiusPx, hasLineOfSight, MissileEngine, } from '../src/game/engine/missile-engine.ts' import { getSharedDataRegistry } from '../src/game/engine/data-registry.ts' import { UnitStatList, FIXED_ONE } from '../src/game/engine/stat-list.ts' import { StateBus } from '../src/game/engine/state-bus.ts' import type { CombatUnitContext } from '../src/game/engine/combat-pipeline.ts' import { monsterStatsOf, applyEliteModifiers, type MonsterKind } from '../src/game/monsters.ts' import { Rng } from '../src/game/rng.ts' import { GameEngine } from '../src/game/engine.ts' function makeUnit(registry: any, id: string, name: string, hp = 10000, lightRes = 0): CombatUnitContext { const statList = new UnitStatList(registry, { level: 80, maxhp: hp * FIXED_ONE, hitpoints: hp * FIXED_ONE, lightresist: lightRes, }) const stateBus = new StateBus(statList, registry) return { id, name, statList, stateBus, isDualClaw: false, hasShield: false, weaponMinPhys: 10, weaponMaxPhys: 20, } } describe('Adversarial Verification Suite (Commit 35b9267 Challenge Hardening)', () => { // ------------------------------------------------------------------------- // 1. Difficulty Floor Underflow Protection & Assembly Invariants // ------------------------------------------------------------------------- describe('1. Difficulty Floor Underflow Protection & Low Max HP', () => { it('protects 1 HP targets from underflow in Nightmare difficulty', () => { // For maxHp = 1 in Nightmare, floor(1 * 33 / 100) = 0, but clamped to Math.max(1, ...) const res = calculateStaticFieldDamage(1, 1, 'nightmare', 0) expect(res.damage).toBe(0) expect(res.newHp).toBe(1) expect(res.blockedByFloor).toBe(true) }) it('protects 1 HP targets from underflow in Hell difficulty', () => { // For maxHp = 1 in Hell, floor(1 * 50 / 100) = 0, but clamped to Math.max(1, ...) const res = calculateStaticFieldDamage(1, 1, 'hell', 0) expect(res.damage).toBe(0) expect(res.newHp).toBe(1) expect(res.blockedByFloor).toBe(true) }) it('protects 2 HP targets in Hell difficulty where floor is at least 1', () => { const res = calculateStaticFieldDamage(1, 2, 'hell', 0) expect(res.damage).toBe(0) expect(res.newHp).toBe(1) expect(res.blockedByFloor).toBe(true) }) it('faithfully reproduces 1.13c assembly pre-check when above floor', () => { // In Hell: maxHp = 10000, floor = 5000. Unit has 5001 HP. // D2Game.dll 0x6FC62F13: currentHp > hpFloor -> proceeds with 25% cut (1250) -> 3751 HP const res = calculateStaticFieldDamage(5001, 10000, 'hell', 0) expect(res.damage).toBe(1250) expect(res.newHp).toBe(3751) expect(res.blockedByFloor).toBe(false) // Subsequent cast on 3751 HP is now blocked by floor (3751 <= 5000) const resNext = calculateStaticFieldDamage(res.newHp, 10000, 'hell', 0) expect(resNext.damage).toBe(0) expect(resNext.newHp).toBe(3751) expect(resNext.blockedByFloor).toBe(true) }) }) // ------------------------------------------------------------------------- // 2. Negative Resistance Amplification & Non-Lethal Invariant // ------------------------------------------------------------------------- describe('2. Negative Resistance Amplification & Non-Lethal Invariant', () => { it('scales damage up with negative lightning resistance (Lower Resist / Conviction)', () => { // Baseline 0% res: 250 damage const base = calculateStaticFieldDamage(1000, 1000, 'normal', 0) expect(base.damage).toBe(250) // -50% res: floor(250 * 150 / 100) = 375 damage const neg50 = calculateStaticFieldDamage(1000, 1000, 'normal', -50) expect(neg50.damage).toBe(375) expect(neg50.newHp).toBe(625) // -100% res (D2 lower cap): floor(250 * 200 / 100) = 500 damage (doubled!) const neg100 = calculateStaticFieldDamage(1000, 1000, 'normal', -100) expect(neg100.damage).toBe(500) expect(neg100.newHp).toBe(500) }) it('CRITICAL SAFETY INVARIANT: Static Field NEVER kills, even under extreme negative resistance', () => { // Current HP = 4: base damage = 1. // With -300% resistance, damage without clamp would be 1 * 400 / 100 = 4 (killing target 4 - 4 = 0). // Post-scaling non-lethal clamp Math.min(damage, currentHp - 1) forces damage <= 3, newHp >= 1. const res = calculateStaticFieldDamage(4, 100, 'normal', -300) expect(res.damage).toBeLessThanOrEqual(3) expect(res.newHp).toBeGreaterThanOrEqual(1) // Current HP = 2: base damage = 0. const res2 = calculateStaticFieldDamage(2, 100, 'normal', -100) expect(res2.damage).toBe(0) expect(res2.newHp).toBe(2) // Current HP = 1: blocked by floor. const res1 = calculateStaticFieldDamage(1, 100, 'normal', -100) expect(res1.damage).toBe(0) expect(res1.newHp).toBe(1) expect(res1.blockedByFloor).toBe(true) }) }) // ------------------------------------------------------------------------- // 3. Monster Lightning Resistance & Immunity Wiring // ------------------------------------------------------------------------- describe('3. Monster Lightning Resistance & Immunity Wiring', () => { it('monsterStatsOf correctly populates resistances and lightningResist', () => { const dummyKind = { id: 'stormcaster', nameKey: 'Storm Caster', minHp: 100, maxHp: 100, velocity: 8, aiDistance: 10, meleeRange: 1, experience: 50, level: [15, 45, 75], attack1: { minDamage: 5, maxDamage: 10, toHit: 50 }, attack2: { minDamage: 0, maxDamage: 0, toHit: 0 }, resistances: { physical: 0, magic: 0, fire: 25, lightning: 100, // Immune! cold: 0, poison: 0, }, treasureClasses: [], minions: [], setBoss: false, isSpawn: true, enabled: true, rarity: 1, sparsePopulate: 1, } const stats = monsterStatsOf(dummyKind as unknown as MonsterKind, new Rng(1234), 160) expect(stats.lightningResist).toBe(100) expect(stats.resistances?.lightning).toBe(100) expect(stats.resists?.lightning).toBe(100) // Static Field against this immune monster deals 0 damage const sfRes = calculateStaticFieldDamage(stats.hp, stats.hp, 'normal', stats.lightningResist) expect(sfRes.damage).toBe(0) expect(sfRes.immune).toBe(true) }) it('applyEliteModifiers scales lightning resistance for LightEnchant and MagicResistant', () => { const baseStats = { id: 'fallen', name: 'Fallen', hp: 100, damage: 10, cooldownTicks: 25, reach: 20, aggroRadius: 100, speed: 100, xp: 20, lightningResist: 20, resistances: { physical: 0, magic: 0, fire: 0, lightning: 20, cold: 0, poison: 0 }, } // Light Enchant adds +75 lightning resist: 20 + 75 = 95 const eliteLight = applyEliteModifiers(baseStats, 'unique', ['lightenchant']) expect(eliteLight.lightningResist).toBe(95) expect(eliteLight.resistances?.lightning).toBe(95) // Light Enchant + Magic Resistant (+20): 20 + 75 + 20 = 115 (Immune!) const eliteBoth = applyEliteModifiers(baseStats, 'unique', ['lightenchant', 'magicresistant']) expect(eliteBoth.lightningResist).toBe(115) expect(eliteBoth.resistances?.lightning).toBe(115) const sfRes = calculateStaticFieldDamage(eliteBoth.hp, eliteBoth.hp, 'normal', eliteBoth.lightningResist) expect(sfRes.damage).toBe(0) expect(sfRes.immune).toBe(true) }) }) // ------------------------------------------------------------------------- // 4. Continuous Terrain Collision & Tunneling Prevention // ------------------------------------------------------------------------- describe('4. Continuous Terrain Collision & Tunneling Prevention', () => { it('MissileEngine.tick does not tunnel through thin 10px walls at speed 24', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const owner = makeUnit(registry, 'player', 'Sorceress') // Wall between x=10 and x=20 const terrain = { overlap: (x: number, _y: number) => (x >= 10 && x <= 20 ? 1 : 0), isMissileBlocked: (x: number, _y: number) => x >= 10 && x <= 20, } // Spawn a missile at x=0 with vx=24 (jumping over 10..20 in one frame if point-sampled) const m = engine.spawnMissile({ missileNameOrId: 'nova', sourceSkillId: 48, slvl: 20, owner, startX: 0, startY: 0, targetX: 100, targetY: 0, vx: 24, vy: 0, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'lightning', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, }, }) expect(m).toBeDefined() // Tick 1: Continuous sub-stepping must detect collision between x=0 and x=24 const step = engine.tick(1, [], new Map(), terrain) expect(m!.expired).toBe(true) }) it('hasLineOfSight detects thin walls and correctly passes previous step coordinates', () => { // Wall between x=20 and x=28 (8px wide) const isBlocked = (x: number, _y: number) => x >= 20 && x <= 28 const hasLos = hasLineOfSight(0, 0, 40, 0, isBlocked) expect(hasLos).toBe(false) const openLos = hasLineOfSight(0, 0, 18, 0, isBlocked) expect(openLos).toBe(true) }) }) // ------------------------------------------------------------------------- // 5. Persistent NextDelay & Memory Leak Pruning in GameEngine // ------------------------------------------------------------------------- describe('5. Persistent NextDelay & Memory Leak Pruning in GameEngine', () => { it('GameEngine maintains persistent nextDelayByTarget across ticks and prunes expired entries', () => { const terrain = { widthPx: 1000, heightPx: 1000, overlap: () => 0, } const engine = new GameEngine(terrain as any, { spawn: { x: 100, y: 100 }, stats: [], monsterCount: 0, skills: [], npcDefs: [], combatOptions: { playerSpeed: 100, playerReach: 50, playerCooldownTicks: 10, playerDamage: 5, playerManaPerAttack: 0, respawnTicks: 100, }, } as any) expect(engine.nextDelayByTarget).toBeDefined() expect(engine.nextDelayByTarget.size).toBe(0) // Simulate a NextDelay entry set for target 1 expiring at tick 4 engine.nextDelayByTarget.set(1, 4) engine.nextDelayByTarget.set(2, 50) // Expires at tick 50 // Advance engine to tick 25 ;(engine.world as any).tick = 25 engine.tick({ movement: { x: 0, y: 0 }, attacking: false, digits: [] } as any) // At tick 25, entry 1 (expired at tick 4) must be pruned! Entry 2 (expires tick 50) remains! expect(engine.nextDelayByTarget.has(1)).toBe(false) expect(engine.nextDelayByTarget.has(2)).toBe(true) }) }) })