/** * Adversarial Challenger Stress Testing Suite for Milestone 2 Left-Click & Shift Controls * (Solution Stress Testing Playbook: Differential Fuzzing, Boundary Values, Invariant Checks) */ import { describe, expect, it } from 'vitest' import { KeyboardInput } from '../src/sim/input.ts' import { HudManager } from '../src/ui/hud-manager.ts' import { GameEngine, type GameEngineOptions, type WorldMapProvider } from '../src/game/engine.ts' import { ViewportCamera } from '../src/sim/camera.ts' import { WaypointNetwork } from '../src/game/portal.ts' import { SceneMouseController, screenToWorld, facingOf, projectNpcLabel, getSkillRange, getSkillMana, type MapRuntime, } from '../src/scene/act-scene.ts' class MockWindow extends EventTarget { innerWidth = 1920 innerHeight = 1080 devicePixelRatio = 1 } class MockCanvas extends EventTarget { clientWidth = 1920 clientHeight = 1080 width = 1920 height = 1080 style = { pointerEvents: 'none' } rect = { left: 0, top: 0, width: 1920, height: 1080 } getBoundingClientRect() { return this.rect } getContext() { return null } } class SyntheticKeyboardEvent extends Event { readonly code: string readonly key: string constructor(type: string, init: { code: string; key: string }) { super(type, { cancelable: true }) this.code = init.code this.key = init.key } } class SyntheticPointerEvent extends Event { readonly clientX: number readonly clientY: number readonly button: number readonly buttons: number readonly shiftKey: boolean private _defaultPrevented = false constructor(type: string, init: { clientX: number; clientY: number; button?: number; buttons?: number; shiftKey?: boolean; cancelable?: boolean }) { super(type, { cancelable: init.cancelable ?? true }) this.clientX = init.clientX this.clientY = init.clientY this.button = init.button ?? 0 this.buttons = init.buttons !== undefined ? init.buttons : (1 << (init.button ?? 0)) this.shiftKey = init.shiftKey ?? false } override get defaultPrevented(): boolean { return this._defaultPrevented } override preventDefault() { super.preventDefault() this._defaultPrevented = true } } const mockWin = new MockWindow() // @ts-ignore globalThis.window = mockWin // @ts-ignore globalThis.HTMLInputElement = class HTMLInputElement {} // @ts-ignore globalThis.HTMLSelectElement = class HTMLSelectElement {} function createChallengerScene(options?: { playerPos?: { x: number; y: number } mana?: number leftSkillId?: number rightSkillId?: number npcs?: Array<{ id: number; name: string; x: number; y: number }> waypoints?: Array<{ waypointId: number; x: number; y: number }> monsters?: Array<{ id: string; name: string; x: number; y: number; hp: number }> blockedOverlap?: (x: number, y: number) => number canvasRect?: { left: number; top: number; width: number; height: number } canvasSize?: { width: number; height: number } cameraZoom?: number }) { const canvas = new MockCanvas() as unknown as HTMLCanvasElement & { rect: { left: number; top: number; width: number; height: number } } if (options?.canvasRect) { canvas.rect = options.canvasRect canvas.clientWidth = options.canvasRect.width canvas.clientHeight = options.canvasRect.height } if (options?.canvasSize) { canvas.width = options.canvasSize.width canvas.height = options.canvasSize.height } const win = new MockWindow() const input = new KeyboardInput(win as unknown as Window) input.attach() const dummyTerrain: WorldMapProvider = { widthPx: 4000, heightPx: 4000, overlap: options?.blockedOverlap ?? (() => 0), } const dummyOpts: GameEngineOptions = { spawn: options?.playerPos ?? { x: 1000, y: 1000 }, stats: (options?.monsters ?? []).map(m => ({ id: m.id, name: m.name, hp: m.hp, damage: 10, cooldownTicks: 100, reach: 20, aggroRadius: 100, speed: 0, xp: 10, })), xpTable: [0, 0, 10, 30, 60], itemBases: [], prefixAffixes: [], suffixAffixes: [], skills: [{ id: 'attack', name: 'Attack', manaCost: 0, cooldownTicks: 1, range: 48, projectile: false, speed: 0, baseMinDamage: 10, baseMaxDamage: 15, damagePerLevel: 1, radius: 20 }], npcDefs: (options?.npcs ?? []).map(n => ({ id: `npc-${n.id}`, name: n.name, questId: null, offerLines: ['Greetings, wanderer.'], progressLines: [], doneLines: [], })), questDefs: [], combatOptions: { playerSpeed: 100, playerReach: 52, playerCooldownTicks: 1, playerDamage: 10, playerManaPerAttack: 0, respawnTicks: 100 }, lootSeed: 12345, talkRadius: 80, pickupRadius: 50, inventoryCols: 10, inventoryRows: 4, } const engine = new GameEngine(dummyTerrain, dummyOpts) if (options?.monsters) { for (let i = 0; i < options.monsters.length; i++) { engine.world.monsters[i]!.x = options.monsters[i]!.x engine.world.monsters[i]!.y = options.monsters[i]!.y } } if (options?.npcs) { for (const n of options.npcs) { engine.npcEntities.push({ def: { id: `npc-${n.id}`, name: n.name, questId: null, offerLines: ['Greetings, wanderer.'], progressLines: [], doneLines: [], }, x: n.x, y: n.y, hasSprite: false, }) } } const camera = new ViewportCamera(canvas, 4000, 4000) camera.zoom = options?.cameraZoom ?? 1 const hudManager = new HudManager(canvas, { onToggleAutomap: () => {}, onWaypointTeleport: () => {}, }) hudManager.mana = options?.mana ?? 200 hudManager.hotkeys.leftSkillId = options?.leftSkillId ?? 0 hudManager.hotkeys.rightSkillId = options?.rightSkillId ?? 0 hudManager.syncPublishedState() const waypointNetwork = new WaypointNetwork() if (options?.waypoints) { for (const wp of options.waypoints) { waypointNetwork.register({ waypointId: wp.waypointId, levelId: 1, act: 1, name: 'Test WP', x: wp.x, y: wp.y, }) } } const gridWidth = 500 const blockedGrid = new Uint8Array(500 * 500) const runtime: MapRuntime = { source: 'pack', base: 'samples/d2-packs', act: 1, level: 'Rogue Encampment', levelId: 1, variant: 'default', variants: ['default'], quadrant: 'default', quadrants: ['default'], cellsX: 100, cellsY: 100, widthPx: 4000, heightPx: 4000, floors: [], walls: [], roofs: [], objectDrawables: [], pages: [], priorityPages: [], objectPages: [], grid: { originX: 0, originY: 0, cellsX: 100, cellsY: 100, blocked: blockedGrid, gridWidth, }, npcs: (options?.npcs ?? []).map(n => ({ id: n.id, type: 1, name: n.name, x: n.x, y: n.y, })), entrances: [], warps: [], waypoints: (options?.waypoints ?? []).map(wp => { const dx = wp.x / 16 const dy = wp.y / 8 const subX = Math.round((dy + dx) / 2) const subY = Math.round((dy - dx) / 2) return { source: 'placed' as const, waypointId: wp.waypointId, arriveX: subX, arriveY: subY, x: subX, y: subY, } }), animatedDrawables: [], animSpeed: 1, frameDurationMs: 40, palette: null, charBases: [], monsterTypes: [], monsterPacks: [], entryPath: '', loadPages: async () => {}, } as unknown as MapRuntime const status = { textContent: '' } as HTMLElement const controller = new SceneMouseController({ canvas, engine, camera, input, getRuntime: () => runtime, hudManager, waypointNetwork, status, }) controller.attach() return { canvas, win, input, engine, camera, hudManager, waypointNetwork, runtime, status, controller, } } describe('Challenger Stress Tests: Milestone 2 Left-Click & Shift Controls', () => { describe('1. screenToWorld projection with varying camera zoom levels and DPRs', () => { it('exact round-trip invertibility across zoom levels [0.4x..8.0x] and DPRs [1.0..4.0]', () => { const zooms = [0.4, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0] const dprs = [1.0, 1.25, 1.333, 1.5, 2.0, 2.5, 3.0, 4.0] const viewports = [ { width: 800, height: 600, left: 0, top: 0 }, { width: 1280, height: 720, left: 10, top: 20 }, { width: 1920, height: 1080, left: 100, top: 50 }, { width: 2560, height: 1440, left: 0, top: 0 }, { width: 3440, height: 1440, left: 50, top: 0 }, // ultrawide ] let testIterations = 0 for (const zoom of zooms) { for (const dpr of dprs) { for (const vp of viewports) { const rect = { left: vp.left, top: vp.top, width: vp.width, height: vp.height } const canvas = { width: Math.round(vp.width * dpr), height: Math.round(vp.height * dpr), } const camX = 1500 const camY = 1200 // Test 20 random points per configuration for (let i = 0; i < 20; i++) { testIterations++ const originalWorldX = 500 + (i * 97) % 2000 const originalWorldY = 400 + (i * 113) % 2000 // Forward projection to screen client coordinates: const scaleX = canvas.width > 0 ? rect.width / canvas.width : 1 const scaleY = canvas.height > 0 ? rect.height / canvas.height : 1 const clientX = rect.left + rect.width * 0.5 + (originalWorldX - camX) * zoom * scaleX const clientY = rect.top + rect.height * 0.5 + (originalWorldY - camY) * zoom * scaleY // Invert back via screenToWorld: const unprojected = screenToWorld({ clientX, clientY, rect, canvas, camX, camY, cameraZoom: zoom, }) expect(Math.abs(unprojected.x - originalWorldX)).toBeLessThan(1e-4) expect(Math.abs(unprojected.y - originalWorldY)).toBeLessThan(1e-4) } } } } expect(testIterations).toBe(zooms.length * dprs.length * viewports.length * 20) }) it('center of viewport strictly projects to camera focal position regardless of zoom or DPR', () => { const dprs = [1.0, 1.5, 2.0, 3.0] const zooms = [0.4, 1.0, 2.5, 8.0] for (const dpr of dprs) { for (const zoom of zooms) { const rect = { left: 80, top: 40, width: 1600, height: 900 } const canvas = { width: Math.round(1600 * dpr), height: Math.round(900 * dpr) } const camX = 777.5 const camY = 888.25 const centerClientX = rect.left + rect.width * 0.5 // 80 + 800 = 880 const centerClientY = rect.top + rect.height * 0.5 // 40 + 450 = 490 const world = screenToWorld({ clientX: centerClientX, clientY: centerClientY, rect, canvas, camX, camY, cameraZoom: zoom, }) expect(world.x).toBeCloseTo(camX, 5) expect(world.y).toBeCloseTo(camY, 5) } } }) it('consistency between screenToWorld and projectNpcLabel', () => { const rect = { left: 0, top: 0, width: 1920, height: 1080 } const canvas = { width: 3840, height: 2160 } // DPR 2.0 const camX = 1000 const camY = 1000 const zoom = 1.75 const npc = { x: 1150, y: 920, frame: null } const projection = projectNpcLabel({ npc, camX, camY, cameraZoom: zoom, canvas, rect, markerHeight: 0, }) // unproject the projected dom position const worldBack = screenToWorld({ clientX: projection.domX, clientY: projection.domY + 2, // projectNpcLabel offsets domY by -2 rect, canvas, camX, camY, cameraZoom: zoom, }) expect(worldBack.x).toBeCloseTo(npc.x, 3) expect(worldBack.y).toBeCloseTo(npc.y, 3) }) }) describe('2. Ground Click-to-Move: navTarget, player movement, arrival threshold <= 8px', () => { it('clicking on ground sets navTarget and produces normalized directional movement vector', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 } }) // Click ground at world (1100, 1000) -> 100px East // Camera is centered at (player.x, player.y - 16) = (1000, 984) // client center is (960, 540). (1100, 1000) corresponds to clientX = 960 + 100 = 1060, clientY = 540 + 16 = 556 scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 1060, clientY: 556, button: 0, })) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerup', { clientX: 1060, clientY: 556, button: 0, })) expect(scene.controller.navTarget).not.toBeNull() expect(scene.controller.navTarget!.x).toBeCloseTo(1100, 1) expect(scene.controller.navTarget!.y).toBeCloseTo(1000, 1) const tickResult = scene.controller.tick() expect(tickResult.movement.x).toBeGreaterThan(0.99) expect(tickResult.movement.y).toBeCloseTo(0, 2) expect(tickResult.attacking).toBe(false) }) it('strict arrival threshold <= 8px boundary check', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 } }) scene.controller.navTarget = { x: 1000, y: 1000 } // Test distance exactly 8.000px: MUST stop and clear navTarget scene.controller.navTarget = { x: 1008, y: 1000 } let tick = scene.controller.tick() expect(tick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.navTarget).toBeNull() // Test distance 7.5px (< 8px): MUST stop scene.controller.navTarget = { x: 1000, y: 1007.5 } tick = scene.controller.tick() expect(tick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.navTarget).toBeNull() // Test distance 8.05px (> 8px): MUST continue moving scene.controller.navTarget = { x: 1008.05, y: 1000 } tick = scene.controller.tick() expect(tick.movement.x).toBeGreaterThan(0) expect(scene.controller.navTarget).not.toBeNull() }) it('continuous drag updates navTarget with cursor coordinates on pointermove', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 } }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 1060, clientY: 556, button: 0, })) expect(scene.controller.isLeftMouseDown).toBe(true) // Move cursor across multiple positions const positions = [ { clientX: 1100, clientY: 556 }, { clientX: 1200, clientY: 600 }, { clientX: 900, clientY: 400 }, ] for (const pos of positions) { scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointermove', { clientX: pos.clientX, clientY: pos.clientY, button: 0, })) const expectedWorld = scene.controller.getPointerWorldCoords(pos.clientX, pos.clientY) expect(scene.controller.navTarget!.x).toBeCloseTo(expectedWorld.x, 3) expect(scene.controller.navTarget!.y).toBeCloseTo(expectedWorld.y, 3) } scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerup', { clientX: 900, clientY: 400, button: 0, })) expect(scene.controller.isLeftMouseDown).toBe(false) }) it('running mode applies 1.5x runFactor to movement vector', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 } }) scene.controller.navTarget = { x: 1100, y: 1000 } // 100px East // Walk mode scene.hudManager.isRunning = false const walkTick = scene.controller.tick() expect(walkTick.movement.x).toBeCloseTo(1.0, 3) // Run mode scene.hudManager.isRunning = true const runTick = scene.controller.tick() expect(runTick.movement.x).toBeCloseTo(1.5, 3) }) }) describe('3. NPC interaction: immediate dialogue (<= 80px) vs pathing and arrival (> 80px)', () => { it('immediate dialogue when clicking NPC at distance <= 80px without moving', () => { // NPC placed at (1050, 1000) -> distance 50px <= 80px const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, npcs: [{ id: 1, name: 'Akara', x: 1050, y: 1000 }], }) // Click near Akara's feet (1050, 1000) const clientCoords = { clientX: 960 + 50, clientY: 540 + 16, } scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: clientCoords.clientX, clientY: clientCoords.clientY, button: 0, })) // Must trigger dialogue immediately expect(scene.controller.navTarget).toBeNull() expect(scene.controller.pendingInteraction).toBeNull() expect(scene.status.textContent).toContain('Akara') expect(scene.engine.dialog).not.toBeNull() expect(scene.engine.world.player.facing).toBe(6) // Facing East expect(scene.controller.tick().movement).toEqual({ x: 0, y: 0 }) }) it('paths towards NPC when distance > 80px and opens dialogue on arrival', () => { // NPC placed at (1300, 1000) -> distance 300px > 80px const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, npcs: [{ id: 2, name: 'Warriv', x: 1300, y: 1000 }], }) const clientCoords = { clientX: 960 + 300, clientY: 540 + 16, } scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: clientCoords.clientX, clientY: clientCoords.clientY, button: 0, })) // Must set pendingInteraction and navTarget expect(scene.controller.pendingInteraction).toEqual({ kind: 'npc', npc: expect.objectContaining({ x: 1300, y: 1000 }), }) expect(scene.controller.navTarget).toEqual({ x: 1300, y: 1000 }) // Tick moves towards Warriv const tick1 = scene.controller.tick() expect(tick1.movement.x).toBeGreaterThan(0) expect(scene.engine.dialog).toBeNull() // Step player closer to distance 80px: x = 1220 (dist = 80px) scene.engine.world.player.x = 1220 const arrivalTick = scene.controller.tick() expect(arrivalTick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.pendingInteraction).toBeNull() expect(scene.controller.navTarget).toBeNull() expect(scene.status.textContent).toContain('Warriv') expect(scene.engine.dialog).not.toBeNull() expect(scene.engine.world.player.facing).toBe(6) // Facing East }) it('selects closest NPC when multiple NPCs are nearby', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, npcs: [ { id: 1, name: 'Akara', x: 1030, y: 1000 }, // 30px away { id: 2, name: 'Charsi', x: 1060, y: 1000 }, // 60px away ], }) // Click on Akara (1030, 1000) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 30, clientY: 540 + 16, button: 0, })) expect(scene.status.textContent).toContain('Akara') expect(scene.status.textContent).not.toContain('Charsi') }) }) describe('4. Waypoint interaction: immediate activation (<= 80px) vs pathing and arrival (> 80px)', () => { it('immediate activation and panel opening when clicking Waypoint at distance <= 80px', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, waypoints: [{ waypointId: 1, x: 1040, y: 1000 }], // 40px away }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 40, clientY: 540 + 16, button: 0, })) expect(scene.controller.navTarget).toBeNull() expect(scene.controller.pendingInteraction).toBeNull() expect(scene.waypointNetwork.destinations().some(d => d.waypointId === 1)).toBe(true) expect(scene.hudManager.leftPanel).toBe('waypoint') expect(scene.controller.tick().movement).toEqual({ x: 0, y: 0 }) }) it('paths towards Waypoint when distance > 80px and activates upon arrival', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, waypoints: [{ waypointId: 2, x: 1400, y: 1000 }], // 400px away }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 400, clientY: 540 + 16, button: 0, })) expect(scene.controller.pendingInteraction?.kind).toBe('waypoint') expect(scene.controller.navTarget).not.toBeNull() expect(scene.hudManager.leftPanel).toBe('none') // Tick moves towards waypoint const walkTick = scene.controller.tick() expect(walkTick.movement.x).toBeGreaterThan(0) // Move player within 80px: x = 1330 (dist = 70px) scene.engine.world.player.x = 1330 const arriveTick = scene.controller.tick() expect(arriveTick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.pendingInteraction).toBeNull() expect(scene.controller.navTarget).toBeNull() expect(scene.waypointNetwork.destinations().some(d => d.waypointId === 2)).toBe(true) expect(scene.hudManager.leftPanel).toBe('waypoint') }) }) describe('5. Monster targeting: melee reach 52px vs spell range, approaches and attacks', () => { it('melee attack (skill 0): attacks immediately at distance <= 52px without pathing', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 0, monsters: [{ id: 'mon1', name: 'Fallen', x: 1040, y: 1000, hp: 50 }], // 40px <= 52px }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 40, clientY: 540 + 16, button: 0, })) // Attacks immediately expect(scene.controller.navTarget).toBeNull() expect(scene.controller.pendingInteraction).toBeNull() expect(scene.engine.world.monsters[0]!.hp).toBeLessThan(50) expect(scene.engine.metrics.playerHits).toBe(1) expect(scene.engine.world.player.cooldown).toBe(12) expect(scene.controller.tick().movement).toEqual({ x: 0, y: 0 }) }) it('melee attack (skill 0): paths to monster when distance > 52px and attacks on arrival', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 0, monsters: [{ id: 'mon2', name: 'Zombie', x: 1200, y: 1000, hp: 80 }], // 200px > 52px }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 200, clientY: 540 + 16, button: 0, })) expect(scene.controller.pendingInteraction?.kind).toBe('monster') expect(scene.controller.navTarget).toEqual({ x: 1200, y: 1000 }) expect(scene.engine.world.monsters[0]!.hp).toBe(80) // Move player into reach: x = 1160 (dist = 40px <= 52px) scene.engine.world.player.x = 1160 scene.engine.world.player.cooldown = 0 const attackTick = scene.controller.tick() expect(attackTick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.pendingInteraction).toBeNull() expect(scene.controller.navTarget).toBeNull() expect(scene.engine.world.monsters[0]!.hp).toBeLessThan(80) expect(scene.engine.metrics.playerHits).toBe(1) }) it('spell attack (skill 47 Fire Ball): casts from range (<= 450px) without pathing', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 47, // Fire Ball, range 450 mana: 100, monsters: [{ id: 'mon3', name: 'Gargoyle', x: 1350, y: 1000, hp: 120 }], // 350px <= 450px }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 350, clientY: 540 + 16, button: 0, })) // Casts immediately from 350px away: spawns projectile, deducts mana, no pathing expect(scene.controller.navTarget).toBeNull() expect(scene.controller.pendingInteraction).toBeNull() expect(scene.engine.projectiles.length).toBe(1) expect(scene.engine.projectiles[0]!.skillId).toBe('47') expect(scene.hudManager.mana).toBeLessThan(100) expect(scene.controller.tick().movement).toEqual({ x: 0, y: 0 }) }) it('spell attack: paths when monster is beyond spell range (> 450px)', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 47, mana: 100, monsters: [{ id: 'mon4', name: 'Skeleton', x: 1600, y: 1000, hp: 100 }], // 600px > 450px }) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 600, clientY: 540 + 16, button: 0, })) expect(scene.controller.pendingInteraction?.kind).toBe('monster') expect(scene.controller.navTarget).toEqual({ x: 1600, y: 1000 }) expect(scene.engine.projectiles.length).toBe(0) // Move player into spell range: x = 1200 (dist = 400px <= 450px) scene.engine.world.player.x = 1200 scene.engine.world.player.cooldown = 0 const castTick = scene.controller.tick() expect(castTick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.pendingInteraction).toBeNull() expect(scene.controller.navTarget).toBeNull() expect(scene.engine.projectiles.length).toBe(1) }) it('dead monsters cannot be targeted or clicked', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, monsters: [{ id: 'monDead', name: 'Dead Zombie', x: 1040, y: 1000, hp: 0 }], }) scene.engine.world.monsters[0]!.state = 'dead' // Click directly on dead monster scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 40, clientY: 540 + 16, button: 0, })) // Should be treated as ground click, NOT monster interaction expect(scene.controller.pendingInteraction).toBeNull() expect(scene.controller.navTarget).not.toBeNull() }) }) describe('6. Shift + Left Click: forced {0,0} velocity, face cursor, execute attack without pathing', () => { it('Shift + Left Click forces velocity {0, 0}, faces cursor, and attacks without pathing', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 47, // Fire Ball mana: 100, }) // Hold Shift scene.win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'ShiftLeft', key: 'Shift' })) expect(scene.input.shiftHeld).toBe(true) // Click ground far away (1500, 1000) -> East scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 500, clientY: 540 + 16, button: 0, shiftKey: true, })) // Navigation target must remain null (stand still) expect(scene.controller.navTarget).toBeNull() expect(scene.controller.pendingInteraction).toBeNull() // Player faces cursor (East = 6) expect(scene.engine.world.player.facing).toBe(6) // Spell cast executed expect(scene.engine.projectiles.length).toBe(1) expect(scene.engine.projectiles[0]!.vx).toBeGreaterThan(0) expect(scene.hudManager.mana).toBeLessThan(100) // Tick movement must be strictly {0, 0} const tick = scene.controller.tick() expect(tick.movement).toEqual({ x: 0, y: 0 }) }) it('Shift + Left Click with Melee Attack (skill 0) swings in place without moving', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 0, // Melee attack }) scene.win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'ShiftRight', key: 'Shift' })) expect(scene.input.shiftHeld).toBe(true) // Click North-West scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 - 200, clientY: 540 - 200, button: 0, shiftKey: true, })) expect(scene.controller.navTarget).toBeNull() expect(scene.engine.world.player.facing).toBe(3) // North-West expect(scene.engine.world.player.cooldown).toBe(12) expect(scene.engine.world.player.swingTicks).toBe(6) expect(scene.controller.tick().movement).toEqual({ x: 0, y: 0 }) }) it('continuous hold: player stays rooted in place across multiple frames while casting', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 47, mana: 300, }) scene.win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'ShiftLeft', key: 'Shift' })) scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 200, clientY: 540 + 16, button: 0, shiftKey: true, })) // Run 30 consecutive ticks for (let t = 0; t < 30; t++) { scene.engine.world.player.cooldown = 0 // simulate cooldown completing const tick = scene.controller.tick() expect(tick.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.navTarget).toBeNull() } // Multiple projectiles fired while player never moved expect(scene.engine.projectiles.length).toBeGreaterThan(1) expect(scene.engine.world.player.x).toBe(1000) expect(scene.engine.world.player.y).toBe(1000) }) it('pressing Shift while navTarget is active must immediately arrest movement to {0,0}', () => { const scene = createChallengerScene({ playerPos: { x: 1000, y: 1000 }, leftSkillId: 47, }) // Ground click to start walking East scene.canvas.dispatchEvent(new SyntheticPointerEvent('pointerdown', { clientX: 960 + 200, clientY: 540 + 16, button: 0, })) expect(scene.controller.navTarget).not.toBeNull() // First tick moves player East const tick1 = scene.controller.tick() expect(tick1.movement.x).toBeGreaterThan(0) // Player presses Shift while left mouse is still down scene.win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'ShiftLeft', key: 'Shift' })) expect(scene.input.shiftHeld).toBe(true) // Next tick with Shift held MUST arrest movement to {0, 0} const tickWithShift = scene.controller.tick() expect(tickWithShift.movement).toEqual({ x: 0, y: 0 }) expect(scene.controller.navTarget).toBeNull() }) }) })