feat: resource collection with E-key interaction (Phase 4.3)
Mineral veins yield metals (Fe, Cu, Zn, Au, Sn), geysers yield S/H. Resources spawn as ECS entities with gold/orange dot sprites on tiles. E-key collects nearest resource in range into inventory. Resources deplete after collection. Visual feedback text. 12 new tests (174 total). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
191
tests/interaction.test.ts
Normal file
191
tests/interaction.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Resource Interaction Tests — Phase 4.3
|
||||
*
|
||||
* Tests: resource element picking, proximity detection,
|
||||
* collection into inventory, resource depletion.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createWorld, addEntity, addComponent, query } from 'bitecs';
|
||||
import { Position, Resource, PlayerTag } from '../src/ecs/components';
|
||||
import { Inventory } from '../src/player/inventory';
|
||||
import {
|
||||
pickResourceElement,
|
||||
interactionSystem,
|
||||
type ResourceInfo,
|
||||
type InteractionResult,
|
||||
MINERAL_ELEMENTS,
|
||||
GEYSER_ELEMENTS,
|
||||
} from '../src/player/interaction';
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
function createTestWorld() {
|
||||
const world = createWorld();
|
||||
const resourceData = new Map<number, ResourceInfo>();
|
||||
const inventory = new Inventory();
|
||||
return { world, resourceData, inventory };
|
||||
}
|
||||
|
||||
function addPlayer(world: ReturnType<typeof createWorld>, x: number, y: number): number {
|
||||
const eid = addEntity(world);
|
||||
addComponent(world, eid, Position);
|
||||
addComponent(world, eid, PlayerTag);
|
||||
Position.x[eid] = x;
|
||||
Position.y[eid] = y;
|
||||
return eid;
|
||||
}
|
||||
|
||||
function addResource(
|
||||
world: ReturnType<typeof createWorld>,
|
||||
resourceData: Map<number, ResourceInfo>,
|
||||
x: number,
|
||||
y: number,
|
||||
itemId: string,
|
||||
quantity: number,
|
||||
range = 40,
|
||||
): number {
|
||||
const eid = addEntity(world);
|
||||
addComponent(world, eid, Position);
|
||||
addComponent(world, eid, Resource);
|
||||
Position.x[eid] = x;
|
||||
Position.y[eid] = y;
|
||||
Resource.quantity[eid] = quantity;
|
||||
Resource.interactRange[eid] = range;
|
||||
resourceData.set(eid, { itemId, tileX: Math.floor(x / 32), tileY: Math.floor(y / 32) });
|
||||
return eid;
|
||||
}
|
||||
|
||||
// === pickResourceElement ===
|
||||
|
||||
describe('pickResourceElement', () => {
|
||||
it('always returns element from provided list', () => {
|
||||
for (let x = 0; x < 20; x++) {
|
||||
for (let y = 0; y < 20; y++) {
|
||||
const el = pickResourceElement(x, y, 12345, MINERAL_ELEMENTS);
|
||||
expect(MINERAL_ELEMENTS).toContain(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('is deterministic (same inputs → same output)', () => {
|
||||
const a = pickResourceElement(5, 10, 42, MINERAL_ELEMENTS);
|
||||
const b = pickResourceElement(5, 10, 42, MINERAL_ELEMENTS);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('varies with different positions', () => {
|
||||
const results = new Set<string>();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
results.add(pickResourceElement(i, i * 7, 42, MINERAL_ELEMENTS));
|
||||
}
|
||||
// Should use more than one element (statistical certainty)
|
||||
expect(results.size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('works with geyser elements', () => {
|
||||
const el = pickResourceElement(3, 7, 42, GEYSER_ELEMENTS);
|
||||
expect(GEYSER_ELEMENTS).toContain(el);
|
||||
});
|
||||
});
|
||||
|
||||
// === Interaction System ===
|
||||
|
||||
describe('interactionSystem — collection', () => {
|
||||
it('collects element when in range and pressing E', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 5);
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.type).toBe('collected');
|
||||
expect(result!.itemId).toBe('Fe');
|
||||
expect(inventory.getCount('Fe')).toBe(1);
|
||||
});
|
||||
|
||||
it('does nothing when E is not pressed', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 5);
|
||||
|
||||
const result = interactionSystem(world, false, inventory, resourceData);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(inventory.getCount('Fe')).toBe(0);
|
||||
});
|
||||
|
||||
it('does nothing when no resources in range', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 500, 500, 'Fe', 5); // far away
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.type).toBe('nothing_nearby');
|
||||
});
|
||||
|
||||
it('picks closest resource when multiple in range', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 130, 100, 'Cu', 3); // 30px away
|
||||
addResource(world, resourceData, 115, 100, 'Fe', 5); // 15px away (closer)
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result!.itemId).toBe('Fe'); // picked closer one
|
||||
});
|
||||
|
||||
it('decrements resource quantity on collection', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
const resEid = addResource(world, resourceData, 120, 100, 'Fe', 3);
|
||||
|
||||
interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(Resource.quantity[resEid]).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactionSystem — depletion', () => {
|
||||
it('depletes resource when quantity reaches 0', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 1);
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result!.type).toBe('depleted');
|
||||
expect(result!.itemId).toBe('Fe');
|
||||
expect(resourceData.size).toBe(0);
|
||||
});
|
||||
|
||||
it('removes entity from world on depletion', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 1);
|
||||
|
||||
interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
const remaining = query(world, [Resource]);
|
||||
expect(remaining.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactionSystem — inventory full', () => {
|
||||
it('reports inventory_full when cannot add', () => {
|
||||
const { world, resourceData } = createTestWorld();
|
||||
const inventory = new Inventory(1, 1); // very small
|
||||
inventory.addItem('H', 1); // fills it up
|
||||
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 5);
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result!.type).toBe('inventory_full');
|
||||
expect(Resource.quantity[query(world, [Resource])[0]]).toBe(5); // not consumed
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user