Skip to main content
Glama
terrain-patterns.ts12.6 kB
/** * terrain-patterns.ts * * Procedural terrain pattern generators for consistent geometric layouts * Used by generate_terrain_patch and generate_terrain_pattern tools */ export interface TerrainPatternResult { obstacles: string[]; // "x,y" format water: string[]; difficultTerrain: string[]; props: Array<{ position: string; label: string; heightFeet: number; propType: string; cover: string; }>; } /** * Generate a river valley pattern * Two parallel cliff walls with a wide river in the center */ export function generateRiverValley( originX: number, originY: number, width: number, height: number ): TerrainPatternResult { const obstacles: string[] = []; const water: string[] = []; // Calculate wall positions (at edges) and river center const westWallX = originX + 2; const eastWallX = originX + width - 3; const riverStartX = originX + Math.floor(width / 2) - 1; const riverWidth = 3; // Generate west cliff wall for (let y = originY; y < originY + height; y++) { obstacles.push(`${westWallX},${y}`); // Add some depth to cliff (2 tiles wide) obstacles.push(`${westWallX + 1},${y}`); } // Generate east cliff wall for (let y = originY; y < originY + height; y++) { obstacles.push(`${eastWallX},${y}`); obstacles.push(`${eastWallX - 1},${y}`); } // Generate river (3 tiles wide in center) for (let y = originY; y < originY + height; y++) { for (let dx = 0; dx < riverWidth; dx++) { water.push(`${riverStartX + dx},${y}`); } } // Add cliff props const props = [ { position: `${westWallX},${originY + 2}`, label: 'West Cliff', heightFeet: 30, propType: 'structure', cover: 'full' }, { position: `${eastWallX},${originY + 2}`, label: 'East Cliff', heightFeet: 30, propType: 'structure', cover: 'full' }, ]; return { obstacles, water, difficultTerrain: [], props }; } /** * Generate a canyon pattern (horizontal walls) * Two parallel walls running east-west with a pass between */ export function generateCanyon( originX: number, originY: number, width: number, height: number ): TerrainPatternResult { const obstacles: string[] = []; // Calculate wall positions const northWallY = originY + 3; const southWallY = originY + height - 4; // Generate north wall for (let x = originX; x < originX + width; x++) { obstacles.push(`${x},${northWallY}`); obstacles.push(`${x},${northWallY - 1}`); } // Generate south wall for (let x = originX; x < originX + width; x++) { obstacles.push(`${x},${southWallY}`); obstacles.push(`${x},${southWallY + 1}`); } const props = [ { position: `${originX + Math.floor(width/2)},${northWallY}`, label: 'North Canyon Wall', heightFeet: 25, propType: 'structure', cover: 'full' }, { position: `${originX + Math.floor(width/2)},${southWallY}`, label: 'South Canyon Wall', heightFeet: 25, propType: 'structure', cover: 'full' }, ]; return { obstacles, water: [], difficultTerrain: [], props }; } /** * Generate an arena pattern * Circular wall perimeter enclosing an open area */ export function generateArena( originX: number, originY: number, width: number, height: number ): TerrainPatternResult { const obstacles: string[] = []; const centerX = originX + Math.floor(width / 2); const centerY = originY + Math.floor(height / 2); const radius = Math.min(width, height) / 2 - 2; // Generate circular perimeter using Bresenham's circle algorithm approximation for (let angle = 0; angle < 360; angle += 5) { const rad = angle * Math.PI / 180; const x = Math.round(centerX + radius * Math.cos(rad)); const y = Math.round(centerY + radius * Math.sin(rad)); const key = `${x},${y}`; if (!obstacles.includes(key)) { obstacles.push(key); } } const props = [ { position: `${centerX},${originY + 1}`, label: 'Arena North Gate', heightFeet: 15, propType: 'structure', cover: 'three-quarter' }, { position: `${centerX},${originY + height - 2}`, label: 'Arena South Gate', heightFeet: 15, propType: 'structure', cover: 'three-quarter' }, ]; return { obstacles, water: [], difficultTerrain: [], props }; } /** * Generate a mountain pass pattern * Narrowing corridor toward the center */ export function generateMountainPass( originX: number, originY: number, width: number, height: number ): TerrainPatternResult { const obstacles: string[] = []; const difficultTerrain: string[] = []; const centerY = originY + Math.floor(height / 2); // Generate narrowing walls for (let y = originY; y < originY + height; y++) { const distFromCenter = Math.abs(y - centerY); const wallOffset = Math.floor(distFromCenter / 3) + 3; // Left wall (narrows toward center) obstacles.push(`${originX + wallOffset},${y}`); // Right wall (mirrors left) obstacles.push(`${originX + width - wallOffset - 1},${y}`); // Add difficult terrain near walls (scree/rocks) if (distFromCenter > 2) { difficultTerrain.push(`${originX + wallOffset + 1},${y}`); difficultTerrain.push(`${originX + width - wallOffset - 2},${y}`); } } const props = [ { position: `${originX + Math.floor(width/2)},${centerY}`, label: 'Pass Chokepoint', heightFeet: 5, propType: 'cover', cover: 'half' }, ]; return { obstacles, water: [], difficultTerrain, props }; } /** * Generate a maze using recursive backtracking algorithm * Creates a proper maze with corridors and walls */ export function generateMaze( originX: number, originY: number, width: number, height: number, seed?: string, corridorWidth: number = 1 ): TerrainPatternResult { const obstacles: string[] = []; // Seeded random for reproducibility const seedNum = seed ? seed.split('').reduce((a, c) => a + c.charCodeAt(0), 0) : Date.now(); let rngState = seedNum; const random = () => { rngState = (rngState * 1103515245 + 12345) & 0x7fffffff; return rngState / 0x7fffffff; }; // Scale maze cells based on corridor width const cellSize = corridorWidth + 1; // wall + corridor const mazeWidth = Math.floor((width - 1) / cellSize); const mazeHeight = Math.floor((height - 1) / cellSize); // Initialize all cells as walls const grid: boolean[][] = Array(height).fill(null).map(() => Array(width).fill(true)); // Carve out the maze using recursive backtracking const stack: [number, number][] = []; const visited: boolean[][] = Array(mazeHeight).fill(null).map(() => Array(mazeWidth).fill(false)); // Start from center-ish const startCellX = Math.floor(mazeWidth / 2); const startCellY = Math.floor(mazeHeight / 2); // Convert cell coords to grid coords const cellToGrid = (cx: number, cy: number): [number, number] => { return [originX + 1 + cx * cellSize, originY + 1 + cy * cellSize]; }; // Carve a cell (make it passable) const carveCell = (cx: number, cy: number) => { const [gx, gy] = cellToGrid(cx, cy); for (let dy = 0; dy < corridorWidth; dy++) { for (let dx = 0; dx < corridorWidth; dx++) { if (gy + dy < originY + height && gx + dx < originX + width) { grid[gy + dy - originY][gx + dx - originX] = false; } } } }; // Carve passage between two adjacent cells const carvePassage = (cx1: number, cy1: number, cx2: number, cy2: number) => { const [gx1, gy1] = cellToGrid(cx1, cy1); const [gx2, gy2] = cellToGrid(cx2, cy2); // Carve the wall between cells const midX = Math.min(gx1, gx2) + (cx1 !== cx2 ? corridorWidth : 0); const midY = Math.min(gy1, gy2) + (cy1 !== cy2 ? corridorWidth : 0); for (let dy = 0; dy < corridorWidth; dy++) { for (let dx = 0; dx < corridorWidth; dx++) { const y = (cy1 === cy2) ? gy1 + dy - originY : midY + dy - originY; const x = (cx1 === cx2) ? gx1 + dx - originX : midX + dx - originX; if (y >= 0 && y < height && x >= 0 && x < width) { grid[y][x] = false; } } } }; // Get unvisited neighbors const getNeighbors = (cx: number, cy: number): [number, number][] => { const neighbors: [number, number][] = []; const dirs: [number, number][] = [[0, -1], [1, 0], [0, 1], [-1, 0]]; for (const [dx, dy] of dirs) { const nx = cx + dx; const ny = cy + dy; if (nx >= 0 && nx < mazeWidth && ny >= 0 && ny < mazeHeight && !visited[ny][nx]) { neighbors.push([nx, ny]); } } return neighbors; }; // Start carving visited[startCellY][startCellX] = true; carveCell(startCellX, startCellY); stack.push([startCellX, startCellY]); while (stack.length > 0) { const [cx, cy] = stack[stack.length - 1]; const neighbors = getNeighbors(cx, cy); if (neighbors.length === 0) { stack.pop(); } else { // Pick random neighbor const [nx, ny] = neighbors[Math.floor(random() * neighbors.length)]; visited[ny][nx] = true; carveCell(nx, ny); carvePassage(cx, cy, nx, ny); stack.push([nx, ny]); } } // Convert grid to obstacle coordinates for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (grid[y][x]) { obstacles.push(`${originX + x},${originY + y}`); } } } return { obstacles, water: [], difficultTerrain: [], props: [] }; } /** * Generate a maze with rooms (chambers connected by corridors) */ export function generateMazeWithRooms( originX: number, originY: number, width: number, height: number, seed?: string, roomCount: number = 5, minRoomSize: number = 4, maxRoomSize: number = 8 ): TerrainPatternResult { // First generate base maze const baseMaze = generateMaze(originX, originY, width, height, seed, 1); const obstacles = new Set(baseMaze.obstacles); // Seeded random const seedNum = seed ? seed.split('').reduce((a, c) => a + c.charCodeAt(0), 0) + 1000 : Date.now(); let rngState = seedNum; const random = () => { rngState = (rngState * 1103515245 + 12345) & 0x7fffffff; return rngState / 0x7fffffff; }; // Carve out rooms const rooms: {x: number; y: number; w: number; h: number}[] = []; for (let i = 0; i < roomCount * 3 && rooms.length < roomCount; i++) { const rw = Math.floor(random() * (maxRoomSize - minRoomSize + 1)) + minRoomSize; const rh = Math.floor(random() * (maxRoomSize - minRoomSize + 1)) + minRoomSize; const rx = originX + Math.floor(random() * (width - rw - 2)) + 1; const ry = originY + Math.floor(random() * (height - rh - 2)) + 1; // Check overlap with existing rooms let overlaps = false; for (const room of rooms) { if (rx < room.x + room.w + 1 && rx + rw + 1 > room.x && ry < room.y + room.h + 1 && ry + rh + 1 > room.y) { overlaps = true; break; } } if (!overlaps) { rooms.push({ x: rx, y: ry, w: rw, h: rh }); // Carve out the room for (let y = ry; y < ry + rh; y++) { for (let x = rx; x < rx + rw; x++) { obstacles.delete(`${x},${y}`); } } } } // Add room props for navigation const props = rooms.map((room, i) => ({ position: `${room.x + Math.floor(room.w/2)},${room.y + Math.floor(room.h/2)}`, label: `Chamber ${i + 1}`, heightFeet: 0, propType: 'marker', cover: 'none' })); return { obstacles: Array.from(obstacles), water: [], difficultTerrain: [], props }; } /** * Get pattern generator by name */ export function getPatternGenerator( pattern: 'river_valley' | 'canyon' | 'arena' | 'mountain_pass' | 'maze' | 'maze_rooms' ): (originX: number, originY: number, width: number, height: number, seed?: string) => TerrainPatternResult { switch (pattern) { case 'river_valley': return generateRiverValley; case 'canyon': return generateCanyon; case 'arena': return generateArena; case 'mountain_pass': return generateMountainPass; case 'maze': return generateMaze; case 'maze_rooms': return (ox, oy, w, h, s) => generateMazeWithRooms(ox, oy, w, h, s); default: return generateCanyon; } } export const PATTERN_DESCRIPTIONS = { river_valley: 'Parallel cliff walls on east/west edges with 3-wide river in center', canyon: 'Two parallel walls running east-west with open pass between', arena: 'Circular wall perimeter enclosing an open fighting area', mountain_pass: 'Narrowing corridor toward center, wider at edges', maze: 'Procedural maze using recursive backtracking - dense corridors', maze_rooms: 'Maze with carved-out rooms/chambers connected by corridors' };

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