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>
192 lines
6.0 KiB
TypeScript
192 lines
6.0 KiB
TypeScript
/**
|
|
* 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
|
|
});
|
|
});
|