/** * Diablo II: Lord of Destruction v1.13c — Challenger Adversarial Stress Suite 2 * Focus: Amazon Javelin & Summons Parity Verification * * Targeted Systems: * 1. Lightning Fury (#035): Charged bolt cascade on impact, multi-target emission, and 4-pierce cascade recursion. * 2. Lightning Strike (#034): Chain hopping across targets with 4-frame NextHitDelay and range bounds. * 3. Decoy (#028): Threat drawing, life scaling from player HP, max resist cap (85%), and stationary posture. * 4. Valkyrie (#032): Base life scaling, Decoy synergy (+20% life per hard point), equipment tiering, 6.0s cooldown. * 5. Slow Missiles (#017): Exact 33% velocity multiplier and duration scaling. * 6. Inner Sight (#008): Exact flat defense strip scaling and combat to-hit integration. * * Ground Truth Invariants: * - Blizzard v1.13c: D2Common.dll, D2Game.dll, Skills.txt, Missiles.txt * - Zero mocks utilized (vi.mock strictly forbidden). */ 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 { MissileEngine, ISO_GROUND_ASPECT_RATIO } from '../../../src/server/engine/missile-engine.ts' import { AuraScanner } from '../../../src/server/engine/aura-scanner.ts' import { SummonManager } from '../../../src/server/ai/summon-manager.ts' import { computeToHitChance, type CombatUnitContext, type SUnitDmgPacket, } from '../../../src/server/engine/combat-pipeline.ts' import { calculateLightningFuryStats, calculateLightningStrikeStats, } from '../../../src/common/skills/amazon-javelin-spear.ts' import { calculateDecoyStats, calculateValkyrieStats, calculateSlowMissilesStats, calculateInnerSightStats, type ValkyrieEquipmentTier, } from '../../../src/common/skills/amazon-passive.ts' import { getSkillCooldownTicks } from '../../../src/server/skills/skills.ts' function createCombatUnit( id: string, pos: { x: number; y: number }, registry: any, opts?: { hp?: number def?: number ar?: number level?: number isMoving?: boolean } ): CombatUnitContext { const hp = opts?.hp ?? 1000 const statList = new UnitStatList(registry, { level: opts?.level ?? 80, hitpoints: hp * FIXED_ONE, maxhp: hp * FIXED_ONE, armorclass: opts?.def ?? 400, tohit: opts?.ar ?? 1000, fireresist: 0, coldresist: 0, lightresist: 0, poisonresist: 0, damageresist: 0, magicresist: 0, x: pos.x, y: pos.y, }) const stateBus = new StateBus(statList, registry) return { id, name: id, statList, stateBus, isMoving: opts?.isMoving ?? false, x: pos.x, y: pos.y, weaponMinPhys: 50, weaponMaxPhys: 100, } } describe('Challenger Adversarial Stress Suite 2 — Amazon Javelin & Summons (1.13c Ground Truth)', () => { // ========================================================================= // 1. LIGHTNING FURY (Skill 35) // ========================================================================= describe('1. Lightning Fury: Impact Cascade & Pierce Recursion', () => { it('spawns radial lightningjavelin sub-missiles to all other nearby alive enemies upon impact', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry) const target1 = createCombatUnit('enemy-1', { x: 100, y: 0 }, registry) const target2 = createCombatUnit('enemy-2', { x: 120, y: 50 }, registry) const target3 = createCombatUnit('enemy-3', { x: 80, y: -40 }, registry) const deadTarget = createCombatUnit('enemy-dead', { x: 110, y: 20 }, registry, { hp: 0 }) deadTarget.statList.setHp256(0) const dmgPacket: SUnitDmgPacket = { skillId: 35, attackKind: 'missile', elemMin256: 1 * FIXED_ONE, elemMax256: 40 * FIXED_ONE, } // Spawn primary Lightning Fury javelin traveling towards target1 const lf = missileEngine.spawnMissile({ missileNameOrId: 'lightningfury', sourceSkillId: 35, slvl: 20, owner: amazon, startX: 0, startY: 0, targetX: 100, targetY: 0, dmgPacket, })! expect(lf).toBeDefined() expect(lf.name).toBe('lightningfury') const targets = [target1, target2, target3, deadTarget] const targetPositions = new Map([ ['enemy-1', { x: 100, y: 0 }], ['enemy-2', { x: 120, y: 50 }], ['enemy-3', { x: 80, y: -40 }], ['enemy-dead', { x: 110, y: 20 }], ]) // Step missile until it collides with target1 let impactTick = -1 for (let t = 0; t < 20; t++) { const res = missileEngine.tick(t, targets, targetPositions) if (res.hits.length > 0) { impactTick = t break } } expect(impactTick).toBeGreaterThanOrEqual(0) // Examine active missiles: should contain the primary LF javelin and spawned sub-bolts const activeMissiles = (missileEngine as any).missiles as any[] const subBolts = activeMissiles.filter( (m: any) => (m.name === 'furylightning' || m.name === 'lightningjavelin') && !m.expired ) // Exactly 2 sub-bolts spawned (towards target2 and target3; deadTarget and self target1 excluded) expect(subBolts.length).toBe(2) for (const bolt of subBolts) { expect(bolt.sourceSkillId).toBe(35) expect(bolt.slvl).toBe(20) expect(bolt.owner.id).toBe('amazon') } }) it('cascades independent bolt bursts on EACH pierced target up to the 4-pierce cap (5 bursts total)', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry) // Set up 5 enemies in a linear row along the flight path const lineEnemies = [ createCombatUnit('line-1', { x: 60, y: 0 }, registry), createCombatUnit('line-2', { x: 120, y: 0 }, registry), createCombatUnit('line-3', { x: 180, y: 0 }, registry), createCombatUnit('line-4', { x: 240, y: 0 }, registry), createCombatUnit('line-5', { x: 300, y: 0 }, registry), ] // 6th enemy beyond max pierce cap const line6 = createCombatUnit('line-6', { x: 360, y: 0 }, registry) // Side flank enemy that receives radial bolts at every pierce location const flankEnemy = createCombatUnit('flank', { x: 180, y: 80 }, registry) const allTargets = [...lineEnemies, line6, flankEnemy] const targetPositions = new Map() for (const tgt of allTargets) { targetPositions.set(tgt.id, { x: tgt.x ?? 0, y: tgt.y ?? 0 }) } const dmgPacket: SUnitDmgPacket = { skillId: 35, attackKind: 'missile', elemMin256: 1 * FIXED_ONE, elemMax256: 40 * FIXED_ONE, } // Primary javelin with 100% pierce chance const lf = missileEngine.spawnMissile({ missileNameOrId: 'lightningfury', sourceSkillId: 35, slvl: 20, owner: amazon, startX: 0, startY: 0, targetX: 500, targetY: 0, dmgPacket, pierceChancePct: 100, })! expect(lf.canPierce).toBe(true) expect(lf.pierceChancePct).toBe(100) let totalPrimaryHits = 0 let totalSubMissilesSpawned = 0 // Step the simulation tick by tick for (let t = 0; t < 60; t++) { const res = missileEngine.tick(t, allTargets, targetPositions) for (const h of res.hits) { if (h.missileName === 'lightningfury') { totalPrimaryHits++ } } totalSubMissilesSpawned += res.subMissilesSpawned.filter( name => name === 'furylightning' || name === 'lightningjavelin' ).length } // 1.13c Invariant: Max pierce count is 4 (hits exactly 5 targets: line-1..line-5) expect(totalPrimaryHits).toBe(5) expect(lf.pierceCount).toBe(4) expect(lf.expired).toBe(true) // Killed after hitting 5th target // Each of the 5 hits triggered a cascade of radial sub-bolts expect(totalSubMissilesSpawned).toBeGreaterThanOrEqual(5) // line-6 was beyond the 4-pierce cap and must NOT have been hit by the primary javelin expect(lf.hitTargetIds.has('line-6')).toBe(false) }) it('dies on first hit when pierce is 0%, spawning only 1 cascade', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry) const e1 = createCombatUnit('e1', { x: 60, y: 0 }, registry) const e2 = createCombatUnit('e2', { x: 120, y: 0 }, registry) const flank = createCombatUnit('flank', { x: 60, y: 60 }, registry) const targets = [e1, e2, flank] const targetPositions = new Map([ ['e1', { x: 60, y: 0 }], ['e2', { x: 120, y: 0 }], ['flank', { x: 60, y: 60 }], ]) const dmgPacket: SUnitDmgPacket = { skillId: 35, attackKind: 'missile', elemMin256: 1 * FIXED_ONE, elemMax256: 40 * FIXED_ONE, } // Primary javelin with 0% pierce const lf = missileEngine.spawnMissile({ missileNameOrId: 'lightningfury', sourceSkillId: 35, slvl: 20, owner: amazon, startX: 0, startY: 0, targetX: 300, targetY: 0, dmgPacket, pierceChancePct: 0, })! let primaryHits = 0 for (let t = 0; t < 30; t++) { const res = missileEngine.tick(t, targets, targetPositions) primaryHits += res.hits.filter(h => h.missileName === 'lightningfury').length } expect(primaryHits).toBe(1) expect(lf.hitTargetIds.has('e1')).toBe(true) expect(lf.hitTargetIds.has('e2')).toBe(false) expect(lf.pierceCount).toBe(0) expect(lf.expired).toBe(true) }) it('differential oracle test for 5-band damage progression across slvl 1..50', () => { // Differential oracle for 5-band lightning damage: 1 - 40 base, scaling +20/30/40/50/50 function oracleLfMaxDamage(lvl: number): number { let max = 40 if (lvl <= 1) return max const b1 = Math.min(lvl, 8) - 1 max += b1 * 20 if (lvl <= 8) return max const b2 = Math.min(lvl, 16) - 8 max += b2 * 30 if (lvl <= 16) return max const b3 = Math.min(lvl, 22) - 16 max += b3 * 40 if (lvl <= 22) return max const b4 = Math.min(lvl, 28) - 22 max += b4 * 50 if (lvl <= 28) return max const b5 = lvl - 28 max += b5 * 50 return max } for (let lvl = 1; lvl <= 50; lvl++) { const stats = calculateLightningFuryStats(lvl) expect(stats.baseMinDamage).toBe(1) expect(stats.baseMaxDamage).toBe(oracleLfMaxDamage(lvl)) expect(stats.releaseBoltsCount).toBe(lvl) expect(stats.searchRadiusPx).toBe(200) } }) }) // ========================================================================= // 2. LIGHTNING STRIKE (Skill 34) // ========================================================================= describe('2. Lightning Strike: Chain Hopping & 4-Frame NextHitDelay', () => { it('verifies chain hops formula min(24, slvl + 1) with exact 24-hop cap', () => { expect(calculateLightningStrikeStats(1).chainHops).toBe(2) expect(calculateLightningStrikeStats(5).chainHops).toBe(6) expect(calculateLightningStrikeStats(10).chainHops).toBe(11) expect(calculateLightningStrikeStats(20).chainHops).toBe(21) expect(calculateLightningStrikeStats(23).chainHops).toBe(24) // 23 + 1 = 24 expect(calculateLightningStrikeStats(24).chainHops).toBe(24) // capped expect(calculateLightningStrikeStats(30).chainHops).toBe(24) // capped expect(calculateLightningStrikeStats(99).chainHops).toBe(24) // capped expect(calculateLightningStrikeStats(1).nextHitDelayFrames).toBe(4) }) it('enforces 4-frame NextHitDelay immunity window on struck targets', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry) const target = createCombatUnit('target', { x: 50, y: 0 }, registry) const targets = [target] const targetPositions = new Map([['target', { x: 50, y: 0 }]]) const dmgPacket: SUnitDmgPacket = { skillId: 34, attackKind: 'missile', elemMin256: 10 * FIXED_ONE, elemMax256: 20 * FIXED_ONE, } // Spawn a missile with nextDelay = 4 (like lightningstrike / chainlightning) const m1 = missileEngine.spawnMissile({ missileNameOrId: 'chainlightning', sourceSkillId: 34, slvl: 20, owner: amazon, startX: 40, startY: 0, targetX: 50, targetY: 0, dmgPacket, })! expect(m1.nextDelayFrames).toBe(4) // Tick 0: m1 hits target at tick 0 -> sets NextHitDelay expiration to tick 0 + 4 = 4 const res0 = missileEngine.tick(0, targets, targetPositions) expect(res0.hits.length).toBe(1) expect(missileEngine.isTargetInNextDelay('target', 0)).toBe(true) expect(missileEngine.isTargetInNextDelay('target', 1)).toBe(true) expect(missileEngine.isTargetInNextDelay('target', 2)).toBe(true) expect(missileEngine.isTargetInNextDelay('target', 3)).toBe(true) // At tick 4, NextHitDelay must expire: expect(missileEngine.isTargetInNextDelay('target', 4)).toBe(false) // Adversarial test: Spawn second missile trying to hit during immunity window (tick 2) const m2 = missileEngine.spawnMissile({ missileNameOrId: 'chainlightning', sourceSkillId: 34, slvl: 20, owner: amazon, startX: 40, startY: 0, targetX: 50, targetY: 0, dmgPacket, })! const res2 = missileEngine.tick(2, targets, targetPositions) // m2 MUST NOT hit target because target is in NextHitDelay! expect(res2.hits.length).toBe(0) expect(m2.hitTargetIds.has('target')).toBe(false) // At tick 4 (after expiration), target can be hit again const m3 = missileEngine.spawnMissile({ missileNameOrId: 'chainlightning', sourceSkillId: 34, slvl: 20, owner: amazon, startX: 40, startY: 0, targetX: 50, targetY: 0, dmgPacket, })! const res4 = missileEngine.tick(4, targets, targetPositions) expect(res4.hits.length).toBe(1) expect(m3.hitTargetIds.has('target')).toBe(true) }) it('chain leaps between nearby targets within 400px search radius, excluding last-hit target', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry) const targetA = createCombatUnit('target-A', { x: 50, y: 0 }, registry) const targetB = createCombatUnit('target-B', { x: 150, y: 0 }, registry) // 100px away from A (within 400px) const targetFar = createCombatUnit('target-Far', { x: 900, y: 0 }, registry) // 750px away from B (out of 400px) const targets = [targetA, targetB, targetFar] const targetPositions = new Map([ ['target-A', { x: 50, y: 0 }], ['target-B', { x: 150, y: 0 }], ['target-Far', { x: 900, y: 0 }], ]) const dmgPacket: SUnitDmgPacket = { skillId: 34, attackKind: 'missile', elemMin256: 10 * FIXED_ONE, elemMax256: 20 * FIXED_ONE, } // Initial strike against targetA with 5 chain hops missileEngine.spawnMissile({ missileNameOrId: 'chainlightning', sourceSkillId: 34, slvl: 20, owner: amazon, startX: 40, startY: 0, targetX: 50, targetY: 0, dmgPacket, chainCount: 5, }) // Tick 0: Hits targetA, spawns chain leap to targetB const res0 = missileEngine.tick(0, targets, targetPositions) expect(res0.hits.length).toBe(1) expect(res0.hits[0].targetId).toBe('target-A') expect(res0.subMissilesSpawned.length).toBe(1) // Chain leap to target-B // Examine active missile spawned for chain leap const activeMissiles = (missileEngine as any).missiles as any[] const chainMissile = activeMissiles.find((m: any) => !m.expired && m.lastHitTargetId === 'target-A') expect(chainMissile).toBeDefined() expect(chainMissile.chainRemaining).toBe(4) // Decremented from 5 to 4 }) }) // ========================================================================= // 3. DECOY (Skill 28) // ========================================================================= describe('3. Decoy: Threat Drawing, HP Scaling & 85% Resist Cap', () => { it('scales Decoy life from Amazon HP using exact formula HP * (0.5 + 0.1 * slvl)', async () => { const registry = await getSharedDataRegistry() const summonManager = new SummonManager(registry) // Test across multiple Amazon HP values and slvls const testCases = [ { playerHp: 500, slvl: 1, expectedPct: 60, expectedHp: 300 }, { playerHp: 500, slvl: 5, expectedPct: 100, expectedHp: 500 }, { playerHp: 1000, slvl: 10, expectedPct: 150, expectedHp: 1500 }, { playerHp: 1200, slvl: 20, expectedPct: 250, expectedHp: 3000 }, { playerHp: 2450, slvl: 30, expectedPct: 350, expectedHp: 8575 }, ] for (const tc of testCases) { const stats = calculateDecoyStats(tc.slvl, tc.playerHp) expect(stats.hpPctOfAmazon).toBe(tc.expectedPct) expect(stats.hp).toBe(tc.expectedHp) // Runtime creation in SummonManager const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry, { hp: tc.playerHp }) const res = summonManager.createPet({ owner: amazon, skillId: 28, slvl: tc.slvl, }) expect(res.created).toBe(true) expect(res.pet!.hp).toBe(tc.expectedHp) expect(res.pet!.maxHp).toBe(tc.expectedHp) expect(res.pet!.kind).toBe('dopplezon') expect(res.pet!.isStationary).toBe(true) } }) it('strictly clamps Decoy allResistances to 85% maximum (min(85, 4 * slvl))', async () => { const registry = await getSharedDataRegistry() const summonManager = new SummonManager(registry) // Sweep slvl 1..50 for (let lvl = 1; lvl <= 50; lvl++) { const stats = calculateDecoyStats(lvl) const expectedRes = Math.min(85, 4 * lvl) expect(stats.allResistancesPct).toBe(expectedRes) if (lvl >= 22) { expect(stats.allResistancesPct).toBe(85) // Capped at 85% } } // Check on runtime SummonedPetUnit const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry, { hp: 1000 }) const resLvl25 = summonManager.createPet({ owner: amazon, skillId: 28, slvl: 25, // 4 * 25 = 100% -> must clamp to 85% }) const pet = resLvl25.pet! expect(pet.statList.getAccruedStat('fireresist')).toBe(85) expect(pet.statList.getAccruedStat('lightresist')).toBe(85) expect(pet.statList.getAccruedStat('coldresist')).toBe(85) expect(pet.statList.getAccruedStat('poisonresist')).toBe(85) }) it('enforces stationary posture and threat-drawing contract (cannot move or attack)', async () => { const registry = await getSharedDataRegistry() const summonManager = new SummonManager(registry) const amazon = createCombatUnit('amazon', { x: 100, y: 100 }, registry) const outcome = summonManager.createPet({ owner: amazon, skillId: 28, slvl: 10, x: 120, y: 100, }) const decoy = outcome.pet! expect(decoy.isStationary).toBe(true) expect(decoy.kind).toBe('dopplezon') // Ticking Decoy in combat with surrounding enemies yields 0 attacks const enemy = createCombatUnit('enemy', { x: 125, y: 100 }, registry) const tickOutcome = summonManager.tickPets({ currentTick: 1, owner: amazon, enemies: [enemy], enemyPositions: new Map([['enemy', { x: 125, y: 100 }]]), missileEngine: new MissileEngine(registry), auraScanner: new AuraScanner(registry), }) expect(tickOutcome.petAttacks).toBe(0) }) }) // ========================================================================= // 4. VALKYRIE (Skill 32) // ========================================================================= describe('4. Valkyrie: Life Scaling, Decoy Synergy, Tiering & 6.0s Cooldown', () => { it('scales base life accurately per 440 * (1 + 0.2 * (slvl - 1))', () => { expect(calculateValkyrieStats(1).baseHp).toBe(440) expect(calculateValkyrieStats(5).baseHp).toBe(792) // 440 * 1.8 expect(calculateValkyrieStats(10).baseHp).toBe(1232) // 440 * 2.8 expect(calculateValkyrieStats(20).baseHp).toBe(2112) // 440 * 4.8 expect(calculateValkyrieStats(30).baseHp).toBe(2992) // 440 * 6.8 }) it('grants +20% life per hard point in Decoy synergy and isolates soft points', async () => { const registry = await getSharedDataRegistry() const summonManager = new SummonManager(registry) // Test 0, 1, 5, 10, 20 hard points const testSynergies = [ { hardPts: 0, mult: 1.0 }, { hardPts: 1, mult: 1.2 }, { hardPts: 5, mult: 2.0 }, { hardPts: 10, mult: 3.0 }, { hardPts: 20, mult: 5.0 }, ] for (const { hardPts, mult } of testSynergies) { const stats = calculateValkyrieStats(10, { decoyHardPoints: hardPts }) expect(stats.decoySynergyBonusPct).toBe(hardPts * 20) expect(stats.finalHp).toBe(Math.floor(1232 * mult)) const amazon = createCombatUnit('amazon', { x: 0, y: 0 }, registry) amazon.statList.setBaseSkillLevel(28, hardPts) const petRes = summonManager.createPet({ owner: amazon, skillId: 32, slvl: 10, }) expect(petRes.pet!.hp).toBe(Math.floor(1232 * mult)) } // Soft Points Isolation: +20 allskills with 0 hard points MUST yield 0% synergy const amazonSoftOnly = createCombatUnit('amazon-soft', { x: 0, y: 0 }, registry) amazonSoftOnly.statList.setBaseSkillLevel(28, 0) amazonSoftOnly.statList.addStat('item_allskills', 20) const softRes = summonManager.createPet({ owner: amazonSoftOnly, skillId: 32, slvl: 10, }) // HP must strictly equal base 1232 HP without synergy expect(softRes.pet!.hp).toBe(1232) }) it('verifies 5 equipment generation tiers across level boundaries', () => { function getExpectedTier(slvl: number): ValkyrieEquipmentTier { if (slvl >= 27) return 'tier5_rare_tiara' if (slvl >= 17) return 'tier4_war_pike' if (slvl >= 11) return 'tier3_gloves' if (slvl >= 7) return 'tier2_boots' return 'tier1_basic' } // Exhaustive boundary testing for (let lvl = 1; lvl <= 40; lvl++) { expect(calculateValkyrieStats(lvl).equipmentTier).toBe(getExpectedTier(lvl)) } // Explicit boundary assertions expect(calculateValkyrieStats(6).equipmentTier).toBe('tier1_basic') expect(calculateValkyrieStats(7).equipmentTier).toBe('tier2_boots') expect(calculateValkyrieStats(10).equipmentTier).toBe('tier2_boots') expect(calculateValkyrieStats(11).equipmentTier).toBe('tier3_gloves') expect(calculateValkyrieStats(16).equipmentTier).toBe('tier3_gloves') expect(calculateValkyrieStats(17).equipmentTier).toBe('tier4_war_pike') expect(calculateValkyrieStats(26).equipmentTier).toBe('tier4_war_pike') expect(calculateValkyrieStats(27).equipmentTier).toBe('tier5_rare_tiara') }) it('strictly enforces 150-frame (6.0s) casting delay cooldown', () => { expect(getSkillCooldownTicks(32)).toBe(150) for (let lvl = 1; lvl <= 30; lvl++) { const stats = calculateValkyrieStats(lvl) expect(stats.cooldownFrames).toBe(150) expect(stats.cooldownSeconds).toBe(6.0) } }) }) // ========================================================================= // 5. SLOW MISSILES (Skill 17) // ========================================================================= describe('5. Slow Missiles: Exact 33% Velocity Multiplier & Duration', () => { it('multiplies projectile velocity by exactly 0.33 (67% reduction)', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const caster = createCombatUnit('caster', { x: 0, y: 0 }, registry) const dmgPacket: SUnitDmgPacket = { skillId: 0, attackKind: 'missile', flatPhysMin256: 10 * FIXED_ONE, flatPhysMax256: 10 * FIXED_ONE, } // Baseline normal missile const normal = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner: caster, startX: 0, startY: 0, targetX: 400, targetY: 200, dmgPacket, })! // Slowed missile const slowed = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner: caster, startX: 0, startY: 0, targetX: 400, targetY: 200, dmgPacket, isSlowed: true, slowMultiplier: 0.33, })! // Step for 10 ticks for (let t = 0; t < 10; t++) { missileEngine.tick(t, [], new Map()) } // Verify exact 0.33 displacement ratio in 2:1 isometric space expect(slowed.x).toBeCloseTo(normal.x * 0.33, 1) expect(slowed.y).toBeCloseTo(normal.y * 0.33, 1) }) it('scales duration linearly per 300 + 60 * (slvl - 1) frames (12.0s + 2.4s/lvl)', () => { for (let lvl = 1; lvl <= 30; lvl++) { const stats = calculateSlowMissilesStats(lvl) const expectedFrames = 300 + 60 * (lvl - 1) expect(stats.durationFrames).toBe(expectedFrames) expect(stats.durationSeconds).toBe(expectedFrames / 25) expect(stats.velocityMultiplier).toBe(0.33) expect(stats.velocityReductionPct).toBe(67) expect(stats.radiusPx).toBe(200) expect(stats.manaCost).toBe(5) } }) }) // ========================================================================= // 6. INNER SIGHT (Skill 8) // ========================================================================= describe('6. Inner Sight: Exact Flat Defense Strip Scaling', () => { it('scales flat defense reduction per -40 - 25 * (slvl - 1)', () => { expect(calculateInnerSightStats(1).flatDefenseReduction).toBe(-40) expect(calculateInnerSightStats(5).flatDefenseReduction).toBe(-140) expect(calculateInnerSightStats(10).flatDefenseReduction).toBe(-265) expect(calculateInnerSightStats(20).flatDefenseReduction).toBe(-515) expect(calculateInnerSightStats(30).flatDefenseReduction).toBe(-765) }) it('scales duration per 700 + 150 * (slvl - 1) frames (28.0s + 6.0s/lvl)', () => { for (let lvl = 1; lvl <= 30; lvl++) { const stats = calculateInnerSightStats(lvl) const expectedFrames = 700 + 150 * (lvl - 1) expect(stats.durationFrames).toBe(expectedFrames) expect(stats.durationSeconds).toBe(expectedFrames / 25) expect(stats.radiusPx).toBe(200) expect(stats.manaCost).toBe(5) } }) it('correctly integrates with combat to-hit chance and clamps defense at 0', () => { const stats20 = calculateInnerSightStats(20) // -515 flat defense const baseMonsterDef = 400 // When flat reduction (-515) exceeds base defense (400), defense is clamped to 0 const debuffedDef = Math.max(0, baseMonsterDef + stats20.flatDefenseReduction) expect(debuffedDef).toBe(0) // Compute to-hit chance with 0 defense: should hit the 95% ceiling (alvl=80, dlvl=80, ar=1000) const hitChance = computeToHitChance({ attackerAr: 1000, defenderDef: debuffedDef, attackerLvl: 80, defenderLvl: 80, }) expect(hitChance).toBe(95) // Max 1.13c hit chance ceiling // Monster with high defense (1000 defense) const highDef = 1000 const hitChanceBefore = computeToHitChance({ attackerAr: 1000, defenderDef: highDef, attackerLvl: 80, defenderLvl: 80, }) const hitChanceAfter = computeToHitChance({ attackerAr: 1000, defenderDef: highDef + stats20.flatDefenseReduction, // 1000 - 515 = 485 attackerLvl: 80, defenderLvl: 80, }) expect(hitChanceBefore).toBe(50) // (2*1000*100*80)/((1000+1000)*160) = 50% expect(hitChanceAfter).toBe(67) // (2*1000*100*80)/((1000+485)*160) = 67% expect(hitChanceAfter).toBeGreaterThan(hitChanceBefore) }) }) // ========================================================================= // 7. ADVERSARIAL FUZZING & DIFFERENTIAL ORACLES (Playbook Compliant) // ========================================================================= describe('7. Adversarial Fuzzing & Differential Oracle Harnesses', () => { it('Phase 2 Fuzzing: Lightning Fury & Lightning Strike synergies over 100 random allocations', () => { // Linear Congruential Generator for reproducible deterministic fuzzing let seed = 123456789 function nextRandom(): number { seed = (1103515245 * seed + 12345) & 0x7fffffff return seed / 0x7fffffff } for (let i = 0; i < 100; i++) { const slvl = Math.floor(nextRandom() * 40) + 1 const ps = Math.floor(nextRandom() * 21) // 0..20 hard points const lb = Math.floor(nextRandom() * 21) const cs = Math.floor(nextRandom() * 21) const lfOrLs = Math.floor(nextRandom() * 21) // 1. Lightning Fury: 1% per point from PS, LB, CS, LS const lfStats = calculateLightningFuryStats(slvl, { powerStrike: ps, lightningBolt: lb, chargedStrike: cs, lightningStrike: lfOrLs, }) const expectedLfBonus = ps + lb + cs + lfOrLs expect(lfStats.synergyBonusPct).toBe(expectedLfBonus) expect(lfStats.synergyMultiplier).toBeCloseTo(1.0 + expectedLfBonus / 100, 5) expect(lfStats.maxDamage).toBe(Math.floor(lfStats.baseMaxDamage * lfStats.synergyMultiplier)) // 2. Lightning Strike: 8% per point from PS, LB, CS, LF const lsStats = calculateLightningStrikeStats(slvl, { powerStrike: ps, lightningBolt: lb, chargedStrike: cs, lightningFury: lfOrLs, }) const expectedLsBonus = 8 * (ps + lb + cs + lfOrLs) expect(lsStats.synergyBonusPct).toBe(expectedLsBonus) expect(lsStats.synergyMultiplier).toBeCloseTo(1.0 + expectedLsBonus / 100, 5) expect(lsStats.maxDamage).toBe(Math.floor(lsStats.baseMaxDamage * lsStats.synergyMultiplier)) } }) it('Phase 2 Fuzzing: Decoy HP & Resistance bounds over 200 random HP and slvl combinations', () => { let seed = 987654321 function nextRandom(): number { seed = (1103515245 * seed + 12345) & 0x7fffffff return seed / 0x7fffffff } for (let i = 0; i < 200; i++) { const playerHp = Math.floor(nextRandom() * 8000) + 10 // 10..8010 HP const slvl = Math.floor(nextRandom() * 60) + 1 // 1..60 slvl const stats = calculateDecoyStats(slvl, playerHp) // Oracle verification const expectedPct = Math.round((0.5 + 0.1 * slvl) * 100) const expectedHp = Math.floor(playerHp * (0.5 + 0.1 * slvl)) const expectedRes = Math.min(85, 4 * slvl) expect(stats.hpPctOfAmazon).toBe(expectedPct) expect(stats.hp).toBe(expectedHp) expect(stats.allResistancesPct).toBe(expectedRes) expect(stats.allResistancesPct).toBeLessThanOrEqual(85) } }) it('Phase 2 Fuzzing: Valkyrie Life & Synergy differential testing over 200 combinations', () => { let seed = 555666777 function nextRandom(): number { seed = (1103515245 * seed + 12345) & 0x7fffffff return seed / 0x7fffffff } for (let i = 0; i < 200; i++) { const slvl = Math.floor(nextRandom() * 50) + 1 const decoyHardPoints = Math.floor(nextRandom() * 30) // 0..29 const stats = calculateValkyrieStats(slvl, { decoyHardPoints }) // Differential oracle const oracleBaseHp = Math.floor(440 * (1 + 0.2 * (slvl - 1))) const oracleDecoyBonus = decoyHardPoints * 20 const oracleFinalHp = Math.floor(oracleBaseHp * (1 + oracleDecoyBonus / 100)) const oracleRes = Math.min(75, 2 * slvl) expect(stats.baseHp).toBe(oracleBaseHp) expect(stats.decoySynergyBonusPct).toBe(oracleDecoyBonus) expect(stats.finalHp).toBe(oracleFinalHp) expect(stats.allResistancesPct).toBe(oracleRes) expect(stats.cooldownFrames).toBe(150) } }) it('Phase 2 Fuzzing: Slow Missiles 33% speed damper across 36 angles (0 to 350 degrees)', async () => { const registry = await getSharedDataRegistry() const missileEngine = new MissileEngine(registry) const caster = createCombatUnit('caster', { x: 0, y: 0 }, registry) const dmgPacket: SUnitDmgPacket = { skillId: 0, attackKind: 'missile', flatPhysMin256: 10 * FIXED_ONE, flatPhysMax256: 10 * FIXED_ONE, } for (let angleDeg = 0; angleDeg < 360; angleDeg += 10) { const angleRad = (angleDeg * Math.PI) / 180 const targetX = Math.round(500 * Math.cos(angleRad)) const targetY = Math.round(500 * Math.sin(angleRad)) const normal = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner: caster, startX: 0, startY: 0, targetX, targetY, dmgPacket, })! const slowed = missileEngine.spawnMissile({ missileNameOrId: 'arrow', sourceSkillId: 0, slvl: 1, owner: caster, startX: 0, startY: 0, targetX, targetY, dmgPacket, isSlowed: true, slowMultiplier: 0.33, })! // Step for 1 tick missileEngine.tick(0, [], new Map()) const normalDist = Math.hypot(normal.x, normal.y) const slowedDist = Math.hypot(slowed.x, slowed.y) // Distance ratio must be 0.33 within floating-point tolerance expect(slowedDist / normalDist).toBeCloseTo(0.33, 2) } }) it('Phase 4 Degenerate Inputs: boundary sanitization and defense clamping', () => { // Degenerate inputs (slvl <= 0, NaN, Infinity) safely clamp to 1 expect(calculateDecoyStats(0, 1000).hp).toBe(600) // Sanitized to slvl 1 (60%) expect(calculateDecoyStats(-5, 1000).hp).toBe(600) expect(calculateDecoyStats(NaN, 1000).hp).toBe(600) expect(calculateValkyrieStats(0).baseHp).toBe(440) expect(calculateValkyrieStats(-10).baseHp).toBe(440) expect(calculateValkyrieStats(NaN).baseHp).toBe(440) expect(calculateSlowMissilesStats(0).durationFrames).toBe(300) expect(calculateInnerSightStats(0).flatDefenseReduction).toBe(-40) // Inner Sight: monster defense 0 -> stripped remains 0 const strip20 = calculateInnerSightStats(20).flatDefenseReduction // -515 const clampedDef = Math.max(0, 0 + strip20) expect(clampedDef).toBe(0) const hitChance = computeToHitChance({ attackerAr: 500, defenderDef: clampedDef, attackerLvl: 50, defenderLvl: 50, }) expect(hitChance).toBe(95) // Capped at 95% }) }) })