Expand beyond vertical slice with two new biomes and massive chemistry expansion: Chemistry: +20 real elements (Li→U), +39 compounds (acids/salts/oxides/organics), +85 reactions (Haber process, thermite variants, smelting, fermentation, etc.) Biomes: Kinetic Mountains (physics/mechanics themed) and Verdant Forests (biology/ecology themed), each with 8 tile types and unique generation rules. Creatures: 6 new species — Pendulums/Mechanoids/Resonators (mountains), Symbiotes/Mimics/Spore-bearers (forests). Species filtered by biome. Infrastructure: CradleScene biome selector UI, generic world generator (tile lookup by property instead of hardcoded names), actinide element category. 487 tests passing (32 new). Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
/**
|
|
* 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 (generic: resource + interactive tiles)
|
|
const mineralTile = biome.tiles.find(t => t.resource);
|
|
const geyserTile = biome.tiles.find(t => t.interactive);
|
|
|
|
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;
|
|
}
|