Tools/Scripts/README.md: the falloff dials and what they actually do -- including that IslandAxisX is a weak lever because the island is already Trench-clamped at ~90% of the map width, so nobody re-derives that the hard way. The spine crest fix, and an explicit note that the spine's AXIS is still a straight line down the map centre: known, deferred, its own task. BLUEPRINT_FORMAT.md config-knobs section: the three new dials, each labelled with whether it moves biomes. CoastProfile does NOT (it cannot move the waterline, so HGTS changes while BIOM/WBID/WBTB/WSRF stay bit-identical); IslandAxis* and OffshoreIslandDensity DO, by design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
8.4 KiB
Markdown
120 lines
8.4 KiB
Markdown
# /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.
|
||
|
||
**No rivers, no erosion, no flow routing anywhere in this pipeline.** A 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.
|
||
Erosion is Phase C work: a hydraulic pass over final terrain, after the shape stops moving.
|
||
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 Ramer–Douglas–Peucker (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.
|