islaApocalypse/Tools/Scripts
beezm 566abe9784 feat: submarine coast shelf + elongation dials (terrain-water task 11)
COAST. The task's premise -- "the island edge is the steepest part" -- is
right, and the diagnostic locates it precisely: it is the SEABED, not the
land. HeightCurve.Apply returns early at and below sea level, so tasks
05-09 reshaped the land and never touched the water. Measured on the same
seed, distance-from-shore vs height:

                       land reaches 10 m   seabed reaches 10 m   gradient
  curve OFF                   31 px               31 px       0.254/0.258
  curve ON (ships)           186 px               31 px       0.038/0.258

So the landward side is ALREADY the broad shallow shelf terrain_design.md
asks for -- 54% of the island sits under 14 m -- and flattening it further
would only make it mushy. What is left is a shoreline that shelves gently
on land and then drops 6.7x steeper the moment it goes under.

CoastShelf fixes that side: depth' = depth * (1 - 0.775*exp(-depth/100 m)).
The seabed leaves the waterline at 22.5% of its former gradient and
recovers smoothly, so deep water and the Trench keep their shape. Measured
after: 5 m depth at 43 px (was 15), 10 m at 75 (was 31), 30 m at 150 (was
94) -- a 2.4-2.9x wider shallows.

It is strictly positive for positive depth, so it CANNOT move the waterline
by one pixel. Biomes and water bodies come out bit-identical, which is why
the coast is separable from elongation in the batch rather than tangled
with it -- contrary to the task's expectation that all coast work moves
biomes.

ELONGATION. IslandAxisX/Y replace the 1.15/0.90 literals (the spine shares
AxisX, as it always shared the literal). Sized by replaying candidate
falloffs against real terrain rather than guessing -- the replay reproduces
the shipped extents exactly, bbox and land count.

That replay says the task's suggested 1.30/0.85 buys +2.0% aspect, because
THE ISLAND IS ALREADY TRENCH-CLAMPED IN X: it spans 90.5% of the map width,
and 1.15 -> 1.40 moves the west coast only 393 -> ~360 px. Aspect responds
almost entirely to AxisY, which costs land:

  1.30/0.85  +2.0% aspect  +2.9% land      1.30/0.80  +4.9%  -2.2%
  1.30/0.82  +3.7% aspect  -0.1% land      1.30/0.75 +11.5%  -7.3%

Default 1.30/0.78 -- a visible step (+~8% aspect, measured 1.137 -> 1.200)
at ~4% land. The developer's eye tunes it from the batch; the table above
is the price list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 23:29:49 -04:00
..
HeightCurve.cs feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10) 2026-08-08 20:31:41 -04:00
HeightCurve.cs.uid feat: curve v2 — per-seed spike normalization, u4 wall, 420 m ceiling (terrain-water task 06) 2026-08-08 00:21:25 -04:00
IslandFalloff.cs feat: submarine coast shelf + elongation dials (terrain-water task 11) 2026-08-08 23:29:49 -04:00
MapGenerator.cs feat: submarine coast shelf + elongation dials (terrain-water task 11) 2026-08-08 23:29:49 -04:00
MapGenerator.cs.uid baseline: working salvaged prototype on Godot 4.7.1 (pre-F1) 2026-08-05 01:45:41 -04:00
MapGenerator.md docs: bring all READMEs current with actual code (post-sweep-10 truth pass) 2026-08-05 05:23:58 -04:00
README.md docs: TDTL v2 body, shelf-edge variation, erosion deferred (terrain-water task 10) 2026-08-08 20:55:39 -04:00
RoundTripHarness.cs feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10) 2026-08-08 20:31:41 -04:00
RoundTripHarness.cs.uid chore: Godot .uid files for the new task-02 scripts 2026-08-07 00:17:46 -04:00
TerrainDetailPass.cs fix: detail must yield to the crater carve (terrain-water task 10) 2026-08-08 21:23:28 -04:00
TerrainDetailPass.cs.uid chore: Godot .uid for TerrainDetailPass (terrain-water task 10) 2026-08-08 20:01:42 -04:00

/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. 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 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.