import { STRIDE, type Mesh } from "../scene/Mesh" /** A bounded ground surface. Implementations may be procedural, sampled, or * loaded; callers only depend on world-space height sampling. */ export type Terrain = { minX: number minZ: number maxX: number maxZ: number heightAt: (x: number, z: number) => number } /** Parameters for the built-in rolling terrain generator. Values describe the * surface only; level-specific holes and materials belong to level data. */ export type RollingTerrainConfig = { inner: number outer: number blend: number amplitude: number frequency: number peakHeight: number peakFrequency: number peakStart: number } export namespace Terrain { /** Built-in square world with a flat center, rolling hills, and edge ridges. */ export function rolling(config: RollingTerrainConfig): Terrain { return { minX: -config.outer, minZ: -config.outer, maxX: config.outer, maxZ: config.outer, heightAt(x, z) { const r = Math.max(Math.abs(x), Math.abs(z)) if (r <= config.inner) { return 0 } const rise = smoothstep(config.inner, config.inner + config.blend, r) const hills = config.amplitude * bumps(x, z, config.frequency) const k = Math.min( 1, (r - config.inner) / (config.outer - config.inner), ) const peaks = config.peakHeight * ridges(x, z, config.peakFrequency) * smoothstep(config.peakStart, 1, k) return rise * (hills + peaks) }, } } export function height(terrain: Terrain, x: number, z: number): number { return terrain.heightAt(x, z) } /** Append one sampled heightfield patch. `include` is level policy evaluated at * each quad center, allowing arbitrary holes without teaching terrain what * occupies them. */ export function patch( terrain: Terrain, mesh: Mesh, x0: number, z0: number, x1: number, z1: number, cols: number, rows: number, uvScale: number, include?: (x: number, z: number) => boolean, ): void { const base = mesh.verts.length / STRIDE const dx = (x1 - x0) / cols const dz = (z1 - z0) / rows const rowLength = cols + 1 for (let row = 0; row <= rows; row++) { const z = z0 + row * dz for (let col = 0; col <= cols; col++) { const x = x0 + col * dx mesh.verts.push(x, terrain.heightAt(x, z), z, x * uvScale, z * uvScale) } } for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const cx = x0 + (col + 0.5) * dx const cz = z0 + (row + 0.5) * dz if (include !== undefined && !include(cx, cz)) { continue } const p = base + row * rowLength + col mesh.indices.push( p, p + rowLength + 1, p + 1, p, p + rowLength, p + rowLength + 1, ) } } } function bumps(x: number, z: number, frequency: number): number { const a = Math.sin(x * frequency) * Math.cos(z * frequency) const b = Math.sin((x + z) * frequency * 0.5 + 1.7) * 0.5 return (a + b + 1.5) / 3 } function ridges(x: number, z: number, frequency: number): number { const n = Math.sin(x * frequency + 1.3) * Math.cos(z * frequency - 0.7) * 0.7 + Math.sin((x + z) * frequency * 0.6 + 2.5) * 0.3 return 1 - Math.abs(n) } function smoothstep(a: number, b: number, x: number): number { const t = Math.max(0, Math.min(1, (x - a) / (b - a || 1e-4))) return t * t * (3 - 2 * t) } }