/** * Diablo II v1.13c Ground Items Pathfinding & Pickup Tests (Issue #391). */ import { describe, it, expect, vi } from 'vitest' import { GameEngine } from '../src/game/engine.ts' import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../src/game/demo-data.ts' import { SceneMouseController } from '../src/scene/act-scene.ts' import type { GroundItemEntity } from '../src/game/ground-items.ts' function createTestEngine() { const terrain = { widthPx: 1000, heightPx: 1000, overlap: () => 0 } const engine = new GameEngine(terrain, { spawn: { x: 500, y: 500 }, stats: [], xpTable: DEMO_EXPERIENCE, skills: DEMO_SKILLS, questDefs: DEMO_QUESTS, combatOptions: { playerSpeed: 4, playerReach: 50, playerCooldownTicks: 10, playerDamage: 5, playerManaPerAttack: 1, respawnTicks: 100, }, talkRadius: 48, pickupRadius: 48, inventoryCols: 4, inventoryRows: 2, // Compact 4x2 grid for testing full capacity lootSeed: 42, npcDefs: [], }) return engine } describe('Ground Items Pickup & Pathfinding (Issue #391)', () => { describe('GameEngine Pickup Methods', () => { it('successfully picks up gold, increments metrics, and removes from groundItems', () => { const engine = createTestEngine() const gold = engine.dropGold(500, 510, 510) expect(gold).not.toBeNull() expect(engine.groundItems.count).toBe(1) const res = engine.pickupGold(gold!.id) expect(res.success).toBe(true) expect(res.amount).toBe(500) expect(engine.groundItems.count).toBe(0) expect(engine.metrics.pickups).toBe(1) }) it('returns false when picking up non-existent gold id', () => { const engine = createTestEngine() const res = engine.pickupGold('non-existent-gold') expect(res.success).toBe(false) expect(res.amount).toBe(0) }) it('picks up an equipment item into the bag when space is available', () => { const engine = createTestEngine() const item = { id: 'test-sword', name: 'Short Sword', nameZh: '短剑', invWidth: 1, invHeight: 2, } const entity = engine.dropItem(item, 520, 520) expect(engine.groundItems.count).toBe(1) const res = engine.pickupItem(entity.id) expect(res.success).toBe(true) expect(res.item).toBe(entity) expect(engine.groundItems.count).toBe(0) expect(engine.metrics.pickups).toBe(1) expect(engine.bag.contents.length).toBe(1) }) it('refuses pickup when the inventory bag is completely full', () => { const engine = createTestEngine() // 4x2 = 8 slots total // Fill the 4x2 bag with four 1x2 items for (let i = 0; i < 4; i++) { const added = engine.bag.add({ id: `filler-${i}`, name: `Filler ${i}`, invWidth: 1, invHeight: 2, } as any) expect(added).not.toBeNull() } expect(engine.bag.contents.length).toBe(4) // Try dropping and picking up another 1x2 item const extraItem = { id: 'extra-axe', name: 'Hand Axe', nameZh: '手斧', invWidth: 1, invHeight: 2, } const entity = engine.dropItem(extraItem, 530, 530) expect(engine.groundItems.count).toBe(1) const res = engine.pickupItem(entity.id) expect(res.success).toBe(false) expect(res.reason).toBe('full') expect(engine.groundItems.count).toBe(1) // Still on ground expect(engine.metrics.inventoryRefusals).toBe(1) }) }) describe('SceneMouseController Click & Pathfinding Integration', () => { function createMockController(engine: GameEngine) { const mockCanvas: any = { getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }), addEventListener: vi.fn(), removeEventListener: vi.fn(), } const mockStatus: any = { textContent: '' } const mockInput: any = { takeLeftClick: () => null, takeRightClick: () => null, shiftHeld: false, movement: () => ({ x: 0, y: 0 }), } const mockRuntime: any = { grid: { cellsX: 50, cellsY: 50 }, waypoints: [], stashes: [], } const controller = new SceneMouseController({ canvas: mockCanvas, engine, camera: { zoom: 1 } as any, input: mockInput, getRuntime: () => mockRuntime, hudManager: null, waypointNetwork: null as any, status: mockStatus, playerAnimator: { play: vi.fn(), update: vi.fn(), currentFrame: null } as any, getCharacter: () => null, }) return { controller, status: mockStatus } } it('immediately picks up ground item within authentic 48px range', () => { const engine = createTestEngine() engine.world.player.x = 500 engine.world.player.y = 500 const entity = engine.dropItem( { id: 'near-potion', name: 'Potion', nameZh: '治疗药剂', invWidth: 1, invHeight: 1 }, 520, // distance = 20px <= 48px 500, ) const { controller, status } = createMockController(engine) controller.pickupGroundItem(entity) expect(engine.groundItems.count).toBe(0) expect(engine.bag.contents.length).toBe(1) expect(status.textContent).toBe('拾起物品:治疗药剂') expect(controller.navTarget).toBeNull() expect(controller.pendingInteraction).toBeNull() }) it('sets pathfinding navTarget when ground item is farther than 48px', () => { const engine = createTestEngine() engine.world.player.x = 500 engine.world.player.y = 500 const entity = engine.dropItem( { id: 'far-sword', name: 'Broad Sword', nameZh: '阔剑', invWidth: 1, invHeight: 3 }, 650, // distance = 150px > 48px 500, ) const { controller } = createMockController(engine) controller.pickupGroundItem(entity) // Item should still be on ground, player begins navigating expect(engine.groundItems.count).toBe(1) expect(controller.navTarget).toEqual({ x: 650, y: 500 }) expect(controller.pendingInteraction).toEqual({ kind: 'ground-item', item: entity }) }) it('automatically executes pickup upon reaching ground item on tick', () => { const engine = createTestEngine() engine.world.player.x = 500 engine.world.player.y = 500 const entity = engine.dropItem( { id: 'distant-gold', isGold: true, amount: 1200 }, 600, 500, ) const { controller, status } = createMockController(engine) controller.pickupGroundItem(entity) // Move player close to item engine.world.player.x = 580 engine.world.player.y = 500 // distance = 20px <= 48px // Tick controller controller.tick() expect(engine.groundItems.count).toBe(0) expect(status.textContent).toBe('拾起金币:1200') expect(controller.pendingInteraction).toBeNull() expect(controller.navTarget).toBeNull() }) it('detects ground item click in handleLeftClickWorld within 32px', () => { const engine = createTestEngine() engine.world.player.x = 500 engine.world.player.y = 500 const entity = engine.dropItem( { id: 'ring', name: 'Ring', nameZh: '戒指', invWidth: 1, invHeight: 1 }, 510, 510, ) const { controller, status } = createMockController(engine) // Player clicks near item coordinates (515, 512), within 32px of (510, 510) controller.handleLeftClickWorld(515, 512) expect(engine.groundItems.count).toBe(0) expect(status.textContent).toBe('拾起物品:戒指') }) it('cleans up both groundItems and engine.ground on pickup, leaving zero residual dots', () => { const engine = createTestEngine() engine.world.player.x = 500 engine.world.player.y = 500 const entity = engine.dropItem( { id: 'sword', name: 'Short Sword', nameZh: '短剑', invWidth: 1, invHeight: 2 }, 505, 505, ) expect(engine.groundItems.count).toBe(1) expect(engine.ground.length).toBe(1) const { controller } = createMockController(engine) controller.pickupGroundItem(entity) // Verified: Both groundItems and engine.ground are cleanly cleared expect(engine.groundItems.count).toBe(0) expect(engine.ground.length).toBe(0) }) }) })