/** * Adversarial Stress & Correctness Verification Suite — Milestone M4 * Skills: Thunder Storm (#057), Energy Shield (#058), Lightning Mastery (#063) * Author: reviewer_lightning_m4 (teamwork_preview_reviewer / critic) * Ground Truth: Diablo II v1.13c (D2Game.dll, D2Common.dll, Skills.txt, Missiles.txt) */ import { describe, expect, it } from 'vitest' 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 { executeSUnitDmg, type CombatUnitContext } from '../src/server/engine/combat-pipeline.ts' import { GameEngine } from '../src/server/engine/engine.ts' import { calculateThunderStormDuration, calculateThunderStormInterval, calculateThunderStormDamage, calculateEnergyShieldDuration, calculateEnergyShieldAbsorbPct, calculateEnergyShieldManaRatio, calculateLightningMasteryBonusPct, calculateChargedBoltDamage, calculateNovaDamage, calculateLightningDamage, calculateChainLightningDamage, calculateSkillDamage, BATCH1_SKILLS, CANONICAL_113C_MISSILES, } from '../src/server/skills/skills.ts' import { getSkillRange, getSkillMana } from '../src/client/world/act-runtime.ts' describe('Adversarial Stress Test: Milestone M4 (Skills 57, 58, 63)', () => { // ========================================================================= // 1. Thunder Storm (57) Adversarial Stress // ========================================================================= describe('1. Thunder Storm (57) Stress Testing', () => { it('stresses 5-band piecewise progression across 1..99 levels', () => { // Band 1: lvl 1..8 (+10/+10) expect(calculateThunderStormDamage(1)).toEqual({ min: 1, max: 100 }) expect(calculateThunderStormDamage(8)).toEqual({ min: 71, max: 170 }) // Band 2: lvl 9..16 (+10/+10) expect(calculateThunderStormDamage(9)).toEqual({ min: 81, max: 180 }) expect(calculateThunderStormDamage(16)).toEqual({ min: 151, max: 250 }) // Band 3: lvl 17..22 (+11/+11) expect(calculateThunderStormDamage(17)).toEqual({ min: 162, max: 261 }) expect(calculateThunderStormDamage(22)).toEqual({ min: 217, max: 316 }) // Band 4: lvl 23..28 (+11/+11) expect(calculateThunderStormDamage(23)).toEqual({ min: 228, max: 327 }) expect(calculateThunderStormDamage(28)).toEqual({ min: 283, max: 382 }) // Band 5: lvl 29+ (+11/+11) expect(calculateThunderStormDamage(29)).toEqual({ min: 294, max: 393 }) expect(calculateThunderStormDamage(40)).toEqual({ min: 415, max: 514 }) expect(calculateThunderStormDamage(99)).toEqual({ min: 1064, max: 1163 }) }) it('stresses cadence interval hyperbolic decay and floor clamping at 25 frames', () => { // Interval formula: max(25, 125 - floor(110 * slvl / (slvl + 6))) expect(calculateThunderStormInterval(1)).toBe(110) expect(calculateThunderStormInterval(5)).toBe(75) expect(calculateThunderStormInterval(10)).toBe(57) expect(calculateThunderStormInterval(20)).toBe(41) expect(calculateThunderStormInterval(30)).toBe(34) expect(calculateThunderStormInterval(40)).toBe(30) expect(calculateThunderStormInterval(50)).toBe(27) expect(calculateThunderStormInterval(60)).toBe(25) // exactly reaches 25 floor! expect(calculateThunderStormInterval(70)).toBe(25) expect(calculateThunderStormInterval(100)).toBe(25) expect(calculateThunderStormInterval(999)).toBe(25) }) it('stresses edge inputs for duration, interval, damage', () => { expect(calculateThunderStormDuration(0)).toBe(800) expect(calculateThunderStormDuration(-99)).toBe(800) expect(calculateThunderStormDuration(NaN as any)).toBe(800) expect(calculateThunderStormInterval(0)).toBe(110) expect(calculateThunderStormInterval(-10)).toBe(110) expect(calculateThunderStormInterval(NaN as any)).toBe(110) expect(calculateThunderStormDamage(0)).toEqual({ min: 1, max: 100 }) expect(calculateThunderStormDamage(-10)).toEqual({ min: 1, max: 100 }) expect(calculateThunderStormDamage(NaN as any)).toEqual({ min: 1, max: 100 }) }) it('stresses runtime GameEngine strike targeting nearest monster within 340px', () => { const terrain = { widthPx: 1280, heightPx: 960, overlap: (_x: number, _y: number) => 0, isMissileBlocked: () => false, } as any const engine = new GameEngine(terrain, { spawn: { x: 500, y: 500 }, monsterCount: 0, stats: [], skills: ['attack'] as any, xpTable: [0, 500], combatOptions: { playerSpeed: 100, playerReach: 40, playerCooldownTicks: 15, playerDamage: 10, playerManaPerAttack: 0, respawnTicks: 75, disableMonsterAggro: true, }, npcRingFallback: false, } as any) // Player at (500, 500) with active Thunder Storm (interval 1 tick for testing) engine.world.player.x = 500 engine.world.player.y = 500 engine.world.player.thunderstormTicks = 100 engine.world.player.thunderstormInterval = 1 engine.world.player.thunderstormNextStrike = 1 engine.world.player.thunderstormDamage = 200 // Add three monsters: // M0: dead monster at (510, 500) (dist 10) - must be ignored! // M1: live monster at (600, 500) (dist 100) - closest valid! // M2: live monster at (750, 500) (dist 250) - farther! // M3: live monster at (900, 500) (dist 400) - outside 340px! engine.world.monsters = [ { index: 0, id: 'm0', x: 510, y: 500, hp: 0, maxHp: 500, state: 'dead', stats: { name: 'm0', id: 'm0', speed: 50, damage: 10, cooldownTicks: 25 }, cooldown: 0 } as any, { index: 1, id: 'm1', x: 600, y: 500, hp: 500, maxHp: 500, state: 'idle', stats: { name: 'm1', id: 'm1', speed: 50, damage: 10, cooldownTicks: 25 }, cooldown: 0 } as any, { index: 2, id: 'm2', x: 750, y: 500, hp: 500, maxHp: 500, state: 'idle', stats: { name: 'm2', id: 'm2', speed: 50, damage: 10, cooldownTicks: 25 }, cooldown: 0 } as any, { index: 3, id: 'm3', x: 900, y: 500, hp: 500, maxHp: 500, state: 'idle', stats: { name: 'm3', id: 'm3', speed: 50, damage: 10, cooldownTicks: 25 }, cooldown: 0 } as any, ] // Tick engine by 1 frame engine.tick({ movement: { x: 0, y: 0 }, attacking: false, picking: false, dropping: null, digits: [], } as any) // M1 (closest alive inside 340px) should take 200 damage! expect(engine.world.monsters[1]?.hp).toBe(300) // M2 should take 0 damage expect(engine.world.monsters[2]?.hp).toBe(500) // M3 should take 0 damage expect(engine.world.monsters[3]?.hp).toBe(500) // Overlay light_cast_2 should spawn on M1 const ov = engine.overlays.find(o => o.overlayName === 'light_cast_2') expect(ov).toBeDefined() expect(ov?.x).toBe(600) expect(ov?.y).toBe(500) }) }) // ========================================================================= // 2. Energy Shield (58) Adversarial Stress // ========================================================================= describe('2. Energy Shield (58) Stress Testing', () => { it('stresses 95% cap across high skill levels', () => { expect(calculateEnergyShieldAbsorbPct(40)).toBe(95) expect(calculateEnergyShieldAbsorbPct(41)).toBe(95) expect(calculateEnergyShieldAbsorbPct(60)).toBe(95) expect(calculateEnergyShieldAbsorbPct(99)).toBe(95) expect(calculateEnergyShieldAbsorbPct(999)).toBe(95) }) it('stresses Telekinesis synergy ratio boundaries and soft point immutability', () => { // 0 hard points -> 2.0 expect(calculateEnergyShieldManaRatio(0)).toBe(2.0) // 1..20 hard points -> 1/16 less per point expect(calculateEnergyShieldManaRatio(1)).toBe(31 / 16) expect(calculateEnergyShieldManaRatio(8)).toBe(24 / 16) expect(calculateEnergyShieldManaRatio(16)).toBe(1.0) expect(calculateEnergyShieldManaRatio(20)).toBe(0.75) // 21+ points -> strictly clamped at 0.75 expect(calculateEnergyShieldManaRatio(21)).toBe(0.75) expect(calculateEnergyShieldManaRatio(40)).toBe(0.75) expect(calculateEnergyShieldManaRatio(99)).toBe(0.75) // Negative points -> strictly clamped at 2.0 expect(calculateEnergyShieldManaRatio(-1)).toBe(2.0) expect(calculateEnergyShieldManaRatio(-50)).toBe(2.0) }) it('stresses pre-resistance vs post-resistance mathematical difference', async () => { const registry = await getSharedDataRegistry() const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE }) const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE, maxmana: 2000 * FIXED_ONE, mana: 2000 * FIXED_ONE, lightresist: 75, // 75% Lightning Resistance }) // TK blvl = 16 (1.0 ratio) defenderStats.setBaseSkillLevel(43, 16) const attackerBus = new StateBus(attackerStats, registry) const defenderBus = new StateBus(defenderStats, registry) defenderBus.applyState({ stateNameOrId: 'energyshield', sourceSkillId: 58, slvl: 20, // 75% absorb }) const attackerCtx: CombatUnitContext = { id: 'a', name: 'A', statList: attackerStats, stateBus: attackerBus } const defenderCtx: CombatUnitContext = { id: 'd', name: 'D', statList: defenderStats, stateBus: defenderBus } // 1000 raw lightning damage const res = executeSUnitDmg(attackerCtx, defenderCtx, { skillId: 49, attackKind: 'spell', autoHit: true, elemType: 'ltng', elemMin256: 1000 * FIXED_ONE, elemMax256: 1000 * FIXED_ONE, }) // Ground truth 1.13c Pre-Resistance order: // 1. Raw: 1000 ltng. // 2. ES absorbs 75% = 750 damage. // 3. Mana drained: 750 * 1.0 = 750 mana. // 4. Remaining raw ltng: 250. // 5. 75% LightRes mitigates remaining 250 down to 62.5 (250 * 0.25). // 6. 256-fixed-point rounding: Math.ceil(16000 / 256) = 63. expect(defenderStats.getMana()).toBe(1250) // 2000 - 750 expect(res.totalDamage).toBe(63) expect(defenderStats.getHp()).toBe(938) }) it('stresses mixed physical and poison damage: physical is absorbed while poison 100% bypasses', async () => { const registry = await getSharedDataRegistry() const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE, maxmana: 1000 * FIXED_ONE, mana: 1000 * FIXED_ONE, }) stats.setBaseSkillLevel(43, 16) // 1.0 ratio const bus = new StateBus(stats, registry) bus.applyState({ stateNameOrId: 'energyshield', sourceSkillId: 58, slvl: 20, // 75% absorb }) const out = bus.triggerReactiveEvents({ event: 'absorbdamage', incomingPhys256: 200 * FIXED_ONE, incomingElem256: 100 * FIXED_ONE, elemType: 'pois', isPoison: true, }) // Physical: 75% of 200 = 150 absorbed, 50 remains expect(out.absorbedPhys256).toBe(150 * FIXED_ONE) // Poison: 0% absorbed! 100 remains expect(out.absorbedElem256).toBe(0) // Mana drained: only 150 mana (from physical), 0 from poison! expect(out.manaDrained256).toBe(150 * FIXED_ONE) expect(stats.getMana()).toBe(850) }) it('stresses extreme overkill damage and shield collapse', async () => { const registry = await getSharedDataRegistry() const stats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, maxmana: 100 * FIXED_ONE, mana: 10 * FIXED_ONE, // only 10 mana left! }) stats.setBaseSkillLevel(43, 0) // 2.0 ratio: 10 mana can only absorb 5 damage const bus = new StateBus(stats, registry) bus.applyState({ stateNameOrId: 'energyshield', sourceSkillId: 58, slvl: 20, // 75% absorb }) // Incoming: 50,000 damage const out = bus.triggerReactiveEvents({ event: 'absorbdamage', incomingPhys256: 50000 * FIXED_ONE, incomingElem256: 0, }) // 10 mana / 2.0 = 5 damage absorbed expect(out.absorbedPhys256).toBe(5 * FIXED_ONE) expect(out.manaDrained256).toBe(10 * FIXED_ONE) expect(stats.getMana()).toBe(0) // Shield collapsed expect(bus.hasState('energyshield')).toBe(false) expect(bus.hasState(60)).toBe(false) }) it('stresses 0 mana available: 0 absorbed, shield collapses instantly', async () => { const registry = await getSharedDataRegistry() const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE, maxmana: 100 * FIXED_ONE, mana: 0, // 0 mana! }) const bus = new StateBus(stats, registry) bus.applyState({ stateNameOrId: 'energyshield', sourceSkillId: 58, slvl: 20, }) const out = bus.triggerReactiveEvents({ event: 'absorbdamage', incomingPhys256: 500 * FIXED_ONE, incomingElem256: 0, }) expect(out.absorbedPhys256).toBe(0) expect(out.manaDrained256).toBe(0) expect(stats.getMana()).toBe(0) expect(bus.hasState('energyshield')).toBe(false) }) }) // ========================================================================= // 3. Lightning Mastery (63) Adversarial Stress // ========================================================================= describe('3. Lightning Mastery (63) Stress Testing', () => { it('stresses formula across extreme skill levels', () => { expect(calculateLightningMasteryBonusPct(1)).toBe(50) expect(calculateLightningMasteryBonusPct(10)).toBe(158) expect(calculateLightningMasteryBonusPct(20)).toBe(278) expect(calculateLightningMasteryBonusPct(40)).toBe(518) expect(calculateLightningMasteryBonusPct(99)).toBe(50 + 98 * 12) // 1226% expect(calculateLightningMasteryBonusPct(0)).toBe(0) expect(calculateLightningMasteryBonusPct(-5)).toBe(0) }) it('proves multiplicative scaling is strictly greater than additive scaling for all 5 skills', () => { // 1. Charged Bolt: Base * (1 + 1.20) * (1 + 2.78) vs Base * (1 + 1.20 + 2.78) const cbBase = calculateChargedBoltDamage(20) const cbBoth = calculateChargedBoltDamage(20, { lightningPoints: 20, lightningMastery: 20 }) const cbAdditiveMin = Math.floor(cbBase.min * (1 + 1.20 + 2.78)) const cbAdditiveMax = Math.floor(cbBase.max * (1 + 1.20 + 2.78)) expect(cbBoth.min).toBeGreaterThan(cbAdditiveMin) expect(cbBoth.max).toBeGreaterThan(cbAdditiveMax) // 2. Lightning: Base * (1 + 1.60) * (1 + 2.78) vs Base * (1 + 1.60 + 2.78) const ltBase = calculateLightningDamage(20) const ltBoth = calculateLightningDamage(20, { chainLightningPoints: 20, lightningMastery: 20 }) const ltAdditiveMax = Math.floor(ltBase.max * (1 + 1.60 + 2.78)) expect(ltBoth.max).toBeGreaterThan(ltAdditiveMax) // 3. Chain Lightning: Base * (1 + 0.80) * (1 + 2.78) vs Base * (1 + 0.80 + 2.78) const clBase = calculateChainLightningDamage(20) const clBoth = calculateChainLightningDamage(20, { lightningPoints: 20, lightningMastery: 20 }) const clAdditiveMax = Math.floor(clBase.max * (1 + 0.80 + 2.78)) expect(clBoth.max).toBeGreaterThan(clAdditiveMax) // 4. Nova: Base * (1 + 2.78) const novaBase = calculateNovaDamage(20) const novaMastered = calculateNovaDamage(20, { lightningMastery: 20 }) expect(novaMastered.min).toBe(Math.floor(novaBase.min * 3.78)) expect(novaMastered.max).toBe(Math.floor(novaBase.max * 3.78)) // 5. Thunder Storm: Base * (1 + 2.78) const tsBase = calculateThunderStormDamage(20) const tsMastered = calculateThunderStormDamage(20, { lightningMastery: 20 }) expect(tsMastered.min).toBe(Math.floor(tsBase.min * 3.78)) expect(tsMastered.max).toBe(Math.floor(tsBase.max * 3.78)) }) it('stresses elemental isolation in executeSUnitDmg (Cold, Fire, Poison, Magic untouched)', async () => { const registry = await getSharedDataRegistry() const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE }) const defenderStats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE }) // Level 40 Lightning Mastery (+518%) attackerStats.setBaseSkillLevel(63, 40) const attackerBus = new StateBus(attackerStats, registry) const defenderBus = new StateBus(defenderStats, registry) const attackerCtx: CombatUnitContext = { id: 'a', name: 'A', statList: attackerStats, stateBus: attackerBus } const defenderCtx: CombatUnitContext = { id: 'd', name: 'D', statList: defenderStats, stateBus: defenderBus } // Fire test const resFire = executeSUnitDmg(attackerCtx, defenderCtx, { skillId: 47, attackKind: 'spell', autoHit: true, elemType: 'fire', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, }) expect(resFire.totalDamage).toBe(100) // Cold test const resCold = executeSUnitDmg(attackerCtx, defenderCtx, { skillId: 59, attackKind: 'spell', autoHit: true, elemType: 'cold', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, }) expect(resCold.totalDamage).toBe(100) // Poison test const resPois = executeSUnitDmg(attackerCtx, defenderCtx, { skillId: 1, attackKind: 'spell', autoHit: true, elemType: 'pois', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, }) expect(resPois.totalDamage).toBe(100) // Lightning test -> should be scaled by +518%: 100 * 6.18 = 618! const resLtng = executeSUnitDmg(attackerCtx, defenderCtx, { skillId: 49, attackKind: 'spell', autoHit: true, elemType: 'ltng', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, }) expect(resLtng.totalDamage).toBe(618) }) }) // ========================================================================= // 4. Invariants & System Integrity // ========================================================================= describe('4. Invariants & System Integrity', () => { it('strictly preserves BATCH1_SKILLS length at 38', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) }) it('verifies thunderstorm1 canonical missile integrity', () => { const msl = CANONICAL_113C_MISSILES['thunderstorm1'] expect(msl).toBeDefined() expect(msl?.id).toBe(166) expect(msl?.celFile).toBe('LightningboltBig') expect(msl?.animLen).toBe(8) expect(msl?.trans).toBe(1) }) }) })