diff --git a/Tools/README.md b/Tools/README.md
index aa47116..0e7bc4a 100644
--- a/Tools/README.md
+++ b/Tools/README.md
@@ -17,11 +17,76 @@ nothing that consumes one at play time.
> **Tools emit data; they never write to a live save.** A generator that could touch live server
> state is a corruption risk for no benefit.
-## Empty this phase — Phase 1 fills it
+## What is here
-The only thing here now is `Scenes/UserDirProbe.tscn` + `Scripts/UserDirProbe.cs`, a print-and-quit
-diagnostic that confirms this project's `user://` directory is its own and not the old prototype's.
-It generates nothing.
+### The generator — pass 1 (Phase 1)
+
+**Ported from the reference's `MapGenerator.GenerateTopography` pass-1 loop** at tag
+`pre-rewrite-reference` (`ab78883`). This is the crown-jewel port: **working code and its tuned
+constants, carried over verbatim — not re-derived from a design summary** (→ D-050,
+`Design - Rewrite - Extraction Manifest.md`).
+
+| File | What it is |
+|---|---|
+| `Scripts/TerrainNoise.cs` | ⭐ The FastNoiseLite config, **every fractal property pinned explicitly** |
+| `Scripts/Topography.cs` | ⭐⭐ Pass 1 — the six elements, in the reference's execution order |
+| `Scripts/IslandFalloff.cs` | `SmoothAbs` + `CREST_EPSILON` (pass-1 half of the reference file) |
+| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
+| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
+| `Scripts/HeightMapRenderer.cs` | Hypsometric PNG + raw `.f32` dump |
+| `Scripts/TerrainGenTool.cs` | Batch entry point (ladder + seed batch + `INDEX.md`) |
+| `Scenes/TerrainGenTool.tscn` | Run this |
+
+```bash
+Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
+ --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn
+```
+
+`ISLA_MAPSIZE` (default 2048) · `ISLA_SEEDS` (comma-separated, positive) · `ISLA_BATCH` ·
+`ISLA_SKIP_RAW=1` · `ISLA_LADDER=0`
+
+**The six ported elements, in order** — the order is load-bearing, not an implementation detail:
+
+1. **wobbled latitude scalar** — `y/MapSize` plus a ±0.1 low-frequency wobble
+2. **island falloff/mask** — squircle + ellipse, 50/50, then `Pow(·, 2.5f)`
+3. **edge roughness** — `noise(x·2.5, y·2.5)`, modulated by the squircle so it bites only at the rim
+4. **southern sinker** — bottom 25%, **before** the power (its effect is superlinear)
+5. **the Trench** — map-anchored, additive on both axes, **after** the power
+6. **mountain spine** — `SmoothAbs` crest, cubic, southern fade, amplitude `0.6f`
+
+> ### ⚠ The latitude scalar is NOT climate temperature.
+>
+> The reference called it `temperature` and let biomes read the same array — a large part of how
+> climate and terrain got fused. Here it is a **stage-1-local latitude field that terrain geometry
+> reads**, and nothing else. Climate is **stage 3** and classifies finished shape (D-049 §2, D-056).
+> Do not alias, store, or rename this into a climate map.
+
+**Deferred to Phase 2, with the seam already open:** the submarine **coast shelf** and the
+**offshore islets** (reference ~:621-664). Both act only below sea level and are judged once water
+renders. `Pass1Result.PreTrenchFalloff` is captured at the exact point they consume, and
+`Pass1Result.HMaxSeed` is carried for the seed-dependent redistribution curve.
+
+**Not here at all:** the curve, erosion, rivers, water bodies, the crater, biomes, roads, the mesher.
+
+### ⚠ The render writes a `Godot.Image` directly — not the capture path
+
+The reference captured maps through a SubViewport + a `TextureRect._Draw`, because it composited
+**vector overlays** (roads, town markers, river polylines) onto the raster. That machinery awaits
+render frames — which is exactly why unattended runs need `xvfb-run` and why `--headless` hangs on
+it.
+
+**Phase 1 has no overlays.** A heightmap is a raster, so it is written per-pixel into an `Image` and
+saved: no viewport, no frame awaits, no display. **Do not reintroduce the capture path by habit** —
+it becomes correct the moment something vector needs compositing, and not before.
+
+### Diagnostics
+
+- `Scenes/UserDirProbe.tscn` — confirms this project's `user://` is its own, not the old
+ prototype's. Exits non-zero on a clash.
+- `Scenes/NoiseDefaultsProbe.tscn` — prints this engine's `FastNoiseLite` constructor defaults and
+ checks them against the recorded baseline. **Run it after any Godot upgrade.** It cannot change
+ the terrain (`TerrainNoise` pins every value); it makes a drifting default *visible* instead of
+ silent.
---
diff --git a/Tools/Scenes/NoiseDefaultsProbe.tscn b/Tools/Scenes/NoiseDefaultsProbe.tscn
new file mode 100644
index 0000000..80e98da
--- /dev/null
+++ b/Tools/Scenes/NoiseDefaultsProbe.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bqp1x2v3isla1"]
+
+[ext_resource type="Script" path="res://Tools/Scripts/NoiseDefaultsProbe.cs" id="1_ndp"]
+
+[node name="NoiseDefaultsProbe" type="Node"]
+script = ExtResource("1_ndp")
diff --git a/Tools/Scenes/TerrainGenTool.tscn b/Tools/Scenes/TerrainGenTool.tscn
new file mode 100644
index 0000000..d4f0b27
--- /dev/null
+++ b/Tools/Scenes/TerrainGenTool.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bqp1x2v3isla2"]
+
+[ext_resource type="Script" path="res://Tools/Scripts/TerrainGenTool.cs" id="1_tgt"]
+
+[node name="TerrainGenTool" type="Node"]
+script = ExtResource("1_tgt")
diff --git a/Tools/Scripts/HeightMapRenderer.cs b/Tools/Scripts/HeightMapRenderer.cs
new file mode 100644
index 0000000..d7c3d63
--- /dev/null
+++ b/Tools/Scripts/HeightMapRenderer.cs
@@ -0,0 +1,148 @@
+using Godot;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// Renders a raw pass-1 height field as a HYPSOMETRIC height-gradient PNG.
+ ///
+ /// ═══ ⚠ WHY THIS WRITES A Godot.Image DIRECTLY, AND NOT VIA THE CAPTURE PATH ═══
+ ///
+ /// The reference captured its maps through a SubViewport + a TextureRect whose `_Draw` painted
+ /// overlays — roads, town markers, river polylines — and then read the viewport texture back.
+ /// That machinery exists to COMPOSITE VECTOR OVERLAYS onto a raster. It also carries a real
+ /// cost: it awaits render frames, which is why unattended runs need `xvfb-run` and why
+ /// `--headless` HANGS on it.
+ ///
+ /// Phase 1 has no overlays. A heightmap is a raster and nothing else, so it is written
+ /// per-pixel into an Image and saved. No viewport, no frame awaits, no display needed.
+ ///
+ /// ⚠ DO NOT REINTRODUCE THE CAPTURE PATH BY HABIT. It becomes correct the moment something
+ /// vector needs compositing on top (roads, towns, river centrelines) — and not before.
+ ///
+ /// ═══ THE COLOUR SCHEME ═══
+ ///
+ /// Two ramps meeting at the sea threshold, Hispaniola-style:
+ /// below — bathymetric: deep navy → shallow cyan-ish blue
+ /// above — hypsometric: shore green → lowland green → tan → brown → grey → white peak
+ ///
+ /// ⚠⚠ THERE ARE NO BIOME WORDS HERE AND THERE MUST NOT BE. Nothing in this file knows about
+ /// jungle, wasteland, snow or desert. It reads a single float per column and picks a colour on
+ /// an elevation ramp. Biomes are a stage-5 CLASSIFICATION of finished shape (D-049 §2, D-056),
+ /// and the whole point of the rewrite's seam is that terrain never learns their names.
+ /// "White at the top" is snow-COLOURED cartography, not a snow biome.
+ ///
+ /// ⚠ The sea threshold is a VISUALIZATION boundary only. No water is modelled: no water bodies,
+ /// no ocean/lake distinction, no flooding. Phase 2 owns all of that.
+ ///
+ public static class HeightMapRenderer
+ {
+ ///
+ /// FIXED ramp anchors in raw height units, deliberately NOT per-run normalized.
+ ///
+ /// Per-run normalization would make every PNG use its full colour range, which looks better
+ /// and lies: two seeds, or two rungs of the ablation ladder, would be coloured by different
+ /// scales and could not be compared by eye. Fixed anchors mean the same colour is the same
+ /// height in every image in a batch. Out-of-range values clamp, and the run prints the true
+ /// min/max so saturation is visible as a number rather than guessed from the picture.
+ ///
+ public const float RampTopHeight = 1.45f;
+
+ /// The deepest height the bathymetric ramp resolves. Below this, flat abyss colour.
+ public const float RampBottomHeight = -1.00f;
+
+ // ---- the land ramp: (t in [0,1] above sea) -> colour ----
+ private static readonly (float t, Color c)[] LandRamp =
+ {
+ (0.00f, new Color(0.36f, 0.55f, 0.35f)), // shore green
+ (0.12f, new Color(0.47f, 0.62f, 0.36f)), // lowland
+ (0.30f, new Color(0.72f, 0.71f, 0.44f)), // dry tan
+ (0.50f, new Color(0.72f, 0.57f, 0.36f)), // brown
+ (0.70f, new Color(0.60f, 0.45f, 0.34f)), // dark brown
+ (0.86f, new Color(0.66f, 0.64f, 0.64f)), // grey rock
+ (1.00f, new Color(0.98f, 0.98f, 0.98f)), // white peak
+ };
+
+ // ---- the sea ramp: (t in [0,1], 0 = deepest) -> colour ----
+ private static readonly (float t, Color c)[] SeaRamp =
+ {
+ (0.00f, new Color(0.03f, 0.06f, 0.18f)), // abyss
+ (0.45f, new Color(0.06f, 0.16f, 0.36f)), // deep
+ (0.75f, new Color(0.11f, 0.30f, 0.52f)), // mid
+ (0.92f, new Color(0.20f, 0.47f, 0.66f)), // shelf
+ (1.00f, new Color(0.42f, 0.68f, 0.79f)), // shallow, at the waterline
+ };
+
+ /// Render and save. Returns the absolute path written.
+ public static string SavePng(Pass1Result result, float seaLevel, string absolutePath)
+ {
+ int n = result.MapSize;
+ var img = Image.CreateEmpty(n, n, false, Image.Format.Rgb8);
+
+ for (int x = 0; x < n; x++)
+ {
+ for (int y = 0; y < n; y++)
+ {
+ float h = result.Height[x, y];
+ Color c;
+
+ if (h >= seaLevel)
+ {
+ float t = Mathf.Clamp((h - seaLevel) / (RampTopHeight - seaLevel), 0f, 1f);
+ c = Sample(LandRamp, t);
+ }
+ else
+ {
+ float t = Mathf.Clamp((h - RampBottomHeight) / (seaLevel - RampBottomHeight), 0f, 1f);
+ c = Sample(SeaRamp, t);
+ }
+
+ // Image is row-major (x = column, y = row); the height field is [x, y] with y
+ // increasing SOUTHWARD, matching the reference's convention. So a larger y is
+ // further down the image, and "the southern sinker" sinks the bottom of the PNG.
+ img.SetPixel(x, y, c);
+ }
+ }
+
+ Error err = img.SavePng(absolutePath);
+ if (err != Error.Ok)
+ GD.PrintErr($"[HeightMapRenderer] SavePng failed ({err}) for {absolutePath}");
+ return absolutePath;
+ }
+
+ private static Color Sample((float t, Color c)[] ramp, float t)
+ {
+ for (int i = 1; i < ramp.Length; i++)
+ {
+ if (t <= ramp[i].t)
+ {
+ float span = ramp[i].t - ramp[i - 1].t;
+ float local = span <= 0f ? 0f : (t - ramp[i - 1].t) / span;
+ return ramp[i - 1].c.Lerp(ramp[i].c, local);
+ }
+ }
+ return ramp[ramp.Length - 1].c;
+ }
+
+ ///
+ /// Dump the raw float height field beside the PNG, little-endian float32, x-major.
+ ///
+ /// Cheap, and it is what makes a future BYTE-LEVEL port-fidelity check possible — the kind
+ /// of automatic oracle that lets taste-iteration happen without correctness being judged by
+ /// eye. → `Design - Tooling - Iteration and Batching.md`, "build the oracle first".
+ ///
+ public static string SaveRaw(Pass1Result result, string absolutePath)
+ {
+ using var f = FileAccess.Open(absolutePath, FileAccess.ModeFlags.Write);
+ if (f == null)
+ {
+ GD.PrintErr($"[HeightMapRenderer] could not open {absolutePath}: {FileAccess.GetOpenError()}");
+ return absolutePath;
+ }
+ int n = result.MapSize;
+ for (int x = 0; x < n; x++)
+ for (int y = 0; y < n; y++)
+ f.StoreFloat(result.Height[x, y]);
+ return absolutePath;
+ }
+ }
+}
diff --git a/Tools/Scripts/HeightMapRenderer.cs.uid b/Tools/Scripts/HeightMapRenderer.cs.uid
new file mode 100644
index 0000000..088495a
--- /dev/null
+++ b/Tools/Scripts/HeightMapRenderer.cs.uid
@@ -0,0 +1 @@
+uid://tufk8dg4g14g
diff --git a/Tools/Scripts/IslandFalloff.cs b/Tools/Scripts/IslandFalloff.cs
new file mode 100644
index 0000000..6afc47c
--- /dev/null
+++ b/Tools/Scripts/IslandFalloff.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// Shape helpers for the island mask, ported from the reference's
+ /// Tools/Scripts/IslandFalloff.cs.
+ ///
+ /// ⚠ ONLY THE PASS-1 PARTS ARE HERE. The reference file also carries the submarine COAST SHELF
+ /// (SHELF_STRENGTH / SHELF_SCALE_M / CoastShelf) and the OFFSHORE ISLET layer (OffshoreBlob,
+ /// OffshoreZoneWeight, CalibrateThreshold, and their constants). Both are DEFERRED to Phase 2 —
+ /// they act on below-sea height and are judged once water renders. They will port into THIS
+ /// file, which is why it keeps the reference's name and shape.
+ ///
+ /// This type is pure math and engine-free. It sits in Tools/ rather than Core/ so the pass-1
+ /// port stays auditable as one unit against one reference file, and so the deferred Phase-2
+ /// halves land beside their siblings rather than in a second location.
+ ///
+ public static class IslandFalloff
+ {
+ ///
+ /// The rounding width of the spine crest, in normalized axis-distance units.
+ /// READ from the reference: IslandFalloff.CREST_EPSILON = 0.03f (~:34).
+ ///
+ public const float CREST_EPSILON = 0.03f;
+
+ ///
+ /// A smooth absolute value: zero AT zero, with zero slope there, converging to |d| away from it.
+ /// READ from the reference (~:42-46), verbatim:
+ /// a = |d|; return a*a / sqrt(a*a + eps*eps);
+ ///
+ /// ═══ WHY IT EXISTS — do not "simplify" it back to Abs ═══
+ ///
+ /// The spine's ridge axis is the line x = centre, and a plain 1 - |x - cx| peaks there
+ /// with a SLOPE DISCONTINUITY. On real terrain that put the four columns at the centre axis
+ /// in the top four slope-step locations out of 5,999 — a visible crease running the whole
+ /// height of the island, measured at ~170x the off-axis controls. It was blamed on the
+ /// falloff blend for a long time; transect measurement ruled that out and found this.
+ ///
+ /// SmoothAbs(0) = 0, so the crest keeps its FULL HEIGHT — it rounds without dropping.
+ /// Measured on the reference: the 1-px kink fell 99.3%, the centre column dropped from
+ /// rank 1 of 5,999 to rank 1,364, and nothing anywhere was lowered.
+ /// → `Design - Terrain - Mountain Spine.md`.
+ ///
+ public static float SmoothAbs(float d, float epsilon)
+ {
+ float a = Math.Abs(d);
+ return a * a / MathF.Sqrt(a * a + epsilon * epsilon);
+ }
+ }
+}
diff --git a/Tools/Scripts/IslandFalloff.cs.uid b/Tools/Scripts/IslandFalloff.cs.uid
new file mode 100644
index 0000000..2d02a5b
--- /dev/null
+++ b/Tools/Scripts/IslandFalloff.cs.uid
@@ -0,0 +1 @@
+uid://k4rjv4ck6l6j
diff --git a/Tools/Scripts/NoiseDefaultsProbe.cs b/Tools/Scripts/NoiseDefaultsProbe.cs
new file mode 100644
index 0000000..0d126d0
--- /dev/null
+++ b/Tools/Scripts/NoiseDefaultsProbe.cs
@@ -0,0 +1,86 @@
+using Godot;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// A print-and-quit diagnostic: reports this engine build's
+ /// CONSTRUCTOR DEFAULTS, and checks them against the values the ported terrain expects.
+ ///
+ /// ═══ WHY THIS EXISTS ═══
+ ///
+ /// The reference prototype set only three noise properties — NoiseType, Seed, Frequency — and
+ /// left fractal type, octaves, gain, lacunarity and weighted-strength untouched. So the island's
+ /// entire fractal character was Godot 4.7.1's CONSTRUCTOR DEFAULTS, not code. Nothing in that
+ /// repo recorded what they were. (chat1/00 §4.1.)
+ ///
+ /// now pins every one of them explicitly, so the terrain no longer
+ /// depends on an engine default. This probe is the other half of that guarantee: it makes a
+ /// silent default change VISIBLE on an engine upgrade instead of quietly reshaping the island.
+ ///
+ /// Run it after any Godot version bump:
+ ///
+ /// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
+ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/NoiseDefaultsProbe.tscn
+ ///
+ /// Exits non-zero if this engine's defaults have drifted from the recorded baseline — so it
+ /// works as a pre-flight check, not just by eye. A non-zero exit is NOT "the terrain is broken":
+ /// the terrain is pinned and fine. It means the baseline note below needs updating, and that
+ /// anyone reading old code which relied on defaults should be warned.
+ ///
+ public partial class NoiseDefaultsProbe : Node
+ {
+ // The defaults measured on Godot 4.7.2.stable.mono (this project's pinned engine), which
+ // match the Godot 4 documented defaults the reference implicitly rode on 4.7.1.
+ private const FastNoiseLite.FractalTypeEnum BaselineFractalType = FastNoiseLite.FractalTypeEnum.Fbm;
+ private const int BaselineOctaves = 5;
+ private const float BaselineGain = 0.5f;
+ private const float BaselineLacunarity = 2.0f;
+ private const float BaselineWeightedStrength = 0.0f;
+
+ public override void _Ready()
+ {
+ var n = new FastNoiseLite();
+
+ GD.Print("==================================================================");
+ GD.Print(" FastNoiseLite CONSTRUCTOR DEFAULTS — this engine build");
+ GD.Print("==================================================================");
+ GD.Print($"engine : {Engine.GetVersionInfo()["string"]}");
+ GD.Print("------------------------------------------------------------------");
+ GD.Print($"NoiseType : {n.NoiseType}");
+ GD.Print($"Frequency : {n.Frequency}");
+ GD.Print($"FractalType : {n.FractalType}");
+ GD.Print($"FractalOctaves : {n.FractalOctaves}");
+ GD.Print($"FractalGain : {n.FractalGain}");
+ GD.Print($"FractalLacunarity : {n.FractalLacunarity}");
+ GD.Print($"FractalWeightedStrength : {n.FractalWeightedStrength}");
+ GD.Print($"Seed : {n.Seed}");
+ GD.Print("------------------------------------------------------------------");
+ GD.Print(" DRIFT CHECK vs the recorded 4.7.2 baseline");
+ GD.Print("------------------------------------------------------------------");
+
+ bool drift = false;
+ drift |= Check("FractalType", n.FractalType.ToString(), BaselineFractalType.ToString());
+ drift |= Check("FractalOctaves", n.FractalOctaves.ToString(), BaselineOctaves.ToString());
+ drift |= Check("FractalGain", n.FractalGain.ToString(), BaselineGain.ToString());
+ drift |= Check("FractalLacunarity", n.FractalLacunarity.ToString(), BaselineLacunarity.ToString());
+ drift |= Check("FractalWeightedStrength", n.FractalWeightedStrength.ToString(), BaselineWeightedStrength.ToString());
+
+ GD.Print("------------------------------------------------------------------");
+ GD.Print(drift
+ ? " ⚠ DRIFT — this engine's defaults differ from the recorded baseline.\n" +
+ " The PORTED TERRAIN IS UNAFFECTED: TerrainNoise pins every value explicitly.\n" +
+ " Update the baseline constants in this probe, and note the delta."
+ : " ✓ NO DRIFT — defaults match the recorded baseline.");
+ GD.Print("==================================================================");
+
+ GetTree().Quit(drift ? 1 : 0);
+ }
+
+ private static bool Check(string name, string actual, string expected)
+ {
+ bool differs = actual != expected;
+ GD.Print($" {(differs ? "✗" : "✓")} {name,-24} actual={actual,-12} baseline={expected}");
+ return differs;
+ }
+ }
+}
diff --git a/Tools/Scripts/NoiseDefaultsProbe.cs.uid b/Tools/Scripts/NoiseDefaultsProbe.cs.uid
new file mode 100644
index 0000000..caa8cdc
--- /dev/null
+++ b/Tools/Scripts/NoiseDefaultsProbe.cs.uid
@@ -0,0 +1 @@
+uid://b7vf6kp8041ie
diff --git a/Tools/Scripts/Pass1Result.cs b/Tools/Scripts/Pass1Result.cs
new file mode 100644
index 0000000..941114b
--- /dev/null
+++ b/Tools/Scripts/Pass1Result.cs
@@ -0,0 +1,88 @@
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// Everything pass 1 produces. → .
+ ///
+ /// ⭐ THREE OF THESE FIELDS ARE THE PHASE-2 SEAM. The reference computed them in pass 1 and
+ /// consumed them in pass 2; exposing them now is what keeps the deferred ports clean, and it
+ /// costs nothing to carry.
+ ///
+ public sealed class Pass1Result
+ {
+ /// Map side in columns.
+ public readonly int MapSize;
+
+ /// The resolved seed this was generated with. Always positive, always recorded.
+ public readonly int Seed;
+
+ ///
+ /// The raw pass-1 height field, [x, y]. Pre-curve, pre-erosion, pre-crater.
+ /// The reference's finalH (~:619) written to _heightMap (~:666).
+ ///
+ public readonly float[,] Height;
+
+ ///
+ /// ⭐ PHASE-2 SEAM — the falloff value captured BEFORE the Pow(·, 2.5f) and BEFORE the
+ /// Trench wall (reference ~:591, preTrenchFalloff).
+ ///
+ /// It is the "how far past the island body are we" number, uncontaminated by the power's
+ /// curvature and by the Trench's additive ramp. The DEFERRED offshore-islet layer reads it
+ /// as its distance mask (OffshoreZoneWeight), which is the only reason it is captured
+ /// mid-computation rather than recomputed.
+ ///
+ /// ⚠ A port that "tidies" the falloff chain into one expression destroys this capture point,
+ /// and the loss would not show up until Phase 2's offshore mask moved. It is exposed here so
+ /// that cannot happen quietly.
+ ///
+ public readonly float[,] PreTrenchFalloff;
+
+ ///
+ /// ⭐ PHASE-2 SEAM — the wobbled latitude scalar, [x, y].
+ ///
+ /// ⚠ THIS IS NOT A CLIMATE TEMPERATURE MAP. See for the full note.
+ /// It is a stage-1-local latitude field that terrain GEOMETRY reads. Phase 2 needs it if the
+ /// latitude sea-level model is ever switched on (SeaLevelModel = "field"); under the
+ /// shipped flat model it has no consumer beyond the spine.
+ ///
+ public readonly float[,] LatitudeField;
+
+ ///
+ /// ⭐ PHASE-2 SEAM — the maximum height over the whole map (reference _hMaxSeed, ~:665).
+ ///
+ /// The reference's v2+ redistribution curve is SEED-DEPENDENT: its spike maps
+ /// [t4, hMaxSeed] onto the peak band, so pass 2 cannot start until pass 1 has scanned
+ /// every pixel. That is precisely why the pass-1/pass-2 boundary is a hard one and not an
+ /// interleave. Carried here so Phase 2 inherits it rather than rediscovering the dependency.
+ ///
+ public readonly float HMaxSeed;
+
+ /// The minimum height. Not a reference field — carried for the renderer's ramp and the report.
+ public readonly float HMinSeed;
+
+ /// Wall-clock milliseconds the pass took.
+ public readonly ulong ElapsedMs;
+
+ public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
+ float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs)
+ {
+ MapSize = mapSize;
+ Seed = seed;
+ Height = height;
+ PreTrenchFalloff = preTrenchFalloff;
+ LatitudeField = latitudeField;
+ HMaxSeed = hMaxSeed;
+ HMinSeed = hMinSeed;
+ ElapsedMs = elapsedMs;
+ }
+
+ /// Fraction of the map at or above the sea threshold. A cheap shape sanity number.
+ public float LandFraction(float seaLevel)
+ {
+ long land = 0;
+ for (int x = 0; x < MapSize; x++)
+ for (int y = 0; y < MapSize; y++)
+ if (Height[x, y] >= seaLevel) land++;
+ return land / (float)((long)MapSize * MapSize);
+ }
+ }
+}
diff --git a/Tools/Scripts/Pass1Result.cs.uid b/Tools/Scripts/Pass1Result.cs.uid
new file mode 100644
index 0000000..df97667
--- /dev/null
+++ b/Tools/Scripts/Pass1Result.cs.uid
@@ -0,0 +1 @@
+uid://dxtoqjudaokbe
diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs
new file mode 100644
index 0000000..b5f3313
--- /dev/null
+++ b/Tools/Scripts/TerrainGenConfig.cs
@@ -0,0 +1,104 @@
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// The generator's configuration, including the per-element ABLATION TOGGLES.
+ ///
+ /// ═══ WHY EVERY PASS-1 ELEMENT IS GATED ═══
+ ///
+ /// This is the standing A/B discipline: every shaping change ships behind a gate with the
+ /// previous behaviour surviving on the other side, so changes stay comparable as a pair rather
+ /// than as a memory of last week's render, and a rejected change costs a flipped default rather
+ /// than a reverted commit. → `Design - Tooling - Iteration and Batching.md`.
+ ///
+ /// For THIS task the gates do a second job: they are the PORT-FIDELITY CHECK. Generating
+ /// base-noise-only, then adding one element at a time, shows each ported element doing what the
+ /// reference's did — rather than judging six simultaneous changes by their sum.
+ ///
+ /// ⚠ Lives in Tools/, not Core/. It configures the generator specifically; Core carries the
+ /// world's data contracts and the scaling rule, not a tool's dials.
+ ///
+ public sealed class TerrainGenConfig
+ {
+ // ---- world ----------------------------------------------------------
+
+ ///
+ /// Map side in columns. A GENERATION PARAMETER — never baked in.
+ /// Iteration runs small (2048–4096); the shape is scale-invariant by construction, so the
+ /// island reads the same at any size and a full-size pass is confirmation, not iteration.
+ ///
+ public int MapSize = 2048;
+
+ ///
+ /// The noise seed. POSITIVE ONLY. Zero or negative means "pick one and print it" — a run
+ /// whose seed is not recorded is a run that cannot be reproduced, so the resolved seed is
+ /// always printed and always in the filename.
+ ///
+ public int Seed = 0;
+
+ // ---- island shape (reference ConfigManager defaults, READ from source) ----
+
+ /// Falloff X axis ratio. Reference: ConfigManager.LEGACY_AXIS_X = 1.15f.
+ public float IslandAxisX = 1.15f;
+
+ /// Falloff Y axis ratio. Reference: ConfigManager.LEGACY_AXIS_Y = 0.90f.
+ public float IslandAxisY = 0.90f;
+
+ ///
+ /// Multiplier on the falloff term in the combine.
+ /// Reference: [Export] public float FalloffStrength = 1.0f; (MapGenerator ~:11),
+ /// and NOT overridden in MapPreview.tscn — so 1.0f is the value the island was tuned at.
+ ///
+ public float FalloffStrength = 1.0f;
+
+ ///
+ /// The flat sea level, in raw height units. Reference: ConfigManager.SeaLevelValue = 0.15f
+ /// with SeaLevelModel = "flat", so GetSeaLevel ignores latitude entirely.
+ ///
+ /// ⚠ PHASE 1 USES THIS AS A VISUALIZATION THRESHOLD ONLY — the boundary between the
+ /// bathymetric and hypsometric colour ramps. No water is modelled, no water bodies are
+ /// identified, nothing floods. That is Phase 2.
+ ///
+ public float SeaLevel = 0.15f;
+
+ // ---- ABLATION TOGGLES — the pass-1 ladder ---------------------------
+
+ /// Rung 1: the base terrain noise, (noise(x,y)+1)/2. Off = flat zero.
+ public bool BaseNoise = true;
+
+ ///
+ /// Rung 2: the island falloff/mask — squircle + ellipse blend, and the Pow(·, 2.5f).
+ /// ⚠ Off also disables rungs 3–5 in effect: edge noise, the sinker and the Trench are all
+ /// modifiers OF the falloff, so with no falloff there is nothing for them to modify. The
+ /// toggles stay independent so the ladder reads honestly; the report says so.
+ ///
+ public bool IslandFalloff = true;
+
+ /// Rung 3: coastline edge roughness, modulated by the squircle.
+ public bool EdgeNoise = true;
+
+ /// Rung 4: the southern sinker — extra sinking pressure in the bottom 25%.
+ public bool SouthernSinker = true;
+
+ /// Rung 5: the Trench — the map-anchored outer-band wall that guarantees an ocean border.
+ public bool Trench = true;
+
+ /// Rung 6: the mountain spine up the centre-X axis.
+ public bool MountainSpine = true;
+
+ /// A short label for this variant, used in output filenames. E.g. "full", "base_only".
+ public string VariantLabel = "full";
+
+ /// The scale object every distance and frequency in the generator derives from.
+ public GenerationScale Scale => new GenerationScale(MapSize);
+
+ public TerrainGenConfig Clone() => (TerrainGenConfig)MemberwiseClone();
+
+ public override string ToString() =>
+ $"MapSize={MapSize} Seed={Seed} axis={IslandAxisX:F2}x/{IslandAxisY:F2}y " +
+ $"falloffStrength={FalloffStrength:F2} sea={SeaLevel:F2} variant={VariantLabel} " +
+ $"[base={BaseNoise} falloff={IslandFalloff} edge={EdgeNoise} sinker={SouthernSinker} " +
+ $"trench={Trench} spine={MountainSpine}]";
+ }
+}
diff --git a/Tools/Scripts/TerrainGenConfig.cs.uid b/Tools/Scripts/TerrainGenConfig.cs.uid
new file mode 100644
index 0000000..6e86a24
--- /dev/null
+++ b/Tools/Scripts/TerrainGenConfig.cs.uid
@@ -0,0 +1 @@
+uid://da5dt7r145j67
diff --git a/Tools/Scripts/TerrainGenTool.cs b/Tools/Scripts/TerrainGenTool.cs
new file mode 100644
index 0000000..c5c4d67
--- /dev/null
+++ b/Tools/Scripts/TerrainGenTool.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// The Phase-1 generation entry point: runs a seed batch plus the ablation ladder, writes
+ /// hypsometric PNGs and raw height dumps into the batch layout, and quits.
+ ///
+ /// ═══ RUNNING IT ═══
+ ///
+ /// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
+ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn
+ ///
+ /// ⚠ `--headless` is correct HERE and will stay correct as long as the renderer writes a
+ /// Godot.Image directly. It awaits no render frames. The moment anything composites a `_Draw`
+ /// overlay through a SubViewport, this becomes an `xvfb-run` job — see Tools/README.md.
+ ///
+ /// ═══ CONFIGURATION (all optional; every path is env-overridable per the tooling rails) ═══
+ ///
+ /// ISLA_MAPSIZE map side in columns (default 2048 — iteration size)
+ /// ISLA_SEEDS comma-separated positive ints (default: the pinned batch below)
+ /// ISLA_BATCH batch folder name (default 01_pass1_port)
+ /// ISLA_OUTPUT_DIR where batches/ lives (Core/ToolingPaths)
+ /// ISLA_SKIP_RAW "1" to skip the .f32 dumps
+ /// ISLA_LADDER "0" to skip the ablation ladder (for a full-size confirmation pass)
+ ///
+ public partial class TerrainGenTool : Node
+ {
+ ///
+ /// PINNED POSITIVE SEEDS. Pinned, not random, so a batch is reproducible and two batches are
+ /// comparable — a standing convention. Positive only.
+ ///
+ private static readonly int[] DefaultSeeds = { 1063685222, 20260819, 777001, 424242 };
+
+ private const int DefaultMapSize = 2048;
+
+ public override void _Ready()
+ {
+ ToolingPaths.Configure(OS.GetUserDataDir());
+
+ int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
+ int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
+ string batch = EnvStr("ISLA_BATCH", "01_pass1_port");
+ bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
+ bool ladder = EnvStr("ISLA_LADDER", "1") == "1";
+
+ var scale = new GenerationScale(mapSize);
+ string batchRoot = Path.Combine(ToolingPaths.BatchesRoot, batch);
+ string scratch = ToolingPaths.BatchScratch(batchRoot);
+ DirAccess.MakeDirRecursiveAbsolute(batchRoot);
+ DirAccess.MakeDirRecursiveAbsolute(scratch); // persistent; never cleaned
+
+ GD.Print("==================================================================");
+ GD.Print(" PASS-1 TERRAIN GENERATION — the crown-jewel port (Phase 1)");
+ GD.Print("==================================================================");
+ GD.Print($"MapSize : {mapSize} (scaleFactor {scale.ScaleFactor:F3})");
+ GD.Print($"noise : {TerrainNoise.Describe(0, scale).Replace(" · seed 0", "")}");
+ GD.Print($"seeds : {string.Join(", ", seeds)}");
+ GD.Print($"batch : {batchRoot}");
+ GD.Print("------------------------------------------------------------------");
+ GD.Print(ToolingPaths.Describe());
+ GD.Print("==================================================================");
+
+ var rows = new List();
+
+ // ═══ 1. THE ABLATION LADDER — first seed only, one element added per rung ═══
+ //
+ // This is the port-fidelity check: six simultaneous changes cannot be judged by their
+ // sum, so each element is switched on in the reference's own order and looked at alone.
+ if (ladder)
+ {
+ GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---");
+ foreach (var (label, mutate) in Ladder())
+ {
+ var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label };
+ mutate(cfg);
+ rows.Add(RunOne(cfg, batchRoot, skipRaw));
+ }
+ }
+ else GD.Print("\n--- ablation ladder skipped (ISLA_LADDER=0) ---");
+
+ // ═══ 2. THE SEED BATCH — the full six-element config across pinned seeds ═══
+ GD.Print("\n--- SEED BATCH (full pass-1) ---");
+ foreach (int seed in seeds)
+ {
+ var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" };
+ rows.Add(RunOne(cfg, batchRoot, skipRaw));
+ }
+
+ WriteIndex(batchRoot, mapSize, scale, seeds, rows);
+
+ GD.Print("\n==================================================================");
+ GD.Print($" DONE — {rows.Count} maps in {batchRoot}");
+ GD.Print("==================================================================");
+ GetTree().Quit(0);
+ }
+
+ ///
+ /// The ablation rungs, in the reference's execution order. Each rung ADDS one element to the
+ /// one before it, so a difference between adjacent images is attributable to exactly one
+ /// ported element.
+ ///
+ private static List<(string, Action)> Ladder() => new()
+ {
+ ("ab1_base_only", c => { c.IslandFalloff = false; c.EdgeNoise = false; c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }),
+ ("ab2_falloff", c => { c.EdgeNoise = false; c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }),
+ ("ab3_edge", c => { c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }),
+ ("ab4_sinker", c => { c.Trench = false; c.MountainSpine = false; }),
+ ("ab5_trench", c => { c.MountainSpine = false; }),
+ ("ab6_spine_full", c => { /* everything on — identical to the "full" variant */ }),
+ };
+
+ private static string RunOne(TerrainGenConfig cfg, string batchRoot, bool skipRaw)
+ {
+ Pass1Result r = Topography.Generate(cfg);
+
+ // batches/NN_/_/
+ string dir = Path.Combine(batchRoot, $"{cfg.Seed}_{cfg.VariantLabel}");
+ DirAccess.MakeDirRecursiveAbsolute(dir);
+
+ string png = HeightMapRenderer.SavePng(r, cfg.SeaLevel, Path.Combine(dir, "height.png"));
+ if (!skipRaw) HeightMapRenderer.SaveRaw(r, Path.Combine(dir, "height.f32"));
+
+ float land = r.LandFraction(cfg.SeaLevel);
+ GD.Print($" {cfg.VariantLabel,-16} seed {cfg.Seed,-11} " +
+ $"h[{r.HMinSeed,7:F3} .. {r.HMaxSeed,6:F3}] land {land * 100,5:F1}% {r.ElapsedMs,5} ms");
+
+ return $"| `{cfg.Seed}_{cfg.VariantLabel}` | {cfg.Seed} | {cfg.VariantLabel} | " +
+ $"{r.HMinSeed:F3} | {r.HMaxSeed:F3} | {land * 100:F1}% | {r.ElapsedMs} ms |";
+ }
+
+ private static void WriteIndex(string batchRoot, int mapSize, GenerationScale scale, int[] seeds, List rows)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# Batch — pass-1 port (Phase 1)");
+ sb.AppendLine();
+ sb.AppendLine("Raw pass-1 terrain from the ported crown-jewel noise. **No curve, no erosion, no");
+ sb.AppendLine("rivers, no water, no crater, no biomes.** Each folder holds `height.png` (hypsometric)");
+ sb.AppendLine("and `height.f32` (raw float32, x-major, for byte-level comparison).");
+ sb.AppendLine();
+ sb.AppendLine($"- **MapSize:** {mapSize} (scaleFactor {scale.ScaleFactor:F3})");
+ sb.AppendLine($"- **Noise:** {TerrainNoise.Describe(0, scale).Replace(" · seed 0", "")}");
+ sb.AppendLine($"- **Sea threshold (visualization only):** 0.15");
+ sb.AppendLine($"- **Colour ramp anchors (FIXED across the batch):** sea {HeightMapRenderer.RampBottomHeight} .. 0.15, land 0.15 .. {HeightMapRenderer.RampTopHeight}");
+ sb.AppendLine($"- **Seeds:** {string.Join(", ", seeds)}");
+ sb.AppendLine();
+ sb.AppendLine("## What to look at");
+ sb.AppendLine();
+ sb.AppendLine("**The ablation ladder** (`ab1`..`ab6`, all on seed " + seeds[0] + ") adds one ported element per");
+ sb.AppendLine("rung, in the reference's execution order — so any difference between adjacent images is");
+ sb.AppendLine("attributable to exactly one element:");
+ sb.AppendLine();
+ sb.AppendLine("| Rung | Adds | Expect to see |");
+ sb.AppendLine("|---|---|---|");
+ sb.AppendLine("| `ab1_base_only` | base noise | featureless fractal field, no island, no coast |");
+ sb.AppendLine("| `ab2_falloff` | island mask + `Pow(·,2.5)` | an island appears, smooth-edged |");
+ sb.AppendLine("| `ab3_edge` | edge roughness | the coastline goes jagged — **and only the coastline** |");
+ sb.AppendLine("| `ab4_sinker` | southern sinker | the bottom 25% sinks; southern land bridges break up |");
+ sb.AppendLine("| `ab5_trench` | the Trench | a hard ocean border on all four edges |");
+ sb.AppendLine("| `ab6_spine_full` | mountain spine | a ridge up the centre, fading out toward the south |");
+ sb.AppendLine();
+ sb.AppendLine("**The seed batch** (`_full`) is the complete six-element pass-1 across pinned seeds.");
+ sb.AppendLine();
+ sb.AppendLine("> ⚠ **The seabed reads raw, and that is expected.** The submarine coast shelf and the");
+ sb.AppendLine("> offshore islets are DEFERRED to Phase 2 — they act only below sea level and are judged");
+ sb.AppendLine("> once water renders. Steep, plain bathymetry here is not a bug.");
+ sb.AppendLine();
+ sb.AppendLine("## Results");
+ sb.AppendLine();
+ sb.AppendLine("| Folder | Seed | Variant | h min | h max | land % | time |");
+ sb.AppendLine("|---|---|---|---|---|---|---|");
+ foreach (string row in rows) sb.AppendLine(row);
+ sb.AppendLine();
+ sb.AppendLine("`scratch/` is persistent and is never cleaned.");
+
+ string index = Path.Combine(batchRoot, "INDEX.md");
+ using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write);
+ if (f == null) { GD.PrintErr($"could not write {index}"); return; }
+ f.StoreString(sb.ToString());
+ }
+
+ // ---- env helpers ----------------------------------------------------
+
+ private static string EnvStr(string k, string fallback)
+ {
+ string v = System.Environment.GetEnvironmentVariable(k);
+ return string.IsNullOrWhiteSpace(v) ? fallback : v;
+ }
+
+ private static int EnvInt(string k, int fallback)
+ => int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
+
+ private static int[] EnvSeeds(string k, int[] fallback)
+ {
+ string v = EnvStr(k, null);
+ if (v == null) return fallback;
+ var outp = new List();
+ foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
+ return outp.Count > 0 ? outp.ToArray() : fallback;
+ }
+ }
+}
diff --git a/Tools/Scripts/TerrainGenTool.cs.uid b/Tools/Scripts/TerrainGenTool.cs.uid
new file mode 100644
index 0000000..e10d225
--- /dev/null
+++ b/Tools/Scripts/TerrainGenTool.cs.uid
@@ -0,0 +1 @@
+uid://e2oef4cf0vsl
diff --git a/Tools/Scripts/TerrainNoise.cs b/Tools/Scripts/TerrainNoise.cs
new file mode 100644
index 0000000..b1ee0f2
--- /dev/null
+++ b/Tools/Scripts/TerrainNoise.cs
@@ -0,0 +1,93 @@
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// ⭐⭐ THE CROWN JEWEL — the terrain noise configuration, ported and PINNED.
+ /// → D-050 ("port, don't re-derive"), `Design - Rewrite - Extraction Manifest.md`.
+ ///
+ /// ═══ WHAT THE REFERENCE ACTUALLY SET, AND WHAT IT DID NOT ═══
+ ///
+ /// The reference (MapGenerator._Ready ~:141-144) set exactly THREE properties:
+ ///
+ /// _noise = new FastNoiseLite();
+ /// _noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed;
+ /// _noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
+ /// _noise.Frequency = 0.004f / scaleFactor;
+ ///
+ /// That is all four lines. There is no fifth. Fractal type, octaves, gain, lacunarity and
+ /// weighted-strength were NEVER SET anywhere in that codebase — so the island's entire fractal
+ /// character was Godot 4.7.1's `FastNoiseLite` CONSTRUCTOR DEFAULTS, and nothing recorded what
+ /// they were. (chat1/00 §2.1, §4.1.)
+ ///
+ /// ═══ ⚠⚠ WHY EVERY VALUE IS PINNED EXPLICITLY BELOW ═══
+ ///
+ /// Because a default is not a decision, and this project no longer rides one. The terrain must
+ /// not change when the engine version changes, and it must not change when this code is ported
+ /// off Godot's FastNoiseLite to a C++ or hand-rolled implementation — which D-049 explicitly
+ /// anticipates ("C++-ready seams... port path-by-path when settled and hot"). An unpinned
+ /// default would move the island silently, and nothing in the source would say what moved.
+ ///
+ /// Measured on Godot 4.7.2.stable.mono (Tools/Scenes/NoiseDefaultsProbe.tscn):
+ /// FractalType Fbm · Octaves 5 · Gain 0.5 · Lacunarity 2.0 · WeightedStrength 0.0
+ /// These MATCH the Godot 4 documented defaults the reference rode on 4.7.1 — so pinning them
+ /// reproduces the reference look exactly, with no delta to reconcile.
+ ///
+ /// ⚠ Note NoiseType is the one place the reference did NOT ride the default: the constructor
+ /// default is SimplexSmooth, and the reference explicitly chose Simplex. Different noise.
+ ///
+ /// Run NoiseDefaultsProbe after any engine upgrade. It cannot change the terrain — this file
+ /// pins it — but it makes a drifting default visible instead of silent.
+ ///
+ public static class TerrainNoise
+ {
+ // ---- the pinned fractal character (was 4.7.1 constructor defaults) ----
+ public const FastNoiseLite.NoiseTypeEnum PinnedNoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
+ public const FastNoiseLite.FractalTypeEnum PinnedFractalType = FastNoiseLite.FractalTypeEnum.Fbm;
+ public const int PinnedOctaves = 5;
+ public const float PinnedGain = 0.5f;
+ public const float PinnedLacunarity = 2.0f;
+ public const float PinnedWeightedStrength = 0.0f;
+
+ ///
+ /// The base frequency, stated at the 1024 baseline. The reference wrote
+ /// 0.004f / scaleFactor; here the division is 's
+ /// job, so the literal never reaches a call site. → `Design - Tooling - Scaling Discipline.md`.
+ ///
+ public const float BaselineFrequency = 0.004f;
+
+ ///
+ /// Build the one terrain noise field. ⚠ ONE INSTANCE, deliberately.
+ ///
+ /// The reference sampled the latitude wobble, the coastline edge roughness and the base
+ /// height from the SAME `_noise` object — three correlated slices of one field, at three
+ /// different coordinate transforms. That correlation is part of the look, so the port keeps
+ /// one instance rather than "tidying" it into three seeded fields.
+ ///
+ public static FastNoiseLite Create(int seed, GenerationScale scale)
+ {
+ var noise = new FastNoiseLite();
+
+ // --- ported verbatim from the reference ---
+ noise.Seed = seed;
+ noise.NoiseType = PinnedNoiseType;
+ noise.Frequency = scale.NoiseFrequency(BaselineFrequency); // = 0.004f / scaleFactor
+
+ // --- PINNED: the reference left these to the engine. We do not. ---
+ noise.FractalType = PinnedFractalType;
+ noise.FractalOctaves = PinnedOctaves;
+ noise.FractalGain = PinnedGain;
+ noise.FractalLacunarity = PinnedLacunarity;
+ noise.FractalWeightedStrength = PinnedWeightedStrength;
+
+ return noise;
+ }
+
+ /// The pinned configuration as one line, for a run header.
+ public static string Describe(int seed, GenerationScale scale) =>
+ $"Simplex · Fbm · octaves {PinnedOctaves} · gain {PinnedGain} · lacunarity {PinnedLacunarity} · " +
+ $"weighted {PinnedWeightedStrength} · freq {scale.NoiseFrequency(BaselineFrequency):G6} " +
+ $"(= {BaselineFrequency} / {scale.ScaleFactor:F3}) · seed {seed}";
+ }
+}
diff --git a/Tools/Scripts/TerrainNoise.cs.uid b/Tools/Scripts/TerrainNoise.cs.uid
new file mode 100644
index 0000000..ddf3973
--- /dev/null
+++ b/Tools/Scripts/TerrainNoise.cs.uid
@@ -0,0 +1 @@
+uid://bvyldfs65ouuh
diff --git a/Tools/Scripts/Topography.cs b/Tools/Scripts/Topography.cs
new file mode 100644
index 0000000..19c58c9
--- /dev/null
+++ b/Tools/Scripts/Topography.cs
@@ -0,0 +1,292 @@
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// ⭐⭐ PASS 1 — the raw island height field. THE CROWN-JEWEL PORT.
+ /// → D-050 ("port, don't re-derive"), `Design - Rewrite - Extraction Manifest.md`.
+ ///
+ /// Ported from the reference's MapGenerator.GenerateTopography pass-1 loop
+ /// (Tools/Scripts/MapGenerator.cs ~:553-619 at tag pre-rewrite-reference,
+ /// commit ab78883). Constants are READ FROM THAT SOURCE, not re-derived from a design
+ /// summary — that distinction is the whole point of D-050.
+ ///
+ /// ═══ THE SIX ELEMENTS, IN THE REFERENCE'S EXECUTION ORDER ═══
+ ///
+ /// 1. wobbled latitude scalar (ref ~:559-562)
+ /// 2. island falloff / mask (ref ~:567-574, 593)
+ /// 3. edge / coastline roughness (ref ~:577-578)
+ /// 4. southern sinker (ref ~:582-587)
+ /// 5. the Trench (ref ~:595-598)
+ /// 6. mountain spine (ref ~:605-612)
+ /// → combine and write (ref ~:618-619, 665-666)
+ ///
+ /// ⚠⚠ THE ORDER IS LOAD-BEARING AND IS NOT AN IMPLEMENTATION DETAIL.
+ /// The sinker lands BEFORE the power, so its effect is superlinear. The Trench lands AFTER it,
+ /// so it is a raw additive wall the exponent never softens. preTrenchFalloff is captured
+ /// between them. Reordering any of it changes the island.
+ ///
+ /// ═══ ⚠ WHAT IS DELIBERATELY NOT PORTED HERE ═══
+ ///
+ /// The reference's pass-1 loop continues past the height write with two more task-11 passes:
+ /// the submarine COAST SHELF (~:621-640) and the OFFSHORE ISLET layer (~:641-664). Both are
+ /// DEFERRED to Phase 2 by the developer's ruling — they act only on below-sea height and are
+ /// judged once water renders. is exposed for them.
+ ///
+ /// Nothing from pass 2 is here at all: no redistribution curve, no shelf detail, no erosion,
+ /// no rivers, no water bodies, no crater carve, no biomes.
+ ///
+ public static class Topography
+ {
+ // ═══ CONSTANTS, ALL READ FROM THE REFERENCE SOURCE ═══
+ // Named rather than inlined so each one is greppable and has a home for its reason.
+
+ /// The squircle/ellipse blend weight. Reference: Mathf.Lerp(ellipse, squircle, 0.5f) (~:574).
+ private const float BlendSquircleWeight = 0.5f;
+
+ ///
+ /// The ellipse's scale divisor: ellipticalPos.Length() / (MapSize / 1.3f) (~:572).
+ ///
+ /// ⚠ AN UNCOMMENTED SHAPE DIAL IN THE REFERENCE, PORTED AS ONE. It sets the ellipse's
+ /// absolute scale against the squircle's, so it moves the coastline — but the reference
+ /// carried no comment and the vault's "50/50 blend of two falloffs" description does not
+ /// expose it. Ported verbatim because the island was tuned with it; named here so the next
+ /// reader knows it is a dial and not arithmetic. (chat1/00 §2.2.)
+ ///
+ private const float EllipseScaleDivisor = 1.3f;
+
+ /// Edge-noise coordinate multiplier. Reference: GetNoise2D(x * 2.5f, y * 2.5f) (~:577).
+ private const float EdgeNoiseCoordScale = 2.5f;
+
+ /// Edge-noise amplitude. Reference: finalFalloff += (edgeNoise * 0.15f) * squircleFalloff (~:578).
+ private const float EdgeNoiseAmplitude = 0.15f;
+
+ /// Southern sinker threshold, as a fraction of the map. Reference: MapSize * 0.75f (~:582).
+ private const float SouthThresholdFraction = 0.75f;
+
+ /// Southern sinker depth. Reference: finalFalloff += southDepth * 0.6f (~:586).
+ private const float SouthSinkAmount = 0.6f;
+
+ /// The falloff exponent. Reference: Mathf.Pow(finalFalloff, 2.5f) (~:593).
+ private const float FalloffExponent = 2.5f;
+
+ /// Trench onset, as a fraction of the half-axis. Reference: if (distX > 0.90f) (~:597).
+ private const float TrenchOnset = 0.90f;
+
+ /// Trench slope. Reference: finalFalloff += (distX - 0.90f) * 15.0f (~:597-598).
+ private const float TrenchSlope = 15.0f;
+
+ /// Spine gate / fade anchor. Reference: if (temperature < 0.65f) and (0.65f - temperature) (~:606, 610).
+ private const float SpineLatitudeAnchor = 0.65f;
+
+ /// Spine southern-fade slope. Reference: Clamp((0.65f - temperature) * 4.0f, 0, 1) (~:610).
+ private const float SpineFadeSlope = 4.0f;
+
+ /// Spine cubic concentration. Reference: Mathf.Pow(mountainSpine, 3.0f) (~:611).
+ private const float SpineConcentration = 3.0f;
+
+ /// Spine amplitude in raw height units. Reference: * 0.6f (~:611).
+ private const float SpineAmplitude = 0.6f;
+
+ /// Latitude wobble amplitude. Reference: temperature += (tempNoise * 0.2f) - 0.1f (~:561) — i.e. ±0.1.
+ private const float LatitudeWobbleSpan = 0.2f;
+
+ ///
+ /// The latitude noise's decorrelation offset, IN MAP WIDTHS.
+ ///
+ /// ⚠⚠ THE ONE PLACE THIS PORT DELIBERATELY DOES NOT COPY THE REFERENCE'S LITERAL.
+ ///
+ /// The reference wrote GetNoise2D(x + 1000, y + 1000) — an offset in RAW PIXELS.
+ /// Because frequency already carries a 1/MapSize normalization, a pixel offset does not hold
+ /// still across map sizes: 1000 px is 1.0 normalized units at 4K but 0.5 at 8K, so a resize
+ /// silently samples a DIFFERENT SLICE of the noise field. Same feature scale, different
+ /// realization — the same seed gives a different world at a different size, for no reason
+ /// anyone wrote down. (chat1/00 §4.2; the §6 tightening carried in from chat1/01.)
+ ///
+ /// 1000 px at the reference's canonical MapSize of 8192 = 0.1220703125 map widths. Pinning
+ /// THAT reproduces the reference's realization exactly at 8K, and holds it still everywhere
+ /// else — which the reference did not.
+ ///
+ private const float LatitudeNoiseOffsetMapWidths = 1000f / 8192f;
+
+ ///
+ /// Generate the raw pass-1 height field.
+ ///
+ public static Pass1Result Generate(TerrainGenConfig cfg)
+ {
+ ulong t0 = Time.GetTicksMsec();
+
+ GenerationScale scale = cfg.Scale;
+ int mapSize = cfg.MapSize;
+ FastNoiseLite noise = TerrainNoise.Create(cfg.Seed, scale);
+
+ var height = new float[mapSize, mapSize];
+ var preTrenchFalloff = new float[mapSize, mapSize];
+ var latitudeField = new float[mapSize, mapSize];
+
+ // ⚠ MAP-CENTRED, not crater-centred. Reference ~:550:
+ // Vector2 center = new Vector2(MapSize / 2.0f, MapSize / 2.0f);
+ // The crater's centre (_impactCenter) is a separate, randomly placed field used only in
+ // pass 2 and the biome pass. The spine and the mask both key off the MAP centre — the
+ // spine is a map-centred ridge, per `Design - Terrain - Mountain Spine.md`. Confirmed
+ // against source; no crater coupling exists in pass 1.
+ float centerX = mapSize / 2.0f;
+ float centerY = mapSize / 2.0f;
+
+ float axisX = cfg.IslandAxisX;
+ float axisY = cfg.IslandAxisY;
+
+ // Precomputed once — the reference recomputed these per pixel; identical results.
+ float latitudeNoiseOffset = scale.OffsetInMapWidths(LatitudeNoiseOffsetMapWidths);
+ float southThreshold = scale.Fraction(SouthThresholdFraction);
+ float halfSpan = mapSize / 2.0f;
+
+ float hMax = float.MinValue;
+ float hMin = float.MaxValue;
+
+ // ⚠ x IS THE OUTER LOOP, as in the reference. Numerically irrelevant here, but a
+ // PARALLEL port must reduce hMax/hMin rather than share them — noted before someone
+ // reaches for Parallel.For and quietly races on the running max.
+ for (int x = 0; x < mapSize; x++)
+ {
+ for (int y = 0; y < mapSize; y++)
+ {
+ // ═══ 1. WOBBLED LATITUDE SCALAR (ref ~:559-562) ═══
+ //
+ // ⚠⚠ THE REFERENCE CALLED THIS "temperature". IT IS NOT CLIMATE, AND THIS PORT
+ // WILL NOT CALL IT THAT.
+ //
+ // It is a stage-1-local LATITUDE FIELD that terrain GEOMETRY reads — the spine's
+ // southern fade is its only pass-1 consumer. The prototype named it temperature,
+ // then let biomes read the same array, and that shared name is a large part of
+ // how climate and terrain got fused in the first place. In the rewrite,
+ // CLIMATE IS STAGE 3 and classifies finished shape (D-049 §2, D-056); this
+ // scalar is stage 1 and must never be stored as, aliased to, or mistaken for it.
+ //
+ // The ±0.1 WOBBLE IS KEPT, and it is load-bearing: it makes the spine's southern
+ // terminus a low-frequency wave rather than a ruler-straight latitude line.
+ // Replacing this with a bare y/MapSize would straighten it — the exact opposite
+ // of the filed spine-meander want. (chat1/00 §2.3.)
+ float lat = (float)y / mapSize;
+ float latNoise = (noise.GetNoise2D(x + latitudeNoiseOffset, y + latitudeNoiseOffset) + 1.0f) / 2.0f;
+ lat += (latNoise * LatitudeWobbleSpan) - (LatitudeWobbleSpan / 2.0f);
+ latitudeField[x, y] = lat;
+
+ // ═══ 2. THE ISLAND FALLOFF / MASK (ref ~:567-574) ═══
+ float finalFalloff = 0f;
+ float squircleFalloff = 0f;
+
+ if (cfg.IslandFalloff)
+ {
+ // Squircle — Max(nx, ny), giving squared-off corners. ISLAND-anchored:
+ // the axis ratios are applied here.
+ float nx = Mathf.Abs(x - centerX) / (halfSpan * axisX);
+ float ny = Mathf.Abs(y - centerY) / (halfSpan * axisY);
+ squircleFalloff = Mathf.Max(nx, ny);
+
+ // Ellipse — vector length, giving a rounded shape.
+ var ellipticalPos = new Vector2((x - centerX) / axisX, (y - centerY) / axisY);
+ float ellipticalFalloff = ellipticalPos.Length() / (mapSize / EllipseScaleDivisor);
+
+ // 50/50 blend.
+ finalFalloff = Mathf.Lerp(ellipticalFalloff, squircleFalloff, BlendSquircleWeight);
+ }
+
+ // ═══ 3. EDGE / COASTLINE ROUGHNESS (ref ~:577-578) ═══
+ //
+ // ⚠ THE `* squircleFalloff` MODULATION IS PART OF IT AND IS EASY TO DROP.
+ // It makes the roughness ZERO AT THE ISLAND CENTRE and strongest at the rim —
+ // which is why it reads as coastline jitter instead of interior grain. The
+ // vault's description of this element omits it. (chat1/00 §2.2, §4.5.)
+ //
+ // The x2.5 coordinate multiplier is NOT an unscaled quantity: it multiplies
+ // coordinates that the already-normalized frequency then scales, so it is a
+ // fixed 2.5x RATIO to the base noise at every map size. (chat1/00 §4.2 —
+ // which corrected the vault on exactly this point.)
+ if (cfg.IslandFalloff && cfg.EdgeNoise)
+ {
+ float edgeNoise = (noise.GetNoise2D(x * EdgeNoiseCoordScale, y * EdgeNoiseCoordScale) + 1.0f) / 2.0f;
+ finalFalloff += (edgeNoise * EdgeNoiseAmplitude) * squircleFalloff;
+ }
+
+ // ═══ 4. THE SOUTHERN SINKER (ref ~:582-587) ═══
+ //
+ // Sinks the stretched land bridges in the bottom 25%. ⚠ BEFORE the power, so its
+ // effect is superlinear — +0.6 on a falloff already near 1 costs far more height
+ // than +0.6 near 0. Moving it after the power would change the southern coast.
+ if (cfg.IslandFalloff && cfg.SouthernSinker && y > southThreshold)
+ {
+ float southDepth = (y - southThreshold) / (mapSize - southThreshold);
+ finalFalloff += southDepth * SouthSinkAmount;
+ }
+
+ // ═══ ⭐ THE PHASE-2 SEAM — captured BEFORE the power and BEFORE the Trench ═══
+ // (ref ~:591). See Pass1Result.PreTrenchFalloff for why this exact point.
+ preTrenchFalloff[x, y] = finalFalloff;
+
+ if (cfg.IslandFalloff)
+ finalFalloff = Mathf.Pow(finalFalloff, FalloffExponent);
+
+ // ═══ 5. THE TRENCH (ref ~:595-598) ═══
+ //
+ // ⚠⚠ MAP-ANCHORED, NOT ISLAND-ANCHORED — distX/distY carry NO axis ratio, unlike
+ // nx/ny above. That is deliberate and load-bearing: the Trench is a guarantee
+ // about the MAP BORDER, not about the island's ellipse. It is what makes the
+ // flood fill's unchecked (0,0) ocean seed safe — the corner is guaranteed
+ // underwater by a x15 wall. DO NOT "unify" it with the mask's normalization.
+ // → `Design - Terrain - Island Mask.md`, chat1/00 §4.5.
+ //
+ // ⚠ The two clauses are ADDITIVE — a corner pixel takes both.
+ // ⚠ AFTER the power, so it is a raw additive wall the exponent never softens.
+ if (cfg.IslandFalloff && cfg.Trench)
+ {
+ float distX = Mathf.Abs(x - centerX) / halfSpan;
+ float distY = Mathf.Abs(y - centerY) / halfSpan;
+ if (distX > TrenchOnset) finalFalloff += (distX - TrenchOnset) * TrenchSlope;
+ if (distY > TrenchOnset) finalFalloff += (distY - TrenchOnset) * TrenchSlope;
+ }
+
+ // ═══ 6. THE MOUNTAIN SPINE (ref ~:605-612) ═══
+ //
+ // ⚠ THE REFERENCE'S `if (temperature < 0.65f)` GATE IS DROPPED, AND THE RESULT IS
+ // BIT-IDENTICAL. southernFade = Clamp((0.65 - lat) * 4, 0, 1) already reaches
+ // EXACTLY ZERO at lat = 0.65 — the same constant the gate tested — so the gate
+ // guarded a term that had already faded to nothing. It introduced no
+ // discontinuity and skipped no visible work. southernFade, not the gate, is what
+ // actually shapes the spine's southern end. (chat1/00 §2.3.)
+ //
+ // The gate is expressed below as `fade > 0`, which is the same predicate stated
+ // where it is true rather than where it is incidental.
+ float mountainSpine = 0f;
+ if (cfg.MountainSpine)
+ {
+ float southernFade = Mathf.Clamp((SpineLatitudeAnchor - lat) * SpineFadeSlope, 0.0f, 1.0f);
+ if (southernFade > 0f)
+ {
+ // ISLAND-anchored: axisX applies here (ref ~:608).
+ float distanceToCenterX = Mathf.Abs(x - centerX) / (halfSpan * axisX);
+ float crest = 1.0f - IslandFalloff.SmoothAbs(distanceToCenterX, IslandFalloff.CREST_EPSILON);
+ mountainSpine = Mathf.Pow(crest, SpineConcentration) * southernFade * SpineAmplitude;
+ }
+ }
+
+ // ═══ COMBINE AND WRITE (ref ~:618-619, 665-666) ═══
+ float rawBase = cfg.BaseNoise ? (noise.GetNoise2D(x, y) + 1.0f) / 2.0f : 0f;
+ float finalH = rawBase + mountainSpine - (finalFalloff * cfg.FalloffStrength);
+
+ if (finalH > hMax) hMax = finalH;
+ if (finalH < hMin) hMin = finalH;
+ height[x, y] = finalH;
+
+ // ⚠ THE REFERENCE'S PASS 1 CONTINUES HERE with the coast shelf (~:621-640) and
+ // the offshore islets (~:641-664). Both DEFERRED to Phase 2 — below-sea only,
+ // judged once water renders. preTrenchFalloff above is their inlet.
+ }
+ }
+
+ return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField,
+ hMax, hMin, Time.GetTicksMsec() - t0);
+ }
+ }
+}
diff --git a/Tools/Scripts/Topography.cs.uid b/Tools/Scripts/Topography.cs.uid
new file mode 100644
index 0000000..a9f51fe
--- /dev/null
+++ b/Tools/Scripts/Topography.cs.uid
@@ -0,0 +1 @@
+uid://cwaxsloo2ncr8