/** * 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(); const inventory = new Inventory(); return { world, resourceData, inventory }; } function addPlayer(world: ReturnType, 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, resourceData: Map, 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(); 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 }); });