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>
115 lines
3.2 KiB
TypeScript
115 lines
3.2 KiB
TypeScript
/**
|
|
* Resource Interaction System — Phase 4.3
|
|
*
|
|
* Handles player collecting elements from world resources
|
|
* (mineral veins, geysers). Press E near a resource to collect.
|
|
*/
|
|
|
|
import { query } from 'bitecs';
|
|
import type { World } from '../ecs/world';
|
|
import { Position, Resource, PlayerTag } from '../ecs/components';
|
|
import { removeGameEntity } from '../ecs/factory';
|
|
import type { Inventory } from './inventory';
|
|
|
|
/** Metadata for a resource entity (string data that can't go in bitECS arrays) */
|
|
export interface ResourceInfo {
|
|
itemId: string; // element symbol or compound id
|
|
tileX: number;
|
|
tileY: number;
|
|
}
|
|
|
|
/** Result of an interaction attempt */
|
|
export interface InteractionResult {
|
|
type: 'collected' | 'depleted' | 'inventory_full' | 'nothing_nearby';
|
|
itemId?: string;
|
|
remaining?: number;
|
|
}
|
|
|
|
/** Elements that mineral veins can yield */
|
|
export const MINERAL_ELEMENTS = ['Fe', 'Cu', 'Zn', 'Au', 'Sn'] as const;
|
|
|
|
/** Elements that geysers can yield */
|
|
export const GEYSER_ELEMENTS = ['S', 'H'] as const;
|
|
|
|
/**
|
|
* Deterministic element picker based on tile position and seed.
|
|
* Same (x, y, seed) always gives the same element.
|
|
*/
|
|
export function pickResourceElement(
|
|
tileX: number,
|
|
tileY: number,
|
|
seed: number,
|
|
options: readonly string[],
|
|
): string {
|
|
// Multiplicative hash for spatial distribution
|
|
const hash = ((tileX * 73856093) ^ (tileY * 19349663) ^ (seed * 83492791)) >>> 0;
|
|
return options[hash % options.length];
|
|
}
|
|
|
|
/**
|
|
* Interaction system — handles E-key resource collection.
|
|
*
|
|
* Finds closest resource in range, adds to inventory, decrements quantity.
|
|
* Returns null if E not pressed, or InteractionResult describing what happened.
|
|
*/
|
|
export function interactionSystem(
|
|
world: World,
|
|
justPressedInteract: boolean,
|
|
inventory: Inventory,
|
|
resourceData: Map<number, ResourceInfo>,
|
|
): InteractionResult | null {
|
|
if (!justPressedInteract) return null;
|
|
|
|
// Find player position
|
|
const players = query(world, [Position, PlayerTag]);
|
|
if (players.length === 0) return null;
|
|
const playerEid = players[0];
|
|
const px = Position.x[playerEid];
|
|
const py = Position.y[playerEid];
|
|
|
|
// Find closest resource in range
|
|
let closestEid: number | null = null;
|
|
let closestDist = Infinity;
|
|
|
|
for (const eid of query(world, [Position, Resource])) {
|
|
const dx = Position.x[eid] - px;
|
|
const dy = Position.y[eid] - py;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const range = Resource.interactRange[eid];
|
|
|
|
if (dist <= range && dist < closestDist) {
|
|
closestEid = eid;
|
|
closestDist = dist;
|
|
}
|
|
}
|
|
|
|
if (closestEid === null) {
|
|
return { type: 'nothing_nearby' };
|
|
}
|
|
|
|
const info = resourceData.get(closestEid);
|
|
if (!info) return { type: 'nothing_nearby' };
|
|
|
|
// Try to add to inventory
|
|
const added = inventory.addItem(info.itemId, 1);
|
|
if (added === 0) {
|
|
return { type: 'inventory_full', itemId: info.itemId };
|
|
}
|
|
|
|
// Decrement resource quantity
|
|
Resource.quantity[closestEid] -= 1;
|
|
|
|
if (Resource.quantity[closestEid] <= 0) {
|
|
// Resource depleted — remove entity
|
|
removeGameEntity(world, closestEid);
|
|
resourceData.delete(closestEid);
|
|
return { type: 'depleted', itemId: info.itemId };
|
|
}
|
|
|
|
return {
|
|
type: 'collected',
|
|
itemId: info.itemId,
|
|
remaining: Resource.quantity[closestEid],
|
|
};
|
|
}
|