import type { BiomeBand, EnvironmentWorldFeature, RoadEnvironmentDescriptor, TerrainDetailConfig, TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, TerrainIslandDescriptor, TerrainMaterial, TerrainMaterialMaps, WorldBounds, WorldFeature } from "./features.js"; import type { AvoidZone } from "./geometry.js"; import { type TerrainPathProfile } from "./pathTerrain.js"; import type { TerraformSnapshot } from "./terraform.js"; /** A surface normal vector at a terrain sample point. */ export type TerrainNormal = readonly [number, number, number]; /** A sampleable ground surface: height and normal at any x/z, with optional bounds and water level. */ export interface TerrainField { sampleHeight(x: number, z: number): number; sampleNormal(x: number, z: number): TerrainNormal; readonly bounds?: WorldBounds; readonly waterLevel?: number; } /** Smoothly interpolated 2D value noise in `[-1, 1]` for the given seed. * @internal */ export declare function valueNoise(x: number, z: number, seed: number): number; /** Octave settings for {@link fractalNoise}: frequency, layering, and optional ridged shaping. */ export interface FractalNoiseConfig { seed: number; frequency: number; octaves: number; lacunarity: number; persistence: number; ridged: boolean; } /** Layers `valueNoise` octaves per `config` into a single normalized noise sample. * @internal */ export declare function fractalNoise(x: number, z: number, config: FractalNoiseConfig): number; /** @internal */ export declare function seedFrom(value: string | number | undefined, fallback: number): number; /** Derives a `TerrainField.sampleNormal` from a height sampler via finite-difference gradients. * @internal */ export declare function withNormal(sampleHeight: (x: number, z: number) => number): TerrainField["sampleNormal"]; export declare const FLAT_FIELD: TerrainField; /** A flat, zero-height `TerrainField` for arenas with no elevation. * @internal */ export declare function flatField(): TerrainField; /** Configuration for {@link noiseField}: seed, amplitude, and fractal noise shaping. */ export interface NoiseFieldConfig { seed?: string | number; amplitude?: number; frequency?: number; octaves?: number; lacunarity?: number; persistence?: number; baseHeight?: number; ridged?: boolean; bounds?: WorldBounds; waterLevel?: number; } /** Builds a `TerrainField` whose height is fractal noise shaped by `config`. * @internal */ export declare function noiseField(config?: NoiseFieldConfig): TerrainField; export interface RollingFieldConfig { seed?: string | number; amplitude?: number; frequency?: number; bounds?: WorldBounds; } /** @internal */ export declare function rollingField(config?: RollingFieldConfig): TerrainField; export interface ArenaFieldConfig { seed?: string | number; bounds?: WorldBounds; } export declare const ARENA_WATER_LEVEL = -0.9; /** Builds a `TerrainField` with a flat spawn plateau, rolling hills, and a basin, for combat arenas. * @internal */ export declare function arenaField(config?: ArenaFieldConfig): TerrainField; /** @internal */ export declare function groundFieldFor(world?: WorldFeature): TerrainField; /** Whether a world declares real terrain (base heightfield or islands) rather than a flat plane — gates terrain-floor sampling in the movement controllers. * @internal */ export declare function hasEnvironmentTerrain(world: WorldFeature | undefined): boolean; /** Ground height between islands when no base terrain exists — deep enough to read as a fall into the void, finite so physics stays sane. */ export declare const ISLAND_VOID_HEIGHT = -256; /** * Composes a base terrain and any number of bounded islands into one world field: inside an island's * rect the island's own field (sampled in island-local coordinates) wins, elsewhere the base terrain * answers, and with no base the gap is `ISLAND_VOID_HEIGHT` void. Later islands win overlaps. * @internal */ export declare function composeIslandFields(base: TerrainField | null, islands: readonly TerrainIslandDescriptor[], voidHeight?: number): TerrainField; /** * Layers an authored sculpt snapshot's offsets over a base field — the editor-to-runtime ground seam. * @internal — reached through `environment({ sculpt })`; not called directly by games. */ export declare function sculptedField(base: TerrainField, snapshot: TerraformSnapshot): TerrainField; /** * Levels a base field toward each clearance zone's center height within its radius (feathered) — so * spawns, plots, and paths sit on flat ground even when a mound is nearby. The strongest overlapping * zone wins. Pairs with scatter's avoid: one clearance zone both flattens ground and repels foliage. * @internal — reached through `environment({ clearings })`; not called directly by games. */ export declare function flattenFieldAround(base: TerrainField, zones: readonly AvoidZone[]): TerrainField; /** * The full ground field for an environment world: base `terrain` composed with any `islands`, then * any authored `sculpt` snapshot, then any `clearings` flattened on top — so an editor-sculpted * heightfield drives both the rendered mesh and player collision, and gameplay spots stay level, * through the one seam every consumer already reads. * @internal */ /** * Drapes each `road()` ribbon's `elevation` onto the ground so a body standing on a road snaps to the * road *surface* the renderer draws (`sampleHeight + elevation`), not the bare terrain beneath it. Without * this the ground field and {@link RoadRibbons}/{@link CityRenderer} disagree by `elevation` per road, and * anything grounded on a road — the player, NPCs, spawns via `groundHeightAt` — sinks by that offset into * the asphalt. Overlapping ribbons (intersections) take the highest lift. Only `sampleHeight` is draped: * the road ribbons are flat, so `sampleNormal` stays the smooth terrain normal — draping it too would spoof * a near-vertical slope at every curb edge and trip slope-slide. * @internal — reached through `environment({ roads })`; not called directly by games. */ export declare function roadDrapedField(base: TerrainField, roads: readonly RoadEnvironmentDescriptor[]): TerrainField; /** * The full ground field for an environment world: base `terrain` composed with any `islands`, then * any authored `sculpt` snapshot, then any `clearings`, then any `roads` draped on top — so an * editor-sculpted heightfield drives both the rendered mesh and player collision, gameplay spots * stay level, and anything grounded on a road stands on the drawn asphalt rather than the bare * terrain beneath it, through the one seam every consumer already reads. * @internal */ export declare function resolveEnvironmentField(feature: EnvironmentWorldFeature): TerrainField; export interface TerrainSlopeSample { /** Unit XZ direction pointing downhill; `[0, 0]` on level ground. */ downhill: readonly [number, number]; /** Rise over run (tan of the slope angle); `0` on level ground. */ steepness: number; } /** Local slope from the field's normal — the input for gravity-roll, ski acceleration, and slide checks (#284.6). * @internal */ export declare function sampleSlope(field: TerrainField, x: number, z: number): TerrainSlopeSample; /** * Downhill force/acceleration from the local slope: direction × sin(slope angle) × `scale`. * Add it to any integrator's XZ velocity each tick (`scale` ≈ gravity for a free-rolling body). * @internal */ export declare function slopeForce(field: TerrainField, x: number, z: number, scale?: number): readonly [number, number]; /** XZ region a {@link raycastHeightField} march is clipped to. */ export interface HeightFieldRayBounds { minX: number; minZ: number; maxX: number; maxZ: number; } /** Tuning for {@link raycastHeightField}: march resolution and refinement depth. */ export interface HeightFieldRaycastOptions { /** XZ region to march within; the ray is clipped to it before any sampling. */ bounds: HeightFieldRayBounds; /** * March step in world units. Use about half the field's cell size — features narrower than one * step can be skipped. Defaults to 1/256 of the larger bounds span, clamped to `[0.25, 4]`. */ step?: number; /** Bisection iterations refining the crossing once found (default 8 ≈ step/256 precision). */ refine?: number; /** Stop marching past this ray distance (default: the full bounds crossing). */ maxDistance?: number; /** * Hard cap on march samples (default 4096). Bounds the work for rays the XZ slab cannot clip — * a near-vertical ray inside bounds has an enormous slab crossing, and an upward ray never * meets the surface at all; the cap turns both into a prompt miss instead of a stall. */ maxSteps?: number; } /** Where a {@link raycastHeightField} march crossed the surface. */ export interface HeightFieldRayHit { x: number; y: number; z: number; /** Ray-parameter distance from the origin to the hit (direction is normalized internally). */ distance: number; } /** * Intersects a ray with a `sampleHeight` field by fixed-step raymarching plus bisection — the * O(steps) editor/gameplay picking seam that replaces brute-force triangle raycasts against a * tessellated ground mesh (tens of thousands of triangle tests per pick on a sculpt-sized grid). * The ray is clipped to `bounds` on XZ first, marched at `step`, and the first above→below * crossing is bisected. An origin already under the surface hits immediately at entry. Returns * null when the clipped ray never crosses the surface. * @capability terrain-raycast pick a point on a heightfield along a camera/pointer ray */ export declare function raycastHeightField(sampleHeight: (x: number, z: number) => number, origin: readonly [number, number, number], direction: readonly [number, number, number], options: HeightFieldRaycastOptions): HeightFieldRayHit | null; export interface TerrainPalette { low: string; high: string; waterline: string; } export declare const DEFAULT_TERRAIN_MATERIAL: TerrainMaterial; export declare const TERRAIN_MATERIAL_PALETTES: Record; /** A resolved {@link TerrainDetailMaterialConfig} — `repeat`/`strength` filled with defaults, `maps` passed through. */ export interface ResolvedTerrainDetailMaterial { maps: TerrainMaterialMaps; repeat: number; strength: number; tint: number; missing: "procedural" | "fail"; } /** A {@link TerrainDetailConfig} with every field resolved to a concrete value — the shape the shell's detail material consumes. */ export type ResolvedTerrainDetail = Required> & { waterLevel: number; material?: ResolvedTerrainDetailMaterial; sweeps: { dry: readonly [number, number, number]; wet: readonly [number, number, number]; }; }; /** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level, * and a terrain without one gets no sand band. `undefined` resolves the implicit default detail. * @internal */ export declare function resolveTerrainDetail(config: TerrainDetailConfig | undefined, terrainWaterLevel?: number): ResolvedTerrainDetail; /** @internal */ export declare function resolveTerrainPalette(descriptor?: Pick): TerrainPalette; /** * Per-z palette sampler over a descriptor's `biomeBands` — ordered zones that cross-fade their * ground palette across a `fade`-wide window centered on the midpoint z between adjacent centers. * Below the first / above the last band clamps to that band's palette. Returns `fallback` when no * bands are declared. Pure math, unit-testable independent of rendering. * @internal */ export declare function createBiomeBandSampler(bands: readonly BiomeBand[], fallback: TerrainPalette): (z: number) => TerrainPalette; /** A concrete fog look — the shape `createBiomeFogSampler` resolves per z from a band's `fog` over the base fog. */ export interface BiomeFogValue { color: string; near: number; far: number; density: number; } /** * Per-z fog sampler over a descriptor's `biomeBands` — cross-fades each band's `fog` (unset fields * falling through to `fallback`) across the same `fade` window as the ground sampler, so fog color and * range track the camera's z. Bands with no `fog` resolve to `fallback`. Pure math, unit-testable. * @internal */ export declare function createBiomeFogSampler(bands: readonly BiomeBand[], fallback: BiomeFogValue): (z: number) => BiomeFogValue; /** A concrete sky look — the shape `createBiomeSkySampler` resolves per z from a band's `sky` over the base sky. */ export interface BiomeSkyValue { horizonColor: string; zenithColor: string; sunIntensity: number; ambientIntensity: number; } /** * Per-z sky sampler over a descriptor's `biomeBands` — cross-fades each band's `sky` (unset fields * falling through to `fallback`) across the same `fade` window as the ground sampler, so horizon/zenith * colors and sun/ambient intensity track the camera's z. Bands with no `sky` resolve to `fallback`. * @internal */ export declare function createBiomeSkySampler(bands: readonly BiomeBand[], fallback: BiomeSkyValue): (z: number) => BiomeSkyValue; /** * Per-position palette sampler over the descriptor's base `material`/`colors` plus its z-ordered * `biomeBands` (painted first) and radial `materialRegions` (painted over) — the multi-biome coloring * seam. Regions paint fully inside `radius` and blend back across `falloff`; later regions win overlaps. * @internal */ export declare function createTerrainPaletteSampler(descriptor: Pick): (x: number, z: number) => TerrainPalette; export interface HeightMapFieldConfig { columns: number; rows: number; samples: ArrayLike; bounds?: WorldBounds; heightScale?: number; baseHeight?: number; waterLevel?: number; } /** @internal */ export declare function heightMapField(config: HeightMapFieldConfig): TerrainField; /** Resolves a `TerrainEnvironmentDescriptor` into a concrete `TerrainField`, applying flatten masks. * @internal */ export declare function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField; /** * Composes authored path profiles onto a field — the shared seam that turns a scene road/river/ramp path * into flattened, graded, carved, or retained ground. Applies after `flatten` masks in `resolveTerrainField` * so a corridor grades over already-leveled pads; call it directly to layer profiles onto any field a game * builds by hand. Returns the field unchanged when no profile is given. * @capability path-terrain apply flatten/grade/carve/retaining path profiles to a terrain field */ export declare function applyPathProfiles(field: TerrainField, profiles: readonly TerrainPathProfile[]): TerrainField; /** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. * @internal */ export declare function snapToGround(field: TerrainField, position: readonly [number, number, number], offset?: number): [number, number, number]; export interface GroundSnapTarget { position: readonly [number, number, number]; } export interface GroundSnapEntityStore { get(id: string): GroundSnapTarget | null; setPose(id: string, pose: { position?: readonly [number, number, number]; }): boolean; } /** Ground-snaps an already-spawned entity in place; returns false when `id` is unknown. * @internal */ export declare function snapEntityToGround(entities: GroundSnapEntityStore, id: string, field: TerrainField, offset?: number): boolean; export declare const DEFAULT_MAX_WALK_SLOPE = 0.6; /** Zeroes out a movement step's x or z component where it would climb steeper than `maxSlope`. * @internal */ export declare function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope?: number): { stepX: number; stepZ: number; };