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:
106
src/world/resources.ts
Normal file
106
src/world/resources.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Resource Spawner — creates ECS entities for harvestable world objects
|
||||
*
|
||||
* Scans the generated tile grid for mineral veins and geysers,
|
||||
* creates an entity at each with a randomly assigned element.
|
||||
*/
|
||||
|
||||
import { addEntity, addComponent } from 'bitecs';
|
||||
import type { World } from '../ecs/world';
|
||||
import { Position, Resource, SpriteRef } from '../ecs/components';
|
||||
import type { TileGrid, BiomeData } from './types';
|
||||
import {
|
||||
pickResourceElement,
|
||||
MINERAL_ELEMENTS,
|
||||
GEYSER_ELEMENTS,
|
||||
type ResourceInfo,
|
||||
} from '../player/interaction';
|
||||
|
||||
/** Resource spawn configuration per tile type */
|
||||
interface ResourceTileConfig {
|
||||
tileId: number;
|
||||
elements: readonly string[];
|
||||
minQuantity: number;
|
||||
maxQuantity: number;
|
||||
interactRange: number;
|
||||
spriteColor: number;
|
||||
spriteRadius: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn resource entities for all resource tiles in the grid.
|
||||
* @returns Map of entity ID → ResourceInfo for string data
|
||||
*/
|
||||
export function spawnResources(
|
||||
world: World,
|
||||
grid: TileGrid,
|
||||
biome: BiomeData,
|
||||
seed: number,
|
||||
): Map<number, ResourceInfo> {
|
||||
const resourceData = new Map<number, ResourceInfo>();
|
||||
|
||||
// Find tile IDs for resource types
|
||||
const mineralTile = biome.tiles.find(t => t.name === 'mineral-vein');
|
||||
const geyserTile = biome.tiles.find(t => t.name === 'geyser');
|
||||
|
||||
const configs: ResourceTileConfig[] = [];
|
||||
|
||||
if (mineralTile) {
|
||||
configs.push({
|
||||
tileId: mineralTile.id,
|
||||
elements: MINERAL_ELEMENTS,
|
||||
minQuantity: 3,
|
||||
maxQuantity: 5,
|
||||
interactRange: 40,
|
||||
spriteColor: 0xffd700, // gold
|
||||
spriteRadius: 4,
|
||||
});
|
||||
}
|
||||
|
||||
if (geyserTile) {
|
||||
configs.push({
|
||||
tileId: geyserTile.id,
|
||||
elements: GEYSER_ELEMENTS,
|
||||
minQuantity: 2,
|
||||
maxQuantity: 4,
|
||||
interactRange: 48,
|
||||
spriteColor: 0xff6600, // orange
|
||||
spriteRadius: 5,
|
||||
});
|
||||
}
|
||||
|
||||
const tileSize = biome.tileSize;
|
||||
|
||||
for (let y = 0; y < grid.length; y++) {
|
||||
for (let x = 0; x < grid[y].length; x++) {
|
||||
const tileId = grid[y][x];
|
||||
const config = configs.find(c => c.tileId === tileId);
|
||||
if (!config) continue;
|
||||
|
||||
// Pick element deterministically
|
||||
const itemId = pickResourceElement(x, y, seed, config.elements);
|
||||
|
||||
// Quantity from deterministic hash
|
||||
const qHash = ((x * 48611) ^ (y * 29423) ^ (seed * 61379)) >>> 0;
|
||||
const range = config.maxQuantity - config.minQuantity + 1;
|
||||
const quantity = config.minQuantity + (qHash % range);
|
||||
|
||||
// Create entity at tile center
|
||||
const eid = addEntity(world);
|
||||
addComponent(world, eid, Position);
|
||||
addComponent(world, eid, Resource);
|
||||
addComponent(world, eid, SpriteRef);
|
||||
|
||||
Position.x[eid] = x * tileSize + tileSize / 2;
|
||||
Position.y[eid] = y * tileSize + tileSize / 2;
|
||||
Resource.quantity[eid] = quantity;
|
||||
Resource.interactRange[eid] = config.interactRange;
|
||||
SpriteRef.color[eid] = config.spriteColor;
|
||||
SpriteRef.radius[eid] = config.spriteRadius;
|
||||
|
||||
resourceData.set(eid, { itemId, tileX: x, tileY: y });
|
||||
}
|
||||
}
|
||||
|
||||
return resourceData;
|
||||
}
|
||||
Reference in New Issue
Block a user