Home Layered Terrain Generation

Layered Terrain Generation (Elevation-First Generate)

Replace the flat single-noise-draw core of admin Generate with a layered pipeline — elevation → climate → biome → base fill → carved features (rivers/paths) → smoothing — so generated land reads as intentional (mountain ranges, valleys, rivers that flow downhill, paths that connect places) instead of statistically-plausible speckle.

Related: 38 — Biomes, adjacency & terrain transitions (catalogs/adjacency/heal this plan builds on top of, unchanged), 47 — Terrain forge v2 (tile texture painting — unrelated system, not touched here).


1. Problem

generate_terrain already has real machinery — domain-warped fBm, a box blur, percentile thresholds, adjacency healing — but every terrain family (grass, dirt, stone, wetland) is drawn from one flat noise field partitioned by weight (domain_family, line 1102). There is no real heightmap: default_elevation_for_family (line 1244) is binary — water/wetland = -0.5, everything else = 0.0. Consequences:

  • "Mountains" (the stone family) are isolated patches with no elevation gradient, no ridge structure, no foothills — they can appear directly beside grass with nothing between them.
  • Lakes/rivers are thresholded off an elevation-proxy channel that isn't the same field terrain families are drawn from, so water and land shape are statistically unrelated to each other.
  • Paths/roads are never generated — Generate only ever treats them as frozen pre-existing context. The "grass, then paths, then mountains" layered narrative can't emerge because there's no path layer at all.
  • Only the water-membership channel gets blurred; the family-selection field itself is warped but not smoothed, so family borders are noise-wobbly rather than clean gradients.

2. Locked principles

Decision Rule
Output format unchanged Still coalesced AABB terrain_zones (38 §10) — this plan changes the algorithm inside the source, not the segment schema
Continue-only, still honored New pipeline still never overwrites occupied/locked cells; existing seed-continuation logic (seed_distance_field) stays as the layer-0 boundary condition
Adjacency/heal unchanged heal_adjacency and the content files stay the final legality pass — layering reduces how often it needs to fire, it doesn't replace it
Elevation is real, not a proxy One authoritative elevation field per generation, used by both family classification and water/river carving — no more disconnected proxy channel
Order matters Elevation → moisture → biome → base fill → rivers/lakes → paths → smoothing → heal → coalesce. Each layer only reads layers above it, never below
Incremental delivery Each layer is an independently shippable change against the same starter-plains segment; no big-bang rewrite
No infinite/streaming worldgen Same non-goal as 38 §10.3 — this is still an authoring-time tool, not runtime chunk generation

3. Pipeline

flowchart TB Seed[Existing terrain\nseed_distance_field] --> Elev Elev[Layer 0\nElevation fBm + ridge] --> Moist[Layer 1\nMoisture fBm] Elev --> ElevSmooth[Elevation blur\nN passes] ElevSmooth --> Biome[Layer 2\nBiome lookup\nelevation x moisture] Moist --> Biome Biome --> BiomeSmooth[Layer 2b\nMajority-filter smoothing] BiomeSmooth --> Fill[Layer 3\nBase family/kind fill\nelevation-banded] ElevSmooth --> Rivers[Layer 4a\nRivers: steepest descent] ElevSmooth --> Lakes[Layer 4a\nLakes: basin fill] Fill --> Paths[Layer 4b\nPaths: A* between\nroads/POIs] Rivers --> Heal[Layer 5\nheal_adjacency\nunchanged] Lakes --> Heal Paths --> Heal Fill --> Heal Heal --> Coalesce[Layer 6\ncover_cells_with_rects\nunchanged]

4. New/changed knobs

Extends TerrainGenerateApiRequest and in-game tools:

Knob Default Purpose
ridge_weight 0.35 Blend of ridge-noise vs. plain fBm in elevation (0 = rolling hills, 1 = sharp ranges)
elevation_smooth_passes 2 Box-blur passes over the elevation field
biome_smooth_passes 1 Majority-filter passes over the biome grid
biome_smooth_window 3 N×N window for the majority filter
generate_paths false Enable Layer 4b path carving between anchors
path_cost_slope_penalty 2.0 How strongly steep elevation discourages A* path routing

Existing knobs (seed, family_weights, water_amount, lake_bias, edge_blend_m, blend_overwrite_m, biome_id, llm_refine) are kept; family_weights shifts meaning slightly — it now biases within-biome kind selection (Layer 3) rather than the global family draw, since biome assignment itself comes from the elevation × moisture table.

No server-settings.yaml changes needed — these are per-request Generate knobs, same pattern as today's ( LlmSettings stays untouched).


6. Exit criteria

  • Elevation field is real and continuous (no binary water/land split); visualized/dumped elevation shows smooth gradients, not noise speckle.
  • Stone/hill terrain only appears at higher elevation bands and clusters into ranges (ridge structure visible), never isolated single cells in flat low-elevation grassland.
  • Rivers flow from higher to lower elevation and terminate in a lake, existing water, or the selection edge — never crossing uphill.
  • Biome grid shows no single-cell "salt and pepper" speckle after the majority-filter pass.
  • generate_paths: true produces a connected path between two anchor points that avoids water and steep slope, carved as path family, legal under existing adjacency rules.
  • heal_adjacency healed-cell count on a standard test selection is lower after layering than before (fewer illegal seams reaching the final repair pass).
  • Existing water_amount / lake_bias knobs still hit their target coverage percentage under the new basin-fill lake logic.
  • No change to segment YAML schema — output is still coalesced terrain_zones AABBs, so 38's downstream consumers (sampling, adjacency, transitions) need zero changes.