257 lines
8.3 KiB
TypeScript
257 lines
8.3 KiB
TypeScript
/**
|
||
* 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)
|
||
})
|
||
})
|
||
})
|