Files
synthesis/tests/interaction.test.ts
Денис Шкабатур e77b9df6e4 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>
2026-02-12 13:25:35 +03:00

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
});
});