diablo2-web/tests/ground-items-pickup.test.ts

257 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)
})
})
})