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:
114
src/player/interaction.ts
Normal file
114
src/player/interaction.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user