/** * Adversarial Stress & Correctness Verification Suite — Milestone M3 * Skills: Charged Bolt (#038) & Telekinesis (#043), Engine Fix (#192, #197) * Author: challenger_lightning_m3 (teamwork_preview_challenger) * Ground Truth: Diablo II v1.13c (D2Game.dll, D2Common.dll, Skills.txt, Missiles.txt) * * Test Dimensions: * 1. Engine Fix: Verify spawned projectiles do NOT duplicate in this.projectiles across 1,000 game frames. * 2. Charged Bolt Shotgun Effect: 24 bolts at point-blank range register 24x damage without NextDelay lockout. * 3. Jitter Table Traversal & Trajectory Spread: Verify all 32 entries of GAT_CHARGED_BOLT_OFFSETS and seed variance. * 4. Fuzz 1,000+ Combinations: slvl (1-99) x Lightning synergy (0-20) against 5-band table and +6% multiplier. * 5. Obstacle Collision: Verify bolts terminate at solid tiles/walls. * 6. Telekinesis Range Boundary: 599px (valid) vs 601px (rejected). * 7. Item Pickup Whitelist: Fuzz allowed (potions, scrolls, gold, ammo, keys) vs rejected (weapons, armor, jewelry, charms). * 8. Object Interaction Whitelist: chests, waypoints, stash, portals, shrines, doors, wells, urns. * 9. Monster Combat: 1-2 damage base (+1/level), 35% knockback, 35 stun ticks. * 10. Energy Shield Ratio Synergy: tkBlvl from 0 to 40 (ratio 2.0 down to 0.75 floor, soft point immutability). */ import { describe, expect, it } from 'vitest' import { tickProjectiles, calculateChargedBoltCount, calculateChargedBoltSynergyMultiplier, calculateChargedBoltDamage, calculateTelekinesisDamage, calculateEnergyShieldManaRatio, isTelekinesisItemAllowed, isTelekinesisObjectAllowed, GAT_CHARGED_BOLT_OFFSETS, calculateSkillDamage, getSkillManaCost, getSkillCooldownTicks, CANONICAL_113C_MISSILES, type Projectile, } from '../src/server/skills/skills.ts' import { MissileEngine, } from '../src/server/engine/missile-engine.ts' import type { CombatUnitContext } from '../src/server/engine/combat-pipeline.ts' import { getSharedDataRegistry } from '../src/baker/mpq-archive-source.ts' import { UnitStatList } from '../src/server/engine/stat-list.ts' import { StateBus } from '../src/server/engine/state-bus.ts' import { GameEngine, type WorldMapProvider, type GameEngineOptions } from '../src/server/engine/engine.ts' import { castSkill } from '../src/server/scene/skill-caster.ts' import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../src/common/data/demo-data.ts' const DUMMY_INPUT = { movement: { x: 0, y: 0 }, attacking: false, pickingUp: false, talking: false, digits: [], saving: false, loading: false, } const OPEN_TERRAIN = { overlap: () => 0, } function createHarnessEngine(monsters?: { x: number; y: number; hp?: number; isUndead?: boolean; name?: string }[]) { const dummyTerrain: WorldMapProvider = { widthPx: 2000, heightPx: 2000, overlap: (x, y) => { // Wall barrier between x = 400 and x = 420 if (x >= 400 && x <= 420 && y >= 0 && y <= 1000) return 1 return 0 }, isMissileBlocked: (x, y) => { if (x >= 400 && x <= 420 && y >= 0 && y <= 1000) return true return false }, } const dummyOpts: GameEngineOptions = { spawn: { x: 100, y: 100 }, stats: [], xpTable: DEMO_EXPERIENCE, skills: DEMO_SKILLS, npcDefs: [], questDefs: DEMO_QUESTS, combatOptions: { playerSpeed: 4, playerReach: 50, playerCooldownTicks: 10, playerDamage: 5, playerManaPerAttack: 1, respawnTicks: 100, }, talkRadius: 50, pickupRadius: 30, inventoryCols: 10, inventoryRows: 4, monsterCount: 0, } const engine = new GameEngine(dummyTerrain, dummyOpts) engine.world.player.x = 100 engine.world.player.y = 100 engine.world.player.mana = 1000 engine.world.player.maxMana = 1000 engine.world.player.cooldown = 0 if (monsters) { engine.world.monsters.length = 0 for (let i = 0; i < monsters.length; i++) { const m = monsters[i]! const isUndead = m.isUndead ?? false engine.world.monsters.push({ index: i, stats: { id: isUndead ? 'skeleton' : 'fallen', name: m.name ?? (isUndead ? 'Skeleton' : 'Fallen'), hp: m.hp ?? 100, damage: 5, cooldownTicks: 25, reach: 25, aggroRadius: 200, speed: 100, xp: 10, }, x: m.x, y: m.y, hp: m.hp ?? 100, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, ...(isUndead ? { isUndead: true } : {}), }) } } return engine } describe('Milestone M3 Adversarial Stress & Verification Suite', () => { // ========================================================================= // 1. Engine Fix Verification: No Projectile Duplication Across 1,000 Frames // ========================================================================= describe('1. Engine Fix: Projectile Deduplication Across 1,000 Frames', () => { it('verifies spawned projectiles do not duplicate in this.projectiles across 1,000 game frames', () => { const engine = createHarnessEngine([ { x: 300, y: 100, hp: 50000 }, { x: 350, y: 120, hp: 50000 }, ]) // Seed initial projectiles that continuously emit sub-projectiles (e.g. blizzardcenter emitting shards) engine.projectiles.push({ skillId: 'blizzard', x: 300, y: 100, vx: 0, vy: 0, damage: 15, ttl: 1000, fromPlayer: true, missileType: 'blizzardcenter', }) // Also seed an active chain lightning projectile bouncing between monsters engine.projectiles.push({ skillId: 'chainlightning', x: 100, y: 100, vx: 15, vy: 0, damage: 25, ttl: 100, fromPlayer: true, missileType: 'chainlightning', chainRemaining: 10, nextHit: 1, nextDelay: 4, }) let duplicateDetections = 0 let totalProjectilesObserved = 0 for (let frame = 1; frame <= 1000; frame++) { // Every 50 frames, periodically cast Charged Bolt to add more active missiles if (frame % 50 === 0) { const status = { textContent: '' } as HTMLElement castSkill(38, 300, 100, { engine, runtime: {} as any, hudManager: null, status }) } engine.tick(DUMMY_INPUT) // Check for any duplicate object references in this.projectiles const projSet = new Set(engine.projectiles) if (projSet.size !== engine.projectiles.length) { duplicateDetections++ } totalProjectilesObserved += engine.projectiles.length } // Assert zero duplicate object instances occurred across all 1,000 frames expect(duplicateDetections).toBe(0) expect(totalProjectilesObserved).toBeGreaterThan(0) }) it('demonstrates pre-fix duplication defect vs post-fix deduplication on spawned projectiles', () => { // blizzardcenter emits a shard when (ageTicks + 1) % 4 === 0. Set ageTicks = 3 to trigger emission on frame 1. const testProjectiles: Projectile[] = [ { skillId: 'blizzard', x: 300, y: 100, vx: 0, vy: 0, damage: 15, ttl: 20, ageTicks: 3, fromPlayer: true, missileType: 'blizzardcenter', }, ] const targets = [{ index: 0, x: 300, y: 100, radius: 20, alive: true }] const outcome = tickProjectiles(testProjectiles, targets, OPEN_TERRAIN) expect(outcome.spawnedProjectiles?.length).toBe(1) // With pre-fix logic: outcome.alive + outcome.spawnedProjectiles const buggyList: Projectile[] = [] buggyList.push(...outcome.alive) if (outcome.spawnedProjectiles && outcome.spawnedProjectiles.length > 0) { buggyList.push(...outcome.spawnedProjectiles) } // Buggy list has duplicate instances of spawned projectiles (2 instances of the shard) const buggySet = new Set(buggyList) expect(buggySet.size).toBeLessThan(buggyList.length) expect(buggyList.length - buggySet.size).toBe(outcome.spawnedProjectiles!.length) // With post-fix logic: outcome.alive only (1 instance of emitter + 1 instance of shard) const fixedList: Projectile[] = [] fixedList.push(...outcome.alive) const fixedSet = new Set(fixedList) expect(fixedSet.size).toBe(fixedList.length) }) }) // ========================================================================= // 2. Charged Bolt Shotgun Effect: NextDelay = 0 Point-Blank 24x Damage // ========================================================================= describe('2. Charged Bolt Shotgun Effect (NextDelay = 0)', () => { it('registers 24x damage across 24 simultaneous bolts at point-blank range without lockout in tickProjectiles', () => { // Monster placed right in front of the caster const monsterTarget = { index: 0, x: 105, y: 100, radius: 20, alive: true } const targets = [monsterTarget] // Spawn 24 point-blank charged bolts aiming at the monster const bolts: Projectile[] = [] for (let i = 0; i < 24; i++) { bolts.push({ skillId: '38', x: 100, y: 100, vx: 12, vy: 0, damage: 10, ttl: 98, fromPlayer: true, missileType: 'chargedbolt', nextDelay: 0, // Authentic 1.13c Missiles.txt NextDelay = 0 pierce: false, seed: i, jitterIndex: i % 32, }) } const outcome = tickProjectiles(bolts, targets, { ...OPEN_TERRAIN, currentTick: 10 }) // All 24 bolts must register hits against the target on the exact same frame expect(outcome.hits.length).toBe(24) for (const hit of outcome.hits) { expect(hit.targetIndex).toBe(0) expect(hit.damage).toBe(10) } // Total shotgun damage is exactly 24x single bolt damage const totalDmg = outcome.hits.reduce((acc, h) => acc + h.damage, 0) expect(totalDmg).toBe(240) // Since pierce is false, all 24 bolts are consumed upon hitting the monster expect(outcome.alive.length).toBe(0) }) it('registers 24 simultaneous hits in MissileEngine without NextDelay lockout', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const ownerStats = new UnitStatList() const owner = { id: 'player', x: 100, y: 100, statList: ownerStats, stateBus: new StateBus(ownerStats), } as any const targetStats = new UnitStatList() targetStats.setHp256(10000 * 256) const targetUnit: CombatUnitContext = { id: 'boss_target', name: 'Boss Target', unitType: 'monster', statList: targetStats, stateBus: new StateBus(targetStats), } const targetPos = new Map([['boss_target', { x: 105, y: 100 }]]) // Spawn 24 chargedbolt missiles at point blank for (let i = 0; i < 24; i++) { engine.spawnMissile({ missileNameOrId: 'chargedbolt', sourceSkillId: 38, slvl: 20, owner, startX: 100, startY: 100, targetX: 105, targetY: 100, dmgPacket: { minDamage: 10, maxDamage: 10, damageType: 'lightning' } as any, vx: 12, vy: 0, maxRangeTicks: 98, seed: i, jitterIndex: i % 32, }) } expect(engine.getActiveMissiles().length).toBe(24) const res = engine.tick(1, [targetUnit], targetPos) // All 24 missiles register hits expect(res.hits.length).toBe(24) for (const h of res.hits) { expect(h.missileName).toBe('chargedbolt') expect(h.targetId).toBe('boss_target') } }) it('contrasts with NextDelay > 0 skill (Nova / Chain Lightning) which restricts to 1 hit per window', () => { const monsterTarget = { index: 0, x: 105, y: 100, radius: 20, alive: true } const targets = [monsterTarget] // Spawn 24 Nova bolts with NextDelay = 4 const novaBolts: Projectile[] = [] for (let i = 0; i < 24; i++) { novaBolts.push({ skillId: '48', x: 100, y: 100, vx: 24, vy: 0, damage: 50, ttl: 13, fromPlayer: true, missileType: 'electricnova', nextDelay: 4, pierce: false, }) } const outcome = tickProjectiles(novaBolts, targets, { ...OPEN_TERRAIN, currentTick: 10 }) // Due to NextDelay = 4, only 1 hit registers despite 24 bolts overlapping! expect(outcome.hits.length).toBe(1) }) }) // ========================================================================= // 3. Jitter Table Traversal & Trajectory Spread // ========================================================================= describe('3. Jitter Table Traversal & Trajectory Spread', () => { it('verifies GAT_CHARGED_BOLT_OFFSETS has exactly 32 entries matching 1.13c D2Common.dll', () => { expect(GAT_CHARGED_BOLT_OFFSETS.length).toBe(32) expect(Object.isFrozen(GAT_CHARGED_BOLT_OFFSETS)).toBe(true) const expected = [ -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 0, 1, -1, 1, ] expect(Array.from(GAT_CHARGED_BOLT_OFFSETS)).toEqual(expected) // Frequency distribution check: const counts = { '-1': 0, '0': 0, '1': 0 } for (const val of GAT_CHARGED_BOLT_OFFSETS) { counts[String(val) as '-1' | '0' | '1']++ } expect(counts['-1']).toBe(11) expect(counts['0']).toBe(10) expect(counts['1']).toBe(11) }) it('verifies all 32 entries are sequentially traversed over 32 frames in tickProjectiles', () => { let projectile: Projectile = { skillId: '38', x: 100, y: 100, vx: 12, vy: 0, damage: 10, ttl: 98, fromPlayer: true, missileType: 'chargedbolt', jitter: true, jitterIndex: 0, } const indicesTraversed: number[] = [] for (let frame = 0; frame < 32; frame++) { indicesTraversed.push(projectile.jitterIndex ?? 0) const outcome = tickProjectiles([projectile], [], OPEN_TERRAIN) expect(outcome.alive.length).toBe(1) projectile = outcome.alive[0]! } // Verify that all indices 0..31 were sequentially traversed expect(indicesTraversed).toEqual(Array.from({ length: 32 }, (_, i) => i)) // After 32 ticks, index wraps back to 0 expect(projectile.jitterIndex).toBe(0) }) it('verifies trajectory variance and cone dispersion when casting Charged Bolt', () => { const engine = createHarnessEngine() const status = { textContent: '' } as HTMLElement castSkill(38, 500, 100, { engine, runtime: {} as any, hudManager: null, status }) // Default slvl 1 spawns min(24, 1 + 2) = 3 bolts expect(engine.projectiles.length).toBe(3) // Step 20 frames forward for (let i = 0; i < 20; i++) { engine.tick(DUMMY_INPUT) } expect(engine.projectiles.length).toBe(3) // Collect final positions across all 3 bolts const yCoords = engine.projectiles.map(b => b.y) const uniqueY = new Set(yCoords.map(y => Math.round(y))) // All 3 bolts fan out with distinct Y coordinates across the dispersion cone expect(uniqueY.size).toBe(3) const minY = Math.min(...yCoords) const maxY = Math.max(...yCoords) const spreadY = maxY - minY expect(spreadY).toBeGreaterThan(15) // cone spread > 15px }) }) // ========================================================================= // 4. Fuzz 1,000+ Combinations: slvl (1-99) x Synergy (0-20) // ========================================================================= describe('4. Fuzz 1,000+ Combinations: slvl (1-99) x Synergy (0-20)', () => { // Independent Ground Truth Reference Oracle for 1.13c Charged Bolt (Skills.txt row 40) function oracleChargedBoltDamage(slvl: number, synergyPoints: number): { min: number; max: number } { const lvl = Math.max(1, Math.floor(slvl)) let min = 2 let max = 4 for (let l = 2; l <= lvl; l++) { let add = 1 if (l >= 29) add = 4 else if (l >= 23) add = 3 else if (l >= 17) add = 2 else if (l >= 9) add = 1 else if (l >= 2) add = 1 min += add max += add } const mult = 1.0 + synergyPoints * 0.06 return { min: Math.floor(min * mult), max: Math.floor(max * mult), } } it('fuzzes 2,079 combinations of slvl (1-99) and Lightning synergy (0-20) with 100% oracle match', () => { let combinationsTested = 0 let mismatches = 0 for (let slvl = 1; slvl <= 99; slvl++) { for (let syn = 0; syn <= 20; syn++) { combinationsTested++ const expected = oracleChargedBoltDamage(slvl, syn) const actual1 = calculateChargedBoltDamage(slvl, { lightning: syn }) const actual2 = calculateChargedBoltDamage(slvl, { lightningPoints: syn }) const actual3 = calculateSkillDamage(38, slvl, { lightning: syn } as any) if ( actual1.min !== expected.min || actual1.max !== expected.max || actual2.min !== expected.min || actual2.max !== expected.max || actual3.min !== expected.min || actual3.max !== expected.max ) { mismatches++ } expect(actual1.min).toBeLessThanOrEqual(actual1.max) expect(actual1.min).toBeGreaterThanOrEqual(2) } } expect(combinationsTested).toBe(99 * 21) // 2,079 combinations expect(mismatches).toBe(0) }) it('verifies exact 5-band boundaries for base damage (synergy = 0)', () => { // Band 1: lvl 1-8 (+1/+1) expect(calculateChargedBoltDamage(1, 0)).toEqual({ min: 2, max: 4 }) expect(calculateChargedBoltDamage(8, 0)).toEqual({ min: 9, max: 11 }) // Band 2: lvl 9-16 (+1/+1) expect(calculateChargedBoltDamage(9, 0)).toEqual({ min: 10, max: 12 }) expect(calculateChargedBoltDamage(16, 0)).toEqual({ min: 17, max: 19 }) // Band 3: lvl 17-22 (+2/+2) expect(calculateChargedBoltDamage(17, 0)).toEqual({ min: 19, max: 21 }) expect(calculateChargedBoltDamage(22, 0)).toEqual({ min: 29, max: 31 }) // Band 4: lvl 23-28 (+3/+3) expect(calculateChargedBoltDamage(23, 0)).toEqual({ min: 32, max: 34 }) expect(calculateChargedBoltDamage(28, 0)).toEqual({ min: 47, max: 49 }) // Band 5: lvl 29+ (+4/+4) expect(calculateChargedBoltDamage(29, 0)).toEqual({ min: 51, max: 53 }) expect(calculateChargedBoltDamage(30, 0)).toEqual({ min: 55, max: 57 }) expect(calculateChargedBoltDamage(99, 0)).toEqual({ min: 331, max: 333 }) }) it('fuzzes bolt count formula across all 99 levels against min(24, slvl + 2)', () => { for (let slvl = 1; slvl <= 99; slvl++) { const expected = Math.min(24, slvl + 2) expect(calculateChargedBoltCount(slvl)).toBe(expected) } expect(calculateChargedBoltCount(1)).toBe(3) expect(calculateChargedBoltCount(21)).toBe(23) expect(calculateChargedBoltCount(22)).toBe(24) expect(calculateChargedBoltCount(23)).toBe(24) expect(calculateChargedBoltCount(99)).toBe(24) }) it('fuzzes mana cost across slvl 1 to 99: 3 + (slvl - 1) * 0.5', () => { for (let slvl = 1; slvl <= 99; slvl++) { const expected = 3 + (slvl - 1) * 0.5 expect(getSkillManaCost(38, slvl)).toBe(expected) } }) }) // ========================================================================= // 5. Obstacle Collision & Wall Termination // ========================================================================= describe('5. Obstacle Collision & Wall Termination', () => { it('verifies charged bolts terminate upon hitting solid walls in tickProjectiles', () => { // Wall barrier at x >= 200 const wallTerrain = { overlap: (x: number) => (x >= 200 ? 1 : 0), isMissileBlocked: (x: number) => x >= 200, } const bolt: Projectile = { skillId: '38', x: 180, y: 100, vx: 12, vy: 0, damage: 10, ttl: 98, fromPlayer: true, missileType: 'chargedbolt', jitter: false, } // Tick 1: moves from 180 to 192 (before wall) -> survives const step1 = tickProjectiles([bolt], [], wallTerrain) expect(step1.alive.length).toBe(1) expect(step1.alive[0]!.x).toBe(192) // Tick 2: moves from 192 to 204 (hits wall at 200) -> dies const step2 = tickProjectiles(step1.alive, [], wallTerrain) expect(step2.alive.length).toBe(0) expect(step2.wallHits).toBe(1) }) it('verifies MissileEngine terminates chargedbolt on terrain collision', async () => { const registry = await getSharedDataRegistry() const terrainCollision = { overlap: (x: number) => (x >= 200 ? 1 : 0), isMissileBlocked: (x: number) => x >= 200, } const engine = new MissileEngine(registry, terrainCollision) const ownerStats = new UnitStatList() const owner = { id: 'player', x: 185, y: 100, statList: ownerStats, stateBus: new StateBus(ownerStats), } as any engine.spawnMissile({ missileNameOrId: 'chargedbolt', sourceSkillId: 38, slvl: 10, owner, startX: 185, startY: 100, targetX: 300, targetY: 100, dmgPacket: { minDamage: 10, maxDamage: 10, damageType: 'lightning' } as any, vx: 12, vy: 0, maxRangeTicks: 98, }) // Tick 1: reaches 197 engine.tick(1, [], new Map()) expect(engine.getActiveMissiles().length).toBe(1) // Tick 2: hits wall at >= 200 engine.tick(2, [], new Map()) expect(engine.getActiveMissiles().length).toBe(0) }) }) // ========================================================================= // 6. Telekinesis Range Boundary: 599px (valid) vs 601px (rejected) // ========================================================================= describe('6. Telekinesis Range Boundary (599px vs 601px)', () => { it('accepts targets at 599px and rejects targets at 601px', () => { const engine = createHarnessEngine() // Add a monster at 599px along X axis: (100 + 599, 100) = (699, 100) engine.world.monsters.push({ index: 0, stats: { id: 'm1', name: 'Fallen', hp: 100, damage: 2, cooldownTicks: 10, reach: 20, aggroRadius: 100, speed: 50, xp: 5 }, x: 699, y: 100, hp: 100, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, }) // Add another monster at 601px along X axis: (100 + 601, 100) = (701, 100) engine.world.monsters.push({ index: 1, stats: { id: 'm2', name: 'Zombie', hp: 100, damage: 2, cooldownTicks: 10, reach: 20, aggroRadius: 100, speed: 50, xp: 5 }, x: 701, y: 100, hp: 100, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, }) const status = { textContent: '' } as HTMLElement // Target at 599px (valid boundary test) const res599 = castSkill(43, 699, 100, { engine, runtime: {} as any, hudManager: null, status }) expect(res599).toBe(true) expect(status.textContent).toContain('心灵传动命中') // Target at 601px (out-of-range boundary test) const res601 = castSkill(43, 701, 100, { engine, runtime: {} as any, hudManager: null, status }) expect(res601).toBe(false) expect(status.textContent).toBe('目标超出心灵传动范围!') // Exact boundary at 600px const res600 = castSkill(43, 700, 100, { engine, runtime: {} as any, hudManager: null, status }) expect(res600).toBe(true) // Diagonal distance: dx = 424, dy = 424 -> sqrt(424^2 + 424^2) = 599.62px <= 600px const resDiagValid = castSkill(43, 100 + 424, 100 + 424, { engine, runtime: {} as any, hudManager: null, status }) expect(resDiagValid).toBe(true) // Diagonal distance: dx = 426, dy = 426 -> sqrt(426^2 + 426^2) = 602.45px > 600px const resDiagInvalid = castSkill(43, 100 + 426, 100 + 426, { engine, runtime: {} as any, hudManager: null, status }) expect(resDiagInvalid).toBe(false) expect(status.textContent).toBe('目标超出心灵传动范围!') }) }) // ========================================================================= // 7. Item Pickup Whitelist: Allowed vs Forbidden Categories // ========================================================================= describe('7. Item Pickup Whitelist Fuzzing', () => { it('accepts all authentic allowed items (potions, scrolls, gold, ammo, keys)', () => { const allowedItems = [ // Potions by code { code: 'hp1', name: 'Minor Healing Potion' }, { code: 'hp2', name: 'Light Healing Potion' }, { code: 'hp3', name: 'Healing Potion' }, { code: 'hp4', name: 'Greater Healing Potion' }, { code: 'hp5', name: 'Super Healing Potion' }, { code: 'mp1', name: 'Minor Mana Potion' }, { code: 'mp2', name: 'Light Mana Potion' }, { code: 'mp3', name: 'Mana Potion' }, { code: 'mp4', name: 'Greater Mana Potion' }, { code: 'mp5', name: 'Super Mana Potion' }, { code: 'rvs', name: 'Rejuvenation Potion' }, { code: 'rvl', name: 'Full Rejuvenation Potion' }, { code: 'vps', name: 'Stamina Potion' }, { code: 'yps', name: 'Antidote Potion' }, { code: 'wms', name: 'Thawing Potion' }, { code: 'ant', name: 'Antidote' }, { code: 'thw', name: 'Thawing' }, { code: 'sta', name: 'Stamina' }, // Potions by localized name { nameZh: '轻微治疗药剂' }, { nameZh: '超强法力药剂' }, { nameZh: '完全活力药剂' }, { nameZh: '解毒药剂' }, { nameZh: '解冻药剂' }, // Scrolls & Tomes { code: 'tsc', name: 'Scroll of Town Portal' }, { code: 'isc', name: 'Scroll of Identify' }, { code: 'tbk', name: 'Tome of Town Portal' }, { code: 'ibk', name: 'Tome of Identify' }, { nameZh: '城镇传送卷轴' }, { nameZh: '辨识之书' }, // Gold { isGold: true, amount: 100 }, { code: 'gld', amount: 500 }, { code: 'gold', amount: 250 }, // Ammo { code: 'aqv', name: 'Arrows' }, { code: 'cqv', name: 'Crossbow Bolts' }, { nameZh: '箭矢' }, { nameZh: '十字弓弹' }, // Keys { code: 'key', name: 'Skeleton Key' }, { nameZh: '钥匙' }, // Explicit types { type: 'potion' }, { type: 'scroll' }, { type: 'gold' }, { type: 'ammo' }, { type: 'key' }, ] for (const item of allowedItems) { expect(isTelekinesisItemAllowed(item)).toBe(true) } }) it('strictly rejects forbidden equipment (weapons, armors, shields, jewelry, charms)', () => { const forbiddenItems = [ // Weapons { code: 'swor', name: 'Short Sword' }, { code: 'axe', name: 'Hand Axe' }, { code: 'bow', name: 'Short Bow' }, { code: 'wand', name: 'Wand' }, { code: 'staff', name: 'Short Staff' }, { code: 'bsw', name: 'Broad Sword' }, { code: 'gsc', name: 'Gladius' }, { code: 'clb', name: 'Club' }, { code: 'wsp', name: 'War Scepter' }, { code: 'flc', name: 'Flail' }, { code: 'crs', name: 'Crystal Sword' }, { nameZh: '水晶剑' }, { nameZh: '巨剑' }, { nameZh: '短弓' }, // Armors / Helms / Shields / Gloves / Boots / Belts { code: 'cap', name: 'Cap' }, { code: 'qui', name: 'Quilted Armor' }, { code: 'lea', name: 'Leather Armor' }, { code: 'hla', name: 'Hard Leather Armor' }, { code: 'fld', name: 'Field Plate' }, { code: 'plt', name: 'Plate Mail' }, { code: 'sml', name: 'Small Shield' }, { code: 'lgl', name: 'Leather Gloves' }, { code: 'lbt', name: 'Leather Boots' }, { code: 'mbl', name: 'Mesh Belt' }, { code: 'crn', name: 'Crown' }, { nameZh: '哥特战甲' }, { nameZh: '圆盾' }, // Jewelry & Charms { code: 'ring', name: 'Ring' }, { code: 'amu', name: 'Amulet' }, { code: 'cm1', name: 'Small Charm' }, { code: 'cm2', name: 'Large Charm' }, { code: 'cm3', name: 'Grand Charm' }, { code: 'jew', name: 'Jewel' }, { nameZh: '戒指' }, { nameZh: '护身符' }, { nameZh: '超大护身符' }, { nameZh: '珠宝' }, // Empty / Invalid null, undefined, {}, { code: '' }, ] for (const item of forbiddenItems) { expect(isTelekinesisItemAllowed(item)).toBe(false) } }) }) // ========================================================================= // 8. Object Interaction Whitelist // ========================================================================= describe('8. Object Interaction Whitelist', () => { it('whitelists chests, waypoints, stash, portals, shrines, doors, wells, urns', () => { const allowedObjects = [ 'chest', 'locked chest', 'chest_gold', 'waypoint', 'rogue_encampment_waypoint', 'stash', 'player_stash', 'portal', 'town_portal', 'red_portal', 'shrine', 'armor_shrine', 'combat_shrine', 'door', 'wood_door', 'jail_door', 'well', 'healing_well', 'urn', 'casket', 'sarcophagus', 'tomb', 'barrel', ] for (const obj of allowedObjects) { expect(isTelekinesisObjectAllowed(obj)).toBe(true) expect(isTelekinesisObjectAllowed(obj.toUpperCase())).toBe(true) expect(isTelekinesisObjectAllowed(` ${obj} `)).toBe(true) } const rejectedObjects = [ 'monster', 'fallen', 'zombie', 'trap', 'fire', 'light', 'torch', 'tree', 'rock', 'fence', 'wall', 'floor', '', null as any, ] for (const obj of rejectedObjects) { expect(isTelekinesisObjectAllowed(obj)).toBe(false) } }) it('triggers remote waypoint and stash interaction within 600px range', () => { const engine = createHarnessEngine() let waypointActivated = false let stashOpened = false const runtime: any = { waypoints: [{ arriveX: 300, arriveY: 100, waypointId: 1 }], stashes: [{ x: 500, y: 100 }], // Placed sufficiently distant from waypoint (dist > 80px) level: 'Rogue Encampment', levelId: 1, act: 1, } const context: any = { waypointNetwork: { activate: (id: number) => { if (id === 1) waypointActivated = true return true }, }, openStash: () => { stashOpened = true }, } const status = { textContent: '' } as HTMLElement // Click waypoint at (300, 100) (dist = 200px <= 600px) const wpResult = castSkill(43, 300, 100, { engine, runtime, hudManager: null, status, waypointNetwork: context.waypointNetwork, openStash: context.openStash, }) expect(wpResult).toBe(true) expect(waypointActivated).toBe(true) // Click stash at (500, 100) (dist = 400px <= 600px) const stashResult = castSkill(43, 500, 100, { engine, runtime, hudManager: null, status, waypointNetwork: context.waypointNetwork, openStash: context.openStash, }) expect(stashResult).toBe(true) expect(stashOpened).toBe(true) }) }) // ========================================================================= // 9. Monster Combat: Damage, 35% Knockback & 35 Stun Ticks // ========================================================================= describe('9. Monster Combat: Damage, 35% Knockback & 35 Stun Ticks', () => { it('verifies combat damage scaling: slvl to slvl + 1 across slvl 1 to 99', () => { for (let slvl = 1; slvl <= 99; slvl++) { const dmg = calculateTelekinesisDamage(slvl) expect(dmg.min).toBe(slvl) expect(dmg.max).toBe(slvl + 1) } expect(calculateTelekinesisDamage(1)).toEqual({ min: 1, max: 2 }) expect(calculateTelekinesisDamage(10)).toEqual({ min: 10, max: 11 }) expect(calculateTelekinesisDamage(20)).toEqual({ min: 20, max: 21 }) }) it('verifies 35 stun ticks applied to hit monster in castSkill', () => { const engine = createHarnessEngine() const targetMonster = { index: 0, stats: { id: 'm1', name: 'Zombie', hp: 1000, damage: 2, cooldownTicks: 10, reach: 20, aggroRadius: 100, speed: 50, xp: 5 }, x: 150, y: 100, hp: 1000, cooldown: 0, state: 'idle' as const, facing: 0, hitFlash: 0, corpseTicks: 0, stunTicks: 0, } engine.world.monsters.push(targetMonster) const status = { textContent: '' } as HTMLElement castSkill(43, 150, 100, { engine, runtime: {} as any, hudManager: null, status }) expect(targetMonster.stunTicks).toBe(35) }) it('empirically verifies 35% knockback probability across 2,000 trials', () => { const engine = createHarnessEngine() let knockbacks = 0 const totalTrials = 2000 for (let i = 0; i < totalTrials; i++) { const initialX = 150 const initialY = 100 const targetMonster = { index: 0, stats: { id: 'm1', name: 'Target', hp: 100000, damage: 0, cooldownTicks: 10, reach: 20, aggroRadius: 100, speed: 50, xp: 5 }, x: initialX, y: initialY, hp: 100000, cooldown: 0, state: 'idle' as const, facing: 0, hitFlash: 0, corpseTicks: 0, } engine.world.monsters = [targetMonster] engine.world.player.cooldown = 0 engine.world.player.mana = 1000 const status = { textContent: '' } as HTMLElement castSkill(43, initialX, initialY, { engine, runtime: {} as any, hudManager: null, status }) // Knockback displaces monster away from player if (targetMonster.x > initialX) { knockbacks++ } } const observedRate = knockbacks / totalTrials // Expected rate is 35%. With n = 2000, standard error is sqrt(0.35 * 0.65 / 2000) = 0.0107 (1.07%). // Accept within [31.5%, 38.5%] (3.5 standard deviations, 99.9% confidence) expect(observedRate).toBeGreaterThanOrEqual(0.315) expect(observedRate).toBeLessThanOrEqual(0.385) }) }) // ========================================================================= // 10. Energy Shield Mana Ratio Synergy // ========================================================================= describe('10. Energy Shield Mana Ratio Synergy', () => { it('verifies calculateEnergyShieldManaRatio across tkBlvl from 0 to 40 with 0.75 floor', () => { // 1.13c formula: (32 - min(20, tkBlvl)) / 16 expect(calculateEnergyShieldManaRatio(0)).toBe(2.0) // 32/16 = 200% mana expect(calculateEnergyShieldManaRatio(1)).toBe(1.9375) // 31/16 expect(calculateEnergyShieldManaRatio(2)).toBe(1.875) // 30/16 expect(calculateEnergyShieldManaRatio(4)).toBe(1.75) // 28/16 expect(calculateEnergyShieldManaRatio(8)).toBe(1.5) // 24/16 = 150% mana expect(calculateEnergyShieldManaRatio(12)).toBe(1.25) // 20/16 expect(calculateEnergyShieldManaRatio(16)).toBe(1.0) // 16/16 = 100% mana expect(calculateEnergyShieldManaRatio(18)).toBe(0.875) // 14/16 expect(calculateEnergyShieldManaRatio(20)).toBe(0.75) // 12/16 = 75% floor // Hard cap at 20 points: further points do not lower ratio below 0.75 expect(calculateEnergyShieldManaRatio(21)).toBe(0.75) expect(calculateEnergyShieldManaRatio(25)).toBe(0.75) expect(calculateEnergyShieldManaRatio(30)).toBe(0.75) expect(calculateEnergyShieldManaRatio(40)).toBe(0.75) expect(calculateEnergyShieldManaRatio(99)).toBe(0.75) }) it('enforces 1.13c blvl synergy rule: soft points (+skills, charges) do NOT reduce ratio', () => { // Character has 8 base hard points, but +12 from gear/soft skills -> effective level 20 const basePoints = 8 const softPoints = 12 const effectiveLevel = basePoints + softPoints // Authentic 1.13c rule: synergy uses blvl only! const ratioWithBase = calculateEnergyShieldManaRatio(basePoints) const ratioWithEffective = calculateEnergyShieldManaRatio(effectiveLevel) // The authentic ratio is 1.50, NOT 0.75 expect(ratioWithBase).toBe(1.5) expect(ratioWithEffective).toBe(0.75) // Soft points must not be passed into calculateEnergyShieldManaRatio expect(ratioWithBase).not.toBe(ratioWithEffective) }) it('handles negative or invalid input values gracefully by defaulting to ratio 2.0', () => { expect(calculateEnergyShieldManaRatio(-5)).toBe(2.0) expect(calculateEnergyShieldManaRatio(NaN)).toBe(2.0) expect(calculateEnergyShieldManaRatio(null as any)).toBe(2.0) expect(calculateEnergyShieldManaRatio(undefined as any)).toBe(2.0) }) }) })