Skip to main content
Glama
index.ts8.14 kB
/** * World Generation Module * * Integrates heightmap, climate, and biome generation into a unified API. * Follows TDD principles with deterministic, seed-based generation. */ export * from './heightmap.js'; export * from './climate.js'; export * from './biome.js'; export * from './river.js'; export * from './lakes.js'; export * from './regions.js'; export * from './structures.js'; export * from './validation.js'; import { generateHeightmap } from './heightmap.js'; import { generateClimateMap } from './climate.js'; import { generateBiomeMap } from './biome.js'; import { generateRivers } from './river.js'; import { generateLakes, LakeSpillway } from './lakes.js'; import { generateRegions, Region } from './regions.js'; import { placeStructures, StructureLocation } from './structures.js'; import { BiomeType } from '../../schema/biome.js'; /** * Complete world generation output */ export interface GeneratedWorld { seed: string; width: number; height: number; /** Elevation map (0-100, sea level at 20) */ elevation: Uint8Array; /** Temperature map (Celsius, -20 to 40) */ temperature: Int8Array; /** Moisture map (percentage, 0-100) */ moisture: Uint8Array; /** Biome assignment */ biomes: BiomeType[][]; /** River map (1 = river, 0 = no river) */ rivers: Uint8Array; /** Region definitions */ regions: Region[]; /** Region ID map */ regionMap: Int32Array; /** Placed structures */ structures: StructureLocation[]; } /** * World generation options */ export interface WorldGenOptions { /** Deterministic seed */ seed: string; /** Map width in cells */ width: number; /** Map height in cells */ height: number; /** Target land ratio (default 0.3 for 30% land) */ landRatio?: number; /** Number of noise octaves (default 6) */ octaves?: number; /** Equator temperature in Celsius (default 30) */ equatorTemp?: number; /** Pole temperature in Celsius (default -10) */ poleTemp?: number; /** Number of regions (default 10) */ numRegions?: number; /** Number of cities (default 5) */ numCities?: number; /** Number of towns (default 10) */ numTowns?: number; /** Number of dungeons (default 5) */ /** Number of dungeons (default 5) */ numDungeons?: number; /** Global temperature offset (shift entire map hotter/colder) */ temperatureOffset?: number; /** Global moisture offset (shift entire map wetter/drier) */ moistureOffset?: number; } /** * Generate a complete world * * This is the primary entry point for world generation. * Produces a deterministic world from a seed. * * @example * ```typescript * const world = generateWorld({ * seed: 'my-world-42', * width: 100, * height: 100, * }); * * console.log(world.biomes[50][50]); // BiomeType at equator, center * ``` */ export function generateWorld(options: WorldGenOptions): GeneratedWorld { const { seed, width, height, landRatio, octaves, numRegions, numCities, numTowns, numDungeons, temperatureOffset, moistureOffset } = options; // Step 1: Generate heightmap const elevation = generateHeightmap(seed, width, height, { landRatio, octaves, }); // Step 2: Generate climate (temperature + moisture) const climate = generateClimateMap(seed, width, height, elevation, { temperatureOffset, moistureOffset }); // Step 3: Assign biomes const biomeMap = generateBiomeMap({ width, height, temperature: climate.temperature, moisture: climate.moisture, elevation, }); // Step 4: Generate Rivers // Convert moisture to precipitation (Float32Array) const precipitation = new Float32Array(width * height); for (let i = 0; i < width * height; i++) precipitation[i] = climate.moisture[i]; const riverSystem = generateRivers({ seed, width, height, elevation, precipitation }); // Create raster river map from vector rivers const riverMap = new Uint8Array(width * height).fill(0); for (const river of riverSystem.rivers) { for (const p of river.path) { riverMap[p.y * width + p.x] = 1; } } // Step 4b: Generate Lakes (fills terrain depressions) const lakeResult = generateLakes({ width, height, elevation, rivers: riverMap, }); // Update biomes for lake tiles for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = y * width + x; if (lakeResult.lakeMap[idx] === 1) { biomeMap.biomes[y][x] = BiomeType.LAKE; // Clear river flag for lake tiles (lake replaces river) riverMap[idx] = 0; } } } // Step 4c: Trace rivers from lake spillways (lakes as river sources) const SEA_LEVEL = 20; for (const spillway of lakeResult.spillways) { traceSpillwayRiver( spillway, elevation, riverMap, lakeResult.lakeMap, SEA_LEVEL, width, height ); } // Step 5: Generate Regions const regionData = generateRegions({ seed, width, height, elevation, biomes: biomeMap.biomes, numRegions }); // Step 6: Place Structures const structures = placeStructures({ seed, width, height, elevation, biomes: biomeMap.biomes, riverMap, numCities, numTowns, numDungeons }); // Step 7: Normalize elevations for display (ocean=0, land=1-100) for (let i = 0; i < width * height; i++) { if (elevation[i] < SEA_LEVEL) { elevation[i] = 0; // Ocean at sea level surface } else { // Remap land from [20, 100] to [1, 100] const landElev = elevation[i] - SEA_LEVEL; // 0-80 range elevation[i] = Math.round(1 + (landElev / 80) * 99); // 1-100 range } } return { seed, width, height, elevation, temperature: climate.temperature, moisture: climate.moisture, biomes: biomeMap.biomes, rivers: riverMap, regions: regionData.regions, regionMap: regionData.regionMap, structures }; } /** * Quick world generation with defaults * * @example * ```typescript * const world = quickWorld('seed123', 50, 50); * ``` */ export function quickWorld(seed: string, width: number = 50, height: number = 50): GeneratedWorld { return generateWorld({ seed, width, height }); } /** * Trace a river from a lake spillway downhill to ocean or existing river */ function traceSpillwayRiver( spillway: LakeSpillway, elevation: Uint8Array, riverMap: Uint8Array, lakeMap: Uint8Array, seaLevel: number, width: number, height: number ): void { const neighbors = [ { dx: 0, dy: -1 }, { dx: 1, dy: -1 }, { dx: 1, dy: 0 }, { dx: 1, dy: 1 }, { dx: 0, dy: 1 }, { dx: -1, dy: 1 }, { dx: -1, dy: 0 }, { dx: -1, dy: -1 }, ]; let x = spillway.outflowX; let y = spillway.outflowY; const maxSteps = width * height; const visited = new Set<number>(); for (let step = 0; step < maxSteps; step++) { const idx = y * width + x; // Stop if we hit ocean if (elevation[idx] < seaLevel) break; // Stop if we hit an existing river (we've connected) if (riverMap[idx] === 1) break; // Stop if we hit a lake (shouldn't happen but safety check) if (lakeMap[idx] === 1) break; // Prevent infinite loops if (visited.has(idx)) break; visited.add(idx); // Mark this tile as river riverMap[idx] = 1; // Find lowest neighbor to flow to const currentElev = elevation[idx]; let bestX = -1; let bestY = -1; let lowestElev = currentElev; for (const { dx, dy } of neighbors) { const nx = x + dx; const ny = y + dy; if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; const nIdx = ny * width + nx; const nElev = elevation[nIdx]; // Skip lake tiles if (lakeMap[nIdx] === 1) continue; // Prefer strictly lower, but allow equal for plateaus if (nElev < lowestElev || (nElev === lowestElev && bestX === -1)) { lowestElev = nElev; bestX = nx; bestY = ny; } } // If no lower neighbor found, we're stuck (endorheic basin) if (bestX === -1) break; x = bestX; y = bestY; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server