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
stonefamily) 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
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: trueproduces a connected path between two anchor points that avoids water and steep slope, carved aspathfamily, legal under existing adjacency rules.heal_adjacencyhealed-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_biasknobs still hit their target coverage percentage under the new basin-fill lake logic. - No change to segment YAML schema — output is still coalesced
terrain_zonesAABBs, so 38's downstream consumers (sampling, adjacency, transitions) need zero changes.