islaApocalypse/Tools/Scripts/README.md
beezm 78e042b805 docs: crater erosion modes and the retired 1.2x exclusion (terrain-water task 19)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 05:50:22 -04:00

154 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# /Tools/Scripts — generation logic
C# backend for the offline developer tools. Decoupled from the live server and client.
## `MapGenerator.cs`
Generates the entire 2D blueprint. Roughly in order:
1. **Config** — reads `ServerConfig.json`, which sets `MapSize` (8192 by default) and the crater
radius. The `MapSize = 4096` field initialiser is a fallback that is immediately overwritten.
2. **Topography** — FastNoiseLite base height plus a mountain spine, minus a squircle distance
falloff, giving a guaranteed island. Noise frequency is divided by `scaleFactor`
(`MapSize / 1024f`) so terrain features stay the same real-world size at any map profile.
The island's proportions are the `IslandAxisX`/`IslandAxisY` dials (`1.30`/`0.78`; the
pre-task-11 shape was `1.15`/`0.90`). **`IslandAxisX` is a weak lever** — the island is
already Trench-clamped at ~90 % of the map width, so aspect responds almost entirely to
`IslandAxisY`, which trades against land area. These move the coastline, so they move biomes.
The mountain spine shares `IslandAxisX` for its width, and its crest is rounded
(`IslandFalloff.SmoothAbs`) — `1 - |x - centre|` used to peak with a slope discontinuity that
was, measured, the largest slope step on the map outside the Trench walls. **The spine's AXIS
is still a straight line down the map centre; that is known, deferred, and its own task.**
With `CoastProfile: "wide"` (the default) the *seabed* leaving the shoreline is shelved
(`IslandFalloff.CoastShelf`): the height curve is identity at and below sea level, so it never
reached the water, and the shoreline used to shelve gently on land then drop 6.7× steeper the
moment it went under. The shelf cannot move the waterline — it is strictly positive for
positive depth — so biomes and water are bit-identical with it on or off. `OffshoreIslandDensity`
(`0.02`, `0` disables) seeds sparse discoverable islets in the open ocean; they are held off the
mainland by a depth moat and out of the Trench by a distance mask, both by construction.
When `TerrainCurve: "v5"` (the default — the task-09 gate's BALANCED winner), the calibrated
height-redistribution curve
(`HeightCurve.cs` — the terraced ascent with spatially modulated shelves: flat farmable
lowlands, a walkable foothill bench at 100±12 m, the white mountain-town plateau at
220±20 m — shelf heights and strength drift across the island so no two flanks wear the same
ring — and a per-seed-normalized summit spike to the 420 m cap) reshapes above-sea terrain
after noise/falloff/Trench and before the crater carve; biome classification reads a
retained uncurved map, so biomes are identical either way. With `TerrainDetail: "v1"` (the
default) two detail passes ride along with the curve (`TerrainDetailPass.cs`): shelf
micro-relief (`ShelfReliefAmp`, ±3 m rolling skin on the benches/plateaus) and shelf-edge
variation (`ShelfEdgeVariation`, 12 m) — a per-column shift of the shelf/riser knot block that
makes the shelf edge scallop into notches, coves and peninsulas instead of tracing a clean
height contour. Both touch exported heights only, and neither can reach below the red ceiling
or above the 420 m cap: the curve's K2 and K6 knots do not move, so a warped column is
bit-identical to an unwarped one outside the shelf/riser stack. Drops the `0_height` hillshade
snapshot (hypsometric bands × NW hillshade) in both modes.
**Erosion (tasks 1718, Phase C0)** — with `Erosion: "v1"` (default **"off"**, opt-in until the
developer's gate approves it) a droplet-based hydraulic erosion pass (`HydraulicErosion.cs`,
standalone numeric, deterministic from the resolved seed) details the curved+detailed render
map after the detail passes and before the crater carve: droplets walk downhill with inertia,
eroding steep fast stretches and depositing where the ground flattens — both spread over the
same cone brush, so neither carving nor dumping can spike a single cell. **Four** hard
governors bound it (`ErosionDropletCount`/`ErosionDropletLifetime`/`ErosionCarveCap`/
`ErosionDepositCap`; both caps are per-cell metres against one net-displacement ledger and are
asserted on exit), a sea clamp forbids carving below sea + margin and leaves below-sea cells
untouched in both directions (the rendered coastline cannot move — asserted every generation),
and the crater is handled by `CraterErosionMode` (task 19): nothing inside the protected strike
core (`CraterErosionCore`, **0.80 ×** CraterRadius — the carve's own extent) is modified, and
outside it either `"full"` applies full strength at once or `"feather"` ramps 0→full out to
`CraterErosionFeather` (1.05 ×). Task 17's hard 1.2 × cutoff was replaced because it held
**620 811 land cells** of ordinary terrain smooth for no geometric reason — measured, the carve's
displacement is exactly 0 beyond 0.80 × — which read as an un-eroded disc with a hard edge.
Keeping the core at the carve radius is what makes erosion and the carve touch **disjoint**
cells; a narrower core lets the carve, which runs afterwards and scales height toward the sea
target, amplify erosion's deltas across the waterline (measured at a 0.50 core: 79 rendered
waterline crossings). The flooded bay and its sea connection never depend on the crater mode —
below-sea cells are read-only in both directions, so the bay can be neither carved open nor
silted shut. Output-height only: biome/water
classification reads the retained pre-erosion map, so `1_biomes`/`0_water` are bit-identical
with erosion on or off.
The task-18 defaults tune for a drainage **hierarchy** — fine rills everywhere feeding a set of
clearly deeper convergent channels — by letting droplets live long enough (384 steps at inertia
0.35, low evaporation) that their paths overlap and deepen shared low lines, and by raising the
carve cap to 15 m so trunks separate from rills instead of both piling against one ceiling.
Measured on seed 1280587109: ~1.9 M cells carved past 0.5 m, 42 k past 5 m, 1.4 k past 10 m,
and 177 connected channel systems of 200+ cells at the 3 m threshold. Erosion concentrates
~9× on the curve's shelf risers, because that is where sustained slope exists; the flat
shelves and lowlands are barely touched, so **the lowland continuation of a trunk is not
erosion's to cut** — that is river promotion's job. **No rivers yet** — the carved channels are
the designated future river routes (Phase C0b promotes them by flow accumulation).
The predecessor D8 drainage-incision pass was written and reverted in task 10 — per-cell
steepest descent on a regular grid can only route along eight headings, and at map scale that
reads as straight hatching, not drainage; the droplet model is the working replacement (its
carve field measures isotropic to within 1.5 % across the folded 45° grid period).
3. **Sea level and water** — sea level per the configured model (`SeaLevelModel`: `"flat"` scalar
— the default, `SeaLevelValue` 0.15 — or the legacy `"field"` latitude Lerp); flood fill
separates true ocean from inland lakes; a mainland fill guarantees one contiguous landmass.
4. **The crater** — placed along the northern coast and carved to below sea level, but only out to
**80 % of its radius**, which guarantees a landbridge rather than severing the island.
5. **Water bodies** — promotes the classified water into explicit data: the ocean as body 1, each
lake connected-component labeled (same 4-connectivity as the true-ocean fill), one transitional
surface level per body. Classification comes from the shared `IsOceanPixel`/`IsLakePixel`
predicates that the biome stage also uses, so the two can never disagree. A priority-flood
pit-fill runs here as validated diagnostics (basin statistics to console; serializes nothing).
Drops the `0_water` snapshot.
6. **Biomes and towns** — biome zoning by height and temperature; tiered town placement (Capitol,
Hubs, Villages, Outposts, POIs) filtered by slope, water proximity and spacing.
7. **Roads** — see below. Skipped entirely when the `SkipRoads` config toggle is on (the ~25-min
A\* pass is the bottleneck; a skip run exports a road-less iteration blueprint in ~80 s with a
loud console banner).
8. **Export** — writes the `.dat` blueprint (dual-write: the v2 tagged container under the primary
seed name, plus the legacy v1 format beside it as `_v1.dat` — see
`Core/Scripts/BLUEPRINT_FORMAT.md`), then renders the PNG snapshot. The v2 file embeds the
resolved generation params (seed, MapSize, crater radius, density, impact centre, provenance).
### The road network
`AStarGrid2D` over the whole map at one node per pixel. Mountains are made expensive rather than
impassable (`1 + elevation³ × 400`), water and the crater are marked solid.
- **Continental loop** — highway nodes sorted by radial angle and connected in a ring.
- **Mountain branch** — a spur from the loop to the snow hub.
- **County roads** — Prim's algorithm daisy-chains remaining towns onto the network. Villages become
Rugged roads, everything else Trails.
- **Abandon protocol** — a town further than 8 % of the map from the network is skipped rather than
pathed to, so one unreachable outpost cannot hang generation.
- **Smoothing** — every path is decimated with RamerDouglasPeucker (tolerance 4.0) then smoothed
with 4 Chaikin passes.
**This is the project's dominant performance problem.** At 8K the grid is 67 million nodes, and the
weight-setup pass touches every one before the first path is requested. The `await` yields between
stages cannot interrupt a single engine-side `GetPointPath` call, so a long path still blocks. Full
regenerations frequently get abandoned.
### The PNG snapshot
`SaveMapSnapshot()` builds an offscreen `SubViewport` in code rather than relying on the scene's
layout, because Godot's UI layout engine will otherwise crush a 4096+ render down to the editor
window size. A `MapDrawProxy` control re-issues the `_Draw()` calls into that viewport — `GetImage()`
captures only the base texture and would otherwise miss the roads and towns entirely — and the code
awaits two `RenderingServer.FramePostDraw` signals so the GPU has actually painted before the image
is read back.
Road colours on the snapshot, useful for identifying a road: **red** = Highway, **black** = Branch,
**dark brown** = Rugged, **light brown** = Trail.
### Outputs
`MapData_Seed_<seed>.dat` (v2), `MapData_Seed_<seed>_v1.dat` (legacy dual-write), and the staged
snapshots `Map_Seed_<seed>_{0_height,0_water,1_biomes,2_towns,3_roads}.png`, all to `user://`.
(`0_height` is the hypsometric hillshade of the curved terrain; `0_water` is painted from the
water stage's own outputs — ocean deep blue, lakes lighter blue, land neutral; `3_roads` is
absent on a SkipRoads run.)
## `RoundTripHarness.cs`
The blueprint format regression test (scene: `Tools/Scenes/RoundTripHarness.tscn`). Loads a
known-good blueprint through the real parser, re-writes it as v2 through the real writer, re-loads
it, and asserts semantic equality (heights bitwise, biomes, towns, every road point). Headless,
seconds per cycle, no generation. Exit code 0 = pass.
## Rules
1. **No magic numbers.** Distances, radii and thresholds derive from `MapSize` or `scaleFactor`, so
the generator behaves identically at 4K and 10K.
2. **Respect the GPU.** Anything exporting visual data must `await` the appropriate frame signals
before reading pixels back.