diff --git a/Core/Scripts/ContinuousCurve.cs b/Core/Scripts/ContinuousCurve.cs
new file mode 100644
index 0000000..dc4df02
--- /dev/null
+++ b/Core/Scripts/ContinuousCurve.cs
@@ -0,0 +1,418 @@
+using System;
+using System.Text;
+
+namespace IslaApocalypse.Core
+{
+ ///
+ /// ⭐⭐ THE CONTINUOUS-GRADE CURVE (chat2/02) — smooth the UPPER staircase, preserve the lowlands.
+ ///
+ /// ═══ WHAT THIS IS, AND WHAT IT REFUSES TO BE ═══
+ ///
+ /// The faithful v5 staircase () terraces the island above the flood
+ /// tiers: foothill riser → bench → mid riser → plateau → summit needle. The developer's verdict
+ /// on the 01 baseline: the LOWLANDS ARE GOOD — the broad low plain, ~75 % of land below 30 m, is
+ /// the thing to keep. The fault is entirely ABOVE them: flat benches read as authored terraces
+ /// and the summit reads as a needle on a hump.
+ ///
+ /// So this curve is PIECEWISE, and the pieces have different loyalties:
+ ///
+ /// raw ≤ SEA IDENTITY. The coastline must not move. (Same line as v5.)
+ /// SEA < raw ≤ K2 ⭐ THE STAIRCASE'S OWN toe+red mapping, BY DELEGATION — the same
+ /// code path, so the lowland output is BIT-IDENTICAL to task 01's.
+ /// Not "equivalent": the same floats. Oracle (d) holds this.
+ /// K2 < raw ≤ ceilingRaw the red band's exit slope, CONTINUED LINEARLY — only non-empty
+ /// when the ceiling is raised above the default 30 m, extending the
+ /// current gentle low grade before the climb begins.
+ /// ceilingRaw < raw ≤ spikeMax
+ /// ⭐ THE NEW CLIMB — one smooth monotone Fritsch–Carlson (PCHIP)
+ /// spline from the lowland ceiling to PEAK_CAP. No bench, no
+ /// plateau, no needle: a coherent massif steepening to a peak.
+ /// raw > spikeMax the gentle tail, as v5: PEAK_CAP + (raw − spikeMax) · TAIL_SLOPE.
+ ///
+ /// ═══ ⚠⚠ WHAT IS DELIBERATELY DROPPED, AND WHAT DELIBERATELY SURVIVES ═══
+ ///
+ /// DROPPED: BENCH_BASE/AMP, PLATEAU_BASE/AMP, SHELF_SPAN_* and their three
+ /// modulation noise fields — the above-flood decorative terracing. That is the entire point of
+ /// this mode.
+ ///
+ /// SURVIVES: SEA, ORANGE_CEIL (14 m) and RED_CEIL (30 m), because they are
+ /// the STORM-LADDER FLOOD TIERS and they live inside the preserved lowland — D-036's
+ /// terrain-shelves-at-flood-tiers is intact where it carries meaning. PEAK_CAP (420 m)
+ /// survives as the summit ceiling, with the per-seed spikeMax normalization unchanged.
+ ///
+ /// ═══ ⚠ MONOTONE BY CONSTRUCTION — WHY THE 24-CORNER SWEEP RETIRES HERE ═══
+ ///
+ /// Fritsch–Carlson tangent limiting guarantees a monotone interpolant for ANY monotone control
+ /// points: every tangent is clamped into the region where the Hermite cubic cannot overshoot.
+ /// The staircase needed a numeric sweep because its effective shape depended on three modulation
+ /// fields and a per-column warp; this curve has no per-column inputs at all — one spline per
+ /// seed. still runs a cheap dense sample per seed,
+ /// because "cannot fail" is exactly the claim worth spending a millisecond checking.
+ ///
+ /// ═══ THE TWO KNOBS (plus the ceiling) — ALL ACT ABOVE THE CEILING ONLY ═══
+ ///
+ /// lowlandCeiling where the preserved low grade hands over to the climb (config, metres;
+ /// default 30 = RED_CEIL, i.e. the flood line — hand over exactly where the
+ /// staircase's lowland ends).
+ /// climbFeather how long the climb hugs the lowland's exit slope before steepening.
+ /// summitDrama how hard the top ~15 % steepens, so the peak reads pointy, not a ramp.
+ ///
+ /// ⚠ NO CONTROL POINT MAY ACT AS A MAGNET. The generator enforces strictly INCREASING segment
+ /// secants below the summit: mass can never pile at an interior point the way it piled at the
+ /// bench, because no interval maps wide-in to narrow-out below the summit onset.
+ ///
+ /// Engine-free (System.MathF), beside — the two modes are one seam.
+ ///
+ public sealed class ContinuousCurve
+ {
+ // ═══ shape constants (not config — the knobs above are the config surface) ═══
+
+ /// Where "the summit" begins, as a fraction of the climb's raw span. The top 15 %.
+ public const float SummitOnset = 0.85f;
+
+ ///
+ /// The ceiling knob's hard bound, metres. The bench sat at 100±12 m; a lowland ceiling at or
+ /// above it could preserve a flat bench, which is the one thing this mode exists to remove.
+ /// 80 m keeps clear air below the old bench's lowest excursion (88 m).
+ ///
+ public const float MaxLowlandCeilingM = 80f;
+
+ ///
+ /// Oracle (e) tripwires, in NORMALIZED climb slope (1 = the climb's average grade).
+ /// Floor: a slope this far below the join slope reads as a bench — the artifact this mode
+ /// removes. Ceiling: a slope this steep below the summit onset reads as a cliff.
+ /// Warn-and-report, not throw — this is an exploration batch.
+ ///
+ public const float NearFlatFactor = 0.25f; // × the normalized join slope
+ public const float CliffCeilingN = 3.5f;
+
+ // ═══ the built spline ═══
+
+ /// The knot set — only K1/K2 are consumed (the preserved toe+red).
+ public readonly CurveKnots Knots;
+
+ /// The anchors — Sea/Orange/Red/PeakCap/TailSlope consumed; bench/plateau ignored.
+ public readonly CurveAnchors Anchors;
+
+ /// Raw height where the preserved lowland hands over to the climb.
+ public readonly float CeilingRaw;
+
+ /// Output height at the handover — the top of the preserved lowland.
+ public readonly float CeilingOut;
+
+ /// This seed's raw summit: EffectiveSpikeMax(hMaxSeed). The climb's right edge.
+ public readonly float SpikeMax;
+
+ /// The red band's exit slope — the climb's C¹ join tangent (raw out per raw in).
+ public readonly float JoinSlopeRaw;
+
+ /// The knob values this spline was built from, for the INDEX and the report.
+ public readonly float LowlandCeilingM, ClimbFeather, SummitDrama;
+
+ // Control points (raw x, out y) and the Fritsch–Carlson tangents. x strictly increasing.
+ private readonly float[] _x, _y, _m;
+
+ private ContinuousCurve(CurveKnots k, CurveAnchors a, float ceilingRaw, float ceilingOut,
+ float spikeMax, float joinSlopeRaw, float lowlandCeilingM, float climbFeather,
+ float summitDrama, float[] x, float[] y, float[] m)
+ {
+ Knots = k; Anchors = a;
+ CeilingRaw = ceilingRaw; CeilingOut = ceilingOut; SpikeMax = spikeMax;
+ JoinSlopeRaw = joinSlopeRaw;
+ LowlandCeilingM = lowlandCeilingM; ClimbFeather = climbFeather; SummitDrama = summitDrama;
+ _x = x; _y = y; _m = m;
+ }
+
+ ///
+ /// Build the per-seed spline. ⚠ PER SEED, because is per seed —
+ /// exactly the same reason the staircase's monotonicity sweep ran per seed.
+ ///
+ /// Throws (refusing the generation) on any configuration that cannot produce the target
+ /// silhouette: a ceiling at bench height, a drama that would fold the summit under its own
+ /// onset, a ceiling above the seed's summit.
+ ///
+ public static ContinuousCurve Build(CurveKnots k, CurveAnchors a, float spikeMax,
+ float lowlandCeilingM, float climbFeather, float summitDrama)
+ {
+ // ---- the preserved lowland's edge ----
+ float redSlope = (a.RedCeil - a.OrangeCeil) / (k.K2 - k.K1);
+
+ if (lowlandCeilingM > MaxLowlandCeilingM)
+ throw new InvalidOperationException(
+ $"[ContinuousCurve] lowlandCeiling {lowlandCeilingM:F1} m is above the {MaxLowlandCeilingM:F0} m " +
+ "bound — close enough to the old bench (100±12 m) to preserve a flat one, which is the " +
+ "artifact this mode exists to remove. Refusing.");
+
+ // ⚠ THE FLOOD LINE IS THE FLOOR, and "30 m" is NOMINAL: RED_CEIL − SEA = 0.12 raw is
+ // actually 30.12 m through the yardstick. Any requested ceiling at or below the red
+ // ceiling means "hand over exactly where the preserved lowland ends", and that handover
+ // is pinned to THE EXACT ANCHORS — (K2, RED_CEIL), no derived floats — so the extension
+ // region is empty by construction and the toe+red band can never be cut. (The first
+ // probe run refused its own default over this 0.12 m nominal gap; pinning is the fix,
+ // not widening a tolerance.)
+ float redCeilM = WorldScale.MetresFromRaw(a.RedCeil - a.Sea);
+ float ceilingOut, ceilingRaw;
+ if (lowlandCeilingM <= redCeilM + 0.01f)
+ {
+ ceilingOut = a.RedCeil;
+ ceilingRaw = k.K2;
+ }
+ else
+ {
+ ceilingOut = a.Sea + WorldScale.RawFromMetres(lowlandCeilingM);
+ // Where the linear red-slope extension reaches that output.
+ ceilingRaw = k.K2 + (ceilingOut - a.RedCeil) / redSlope;
+ }
+
+ if (ceilingRaw >= spikeMax - 1e-3f)
+ throw new InvalidOperationException(
+ $"[ContinuousCurve] lowland ceiling (raw {ceilingRaw:F4}) reaches this seed's summit " +
+ $"(spikeMax {spikeMax:F4}) — no room for a climb. Refusing.");
+ if (climbFeather < 0f || climbFeather > 1f)
+ throw new InvalidOperationException($"[ContinuousCurve] climbFeather {climbFeather} is outside [0,1]. Refusing.");
+ if (summitDrama < 1f)
+ throw new InvalidOperationException($"[ContinuousCurve] summitDrama {summitDrama} < 1 would make the summit SHALLOWER than the climb's average — that is a ramp, not a peak. Refusing.");
+
+ // ---- control points, in normalized climb space ----
+ // u = (raw − ceilingRaw)/(spikeMax − ceilingRaw), v = (out − ceilingOut)/(PeakCap − ceilingOut).
+ float spanRaw = spikeMax - ceilingRaw;
+ float spanOut = a.PeakCap - ceilingOut;
+ float s0 = redSlope * spanRaw / spanOut; // the join slope, normalized
+
+ // The feather point: hug the join slope until u_f, then lift. Larger feather = longer hug.
+ float uF = 0.20f + 0.35f * climbFeather;
+ float vF = s0 * uF * 1.05f; // fractionally above the pure hug, so
+ // the secant already rises — no dip
+
+ // The summit onset: its secant to (1,1) IS the drama. v_s = 1 − drama·(1 − u_s).
+ float uS = SummitOnset;
+ float vS = 1f - summitDrama * (1f - uS);
+ if (vS <= vF + 0.02f)
+ throw new InvalidOperationException(
+ $"[ContinuousCurve] summitDrama {summitDrama:F2} folds the summit onset (v={vS:F3}) " +
+ $"under the feather point (v={vF:F3}) — the mid-climb would have to be flat or " +
+ "descending to compensate. Lower the drama or the feather. Refusing.");
+
+ // A mid point keeps the feather→onset transition smooth, on a gently convex path so the
+ // segment secants stay strictly INCREASING — the no-magnet guarantee.
+ float uM = (uF + uS) * 0.5f;
+ float vM = vF + (vS - vF) * MathF.Pow((uM - uF) / (uS - uF), 1.35f);
+
+ float[] u = { 0f, uF, uM, uS, 1f };
+ float[] v = { 0f, vF, vM, vS, 1f };
+
+ // ⚠ THE NO-MAGNET CHECK, enforced rather than assumed: every secant below the summit
+ // must be strictly greater than the one before it. A wide-in→narrow-out interval below
+ // the onset is a bench in the making.
+ float prevSecant = 0f;
+ for (int i = 1; i < u.Length; i++)
+ {
+ float sec = (v[i] - v[i - 1]) / (u[i] - u[i - 1]);
+ if (sec <= prevSecant)
+ throw new InvalidOperationException(
+ $"[ContinuousCurve] control-point secants are not strictly increasing at segment {i} " +
+ $"({sec:F4} after {prevSecant:F4}) with feather={climbFeather:F2}, drama={summitDrama:F2} — " +
+ "an interior point would act as a magnet. Refusing.");
+ prevSecant = sec;
+ }
+
+ // ---- denormalize and fit ----
+ int n = u.Length;
+ var x = new float[n];
+ var y = new float[n];
+ for (int i = 0; i < n; i++)
+ {
+ x[i] = ceilingRaw + u[i] * spanRaw;
+ y[i] = ceilingOut + v[i] * spanOut;
+ }
+
+ float[] m = FritschCarlsonTangents(x, y, startTangent: redSlope);
+
+ return new ContinuousCurve(k, a, ceilingRaw, ceilingOut, spikeMax, redSlope,
+ lowlandCeilingM, climbFeather, summitDrama, x, y, m);
+ }
+
+ ///
+ /// Fritsch–Carlson (1980) monotone tangents, with a PRESCRIBED start tangent for the C¹
+ /// join. The weighted-harmonic-mean interior tangents already satisfy the monotonicity
+ /// region; the prescribed start is clamped into [0, 3·Δ₀], which is the classical
+ /// sufficient bound — so the join is C¹ wherever the lowland's exit slope permits, and
+ /// safely limited where it does not (which is then reported by the slope sampler, not
+ /// hidden).
+ ///
+ private static float[] FritschCarlsonTangents(float[] x, float[] y, float startTangent)
+ {
+ int n = x.Length;
+ var h = new float[n - 1]; // interval widths
+ var d = new float[n - 1]; // secants
+ for (int i = 0; i < n - 1; i++)
+ {
+ h[i] = x[i + 1] - x[i];
+ d[i] = (y[i + 1] - y[i]) / h[i];
+ }
+
+ var m = new float[n];
+
+ // Start: the C¹ join, clamped into the monotone region.
+ m[0] = Math.Clamp(startTangent, 0f, 3f * d[0]);
+
+ // Interior: weighted harmonic mean — zero if the secants disagree in sign (they cannot
+ // here, both positive, but the guard is the algorithm's own and stays).
+ for (int i = 1; i < n - 1; i++)
+ {
+ if (d[i - 1] * d[i] <= 0f) { m[i] = 0f; continue; }
+ float w1 = 2f * h[i] + h[i - 1];
+ float w2 = h[i] + 2f * h[i - 1];
+ m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i]);
+ }
+
+ // End: one-sided three-point estimate, clamped like the start. The summit's entry
+ // steepness comes from the last secant (the drama), not from an extrapolated spike.
+ float mEnd = ((2f * h[n - 2] + (n > 2 ? h[n - 3] : h[n - 2])) * d[n - 2]
+ - h[n - 2] * (n > 2 ? d[n - 3] : d[n - 2]))
+ / (h[n - 2] + (n > 2 ? h[n - 3] : h[n - 2]));
+ if (mEnd < 0f) mEnd = 0f;
+ m[n - 1] = MathF.Min(mEnd, 3f * d[n - 2]);
+
+ return m;
+ }
+
+ ///
+ /// The curve, for one column. Handles every range: sea identity, the preserved lowland
+ /// (BY DELEGATION to — the same code path, hence the same
+ /// bits), the linear extension, the climb, the tail.
+ ///
+ public float Apply(float h)
+ {
+ // ⭐ IDENTITY AT AND BELOW SEA — the same load-bearing line as v5.
+ if (h <= Anchors.Sea) return h;
+
+ // ⭐ THE PRESERVED LOWLAND: delegate to the staircase's own toe+red branches. Below K2,
+ // HeightCurve.Apply never reads the bench/plateau/edge parameters, so any values pass —
+ // and the output is bit-identical to task 01's staircase, which oracle (d) asserts.
+ if (h < Knots.K2)
+ return HeightCurve.Apply(h, SpikeMax,
+ Anchors.BenchBase, Anchors.ShelfSpanMin, Anchors.PlateauBase, Anchors.ShelfSpanMin,
+ Knots, Anchors, edgeShift: 0f);
+
+ // The red band's grade, continued. Empty at the default ceiling (CeilingRaw == K2);
+ // at h == K2 exactly this is RED_CEIL + 0 — the same value the staircase's foothill
+ // riser produces at its own u = 0.
+ if (h <= CeilingRaw)
+ return Anchors.RedCeil + (h - Knots.K2) * JoinSlopeRaw;
+
+ // The gentle tail, as v5 — a slope, not a clip.
+ if (h >= SpikeMax)
+ return Anchors.PeakCap + (h - SpikeMax) * Anchors.TailSlope;
+
+ // ⭐ THE CLIMB: cubic Hermite on the Fritsch–Carlson tangents.
+ int i = FindInterval(h);
+ float dx = _x[i + 1] - _x[i];
+ float t = (h - _x[i]) / dx;
+ float t2 = t * t, t3 = t2 * t;
+
+ return (2f * t3 - 3f * t2 + 1f) * _y[i]
+ + (t3 - 2f * t2 + t) * dx * _m[i]
+ + (-2f * t3 + 3f * t2) * _y[i + 1]
+ + (t3 - t2) * dx * _m[i + 1];
+ }
+
+ /// The climb's derivative at a raw height inside (CeilingRaw, SpikeMax).
+ public float SlopeAt(float h)
+ {
+ if (h <= CeilingRaw || h >= SpikeMax) return JoinSlopeRaw; // outside the spline proper
+ int i = FindInterval(h);
+ float dx = _x[i + 1] - _x[i];
+ float t = (h - _x[i]) / dx;
+ float t2 = t * t;
+
+ return (6f * t2 - 6f * t) * (_y[i] - _y[i + 1]) / dx
+ + (3f * t2 - 4f * t + 1f) * _m[i]
+ + (3f * t2 - 2f * t) * _m[i + 1];
+ }
+
+ private int FindInterval(float h)
+ {
+ // Four intervals — a linear scan beats a binary search at this size.
+ for (int i = _x.Length - 2; i > 0; i--)
+ if (h >= _x[i]) return i;
+ return 0;
+ }
+
+ ///
+ /// The cheap per-seed proof that "monotone by construction" held in float32 too: a dense
+ /// strict-increase sample over the whole range, sea to past the tail. Throws and refuses on
+ /// violation, exactly as the staircase's sweep did. ~10k samples, sub-millisecond.
+ ///
+ /// A one-line confirmation for the run log.
+ public string AssertStrictlyIncreasing()
+ {
+ float prevH = Anchors.Sea;
+ float prev = Apply(prevH);
+ double top = SpikeMax + 0.5;
+ double step = (top - Anchors.Sea) / 10000.0;
+
+ for (double hd = Anchors.Sea + step; hd <= top; hd += step)
+ {
+ float h = (float)hd;
+ if (h <= prevH) continue; // float32 dedupe, as the staircase's sweep
+ float v = Apply(h);
+ if (v <= prev)
+ throw new InvalidOperationException(
+ $"[ContinuousCurve] MONOTONICITY VIOLATION at h={h}: {v} <= {prev} " +
+ $"(ceiling {LowlandCeilingM:F0} m, feather {ClimbFeather:F2}, drama {SummitDrama:F2}). Refusing to generate.");
+ prev = v;
+ prevH = h;
+ }
+
+ return $"[ContinuousCurve] strict-increase sample passed (10k points, ceiling {LowlandCeilingM:F0} m, " +
+ $"feather {ClimbFeather:F2}, drama {SummitDrama:F2}, spikeMax {SpikeMax:F6}).";
+ }
+
+ ///
+ /// Oracle (e)'s instrument: sample the climb's slope densely and report it in NORMALIZED
+ /// units (1 = the climb's average grade). Returns the extremes and where they sit, plus the
+ /// tripwire verdicts — the caller decides how loudly to say it.
+ ///
+ public (float minN, float minAtRaw, float maxBelowOnsetN, float maxAtRaw, bool nearFlat, bool cliff)
+ SampleClimbSlopes(int samples = 2000)
+ {
+ float spanRaw = SpikeMax - CeilingRaw;
+ float spanOut = Anchors.PeakCap - CeilingOut;
+ float toN = spanRaw / spanOut; // raw slope → normalized
+ float onsetRaw = CeilingRaw + SummitOnset * spanRaw;
+ float s0N = JoinSlopeRaw * toN;
+
+ float minN = float.MaxValue, maxN = float.MinValue, minAt = 0f, maxAt = 0f;
+ for (int i = 1; i < samples; i++)
+ {
+ float h = CeilingRaw + spanRaw * i / samples;
+ float sN = SlopeAt(h) * toN;
+ if (sN < minN) { minN = sN; minAt = h; }
+ if (h < onsetRaw && sN > maxN) { maxN = sN; maxAt = h; }
+ }
+
+ bool nearFlat = minN < s0N * NearFlatFactor;
+ bool cliff = maxN > CliffCeilingN;
+ return (minN, minAt, maxN, maxAt, nearFlat, cliff);
+ }
+
+ /// Raw height where the summit onset sits, and its output — for histogram overlays.
+ public (float raw, float outp) SummitOnsetPoint()
+ {
+ float r = CeilingRaw + SummitOnset * (SpikeMax - CeilingRaw);
+ return (r, Apply(r));
+ }
+
+ /// The control points as one line for the INDEX and the report.
+ public string DescribeControlPoints()
+ {
+ var sb = new StringBuilder();
+ sb.Append($"ceiling {LowlandCeilingM:F0}m feather {ClimbFeather:F2} drama {SummitDrama:F2} · points ");
+ for (int i = 0; i < _x.Length; i++)
+ sb.Append($"({_x[i]:F4},{_y[i]:F4}{(i == 0 ? " C1" : "")}) ");
+ sb.Append($"· join slope {JoinSlopeRaw:F4} raw");
+ return sb.ToString();
+ }
+ }
+}
diff --git a/Core/Scripts/ContinuousCurve.cs.uid b/Core/Scripts/ContinuousCurve.cs.uid
new file mode 100644
index 0000000..23fa91c
--- /dev/null
+++ b/Core/Scripts/ContinuousCurve.cs.uid
@@ -0,0 +1 @@
+uid://cfyqb4kr3w1mu
diff --git a/Tools/Scenes/CurveContinuousTool.tscn b/Tools/Scenes/CurveContinuousTool.tscn
new file mode 100644
index 0000000..c7c638e
--- /dev/null
+++ b/Tools/Scenes/CurveContinuousTool.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cvcontin02isla"]
+
+[ext_resource type="Script" path="res://Tools/Scripts/CurveContinuousTool.cs" id="1_cct"]
+
+[node name="CurveContinuousTool" type="Node"]
+script = ExtResource("1_cct")
diff --git a/Tools/Scripts/CurveBaselineTool.cs b/Tools/Scripts/CurveBaselineTool.cs
index c2e3a77..54c1835 100644
--- a/Tools/Scripts/CurveBaselineTool.cs
+++ b/Tools/Scripts/CurveBaselineTool.cs
@@ -297,6 +297,10 @@ namespace IslaApocalypse.Tools
{
MapSize = mapSize, Seed = seed, VariantLabel = "curve_on",
Curve = true, ShelfDetail = true, Knots = k, Anchors = a,
+ // ⚠ PINNED, not defaulted: this tool exists to reproduce the task-01 staircase
+ // bit-for-bit. chat2/02 moved the config DEFAULT to Continuous for its exploration;
+ // a control batch must not move with a default. (chat2/02.)
+ CurveMode = CurveModeKind.Staircase,
};
///
diff --git a/Tools/Scripts/CurveContinuousTool.cs b/Tools/Scripts/CurveContinuousTool.cs
new file mode 100644
index 0000000..3fd1b31
--- /dev/null
+++ b/Tools/Scripts/CurveContinuousTool.cs
@@ -0,0 +1,580 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// ⭐ THE CONTINUOUS-GRADE EXPLORATION BATCH (chat2/02, rev 3) — smooth the upper staircase,
+ /// preserve the lowlands, and prove both claims before anyone looks at a render.
+ ///
+ /// ═══ THE TARGET SILHOUETTE (judge every shaped histogram against THIS) ═══
+ ///
+ /// The bottom shoulder stays exactly where it is — the broad low pile, ~75 % of land below
+ /// 30 m, UNLIFTED. Above it, the bench and plateau spikes dissolve and the empty valleys fill
+ /// in: one smooth continuous falloff out to a thin dramatic peak at 420 m. Only the top ~25 %
+ /// of land moves.
+ ///
+ /// ═══ THE VARIANTS — tight, one axis each ═══
+ ///
+ /// staircase the faithful M3 control (task 01, bit-identical — oracle a2)
+ /// continuous_default lowland to 30 m, feather 0.40, drama 2.5 — the centerpiece
+ /// continuous_hold_higher lowland ceiling raised to 50 m — the primary axis explored up
+ /// continuous_dramatic_peak drama 4.5 — how pointy the peak gets
+ /// continuous_lifted_WRONG ⚠ the even whole-island lift — DELIBERATELY the wrong direction,
+ /// a contrast bookend. Its oracle-(d) failure is EXPECTED and is
+ /// reported as confirmation of why the direction is wrong.
+ ///
+ /// ═══ ⚠ WHY THIS TOOL RE-MEASURES THE KNOTS INSTEAD OF USING CurveKnots.V2Baseline ═══
+ ///
+ /// Task 01's batch was generated with the knots MEASURED IN THAT RUN, at full float precision.
+ /// The baked V2Baseline constants are those values quoted to six decimals — off by <1e-7 raw,
+ /// which is invisible to every consumer EXCEPT a bit-identity oracle. Oracle (a2) demands the
+ /// staircase reproduce task 01's `.f32` byte-for-byte, so this tool re-runs the identical
+ /// 6-seed calibration (same seeds, same size, same instrument) and uses the measured knots.
+ /// It prints the measured-vs-baked deltas so the equivalence is evidence, not assumption.
+ ///
+ /// ═══ RUNNING IT ═══
+ ///
+ /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
+ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CurveContinuousTool.tscn
+ ///
+ /// ISLA_TASK authoring task number (default 2)
+ /// ISLA_BATCH descriptor, NO prefix (default "curve_continuous")
+ /// ISLA_MAPSIZE variant profile (default 2048)
+ /// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below)
+ /// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192)
+ /// ISLA_SHOWPIECE "0" to skip it
+ /// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port")
+ /// ISLA_T01_SOURCE batch holding task-01 .f32 (default "01_curve_baseline")
+ /// ISLA_SKIP_RAW "1" to skip the .f32 dumps
+ /// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30)
+ /// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4)
+ /// ISLA_DRAMA probe override: summit drama, >= 1 (default 2.5)
+ ///
+ /// ⚠ The three knob overrides move the BASE config only. Each batch variant sets its own axis
+ /// explicitly in its mutator, so a probe env can shift `continuous_default` but can never
+ /// silently move `hold_higher`'s ceiling or `dramatic_peak`'s drama out from under their names.
+ ///
+ public partial class CurveContinuousTool : Node
+ {
+ /// Two representative variant seeds — the primary Phase-1 seed and the tallest one.
+ private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
+
+ ///
+ /// ⚠ THE TASK-01 CALIBRATION POOL, VERBATIM — same six seeds, so the measured knots (and
+ /// with them the staircase control) reproduce task 01 bit-for-bit. Do not "simplify" this
+ /// to the variant seeds; the pool is part of the knots' identity.
+ ///
+ private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
+
+ private const int DefaultMapSize = 2048;
+ private const int DefaultShowpieceSize = 8192;
+
+ public override void _Ready()
+ {
+ // ⚠ An exception out of _Ready does not stop Godot — it logs and the process HANGS with
+ // no main loop to end it. Catch, say what was refused, exit 2. (The standing rule.)
+ try { Run(); }
+ catch (Exception e)
+ {
+ GD.PrintErr("==================================================================");
+ GD.PrintErr($" REFUSED: {e.Message}");
+ GD.PrintErr("==================================================================");
+ GetTree().Quit(2);
+ }
+ }
+
+ private void Run()
+ {
+ ToolingPaths.Configure(OS.GetUserDataDir());
+
+ int task = EnvInt("ISLA_TASK", 2);
+ string descr = EnvStr("ISLA_BATCH", "curve_continuous");
+ int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
+ int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
+ int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
+ bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
+ string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
+ string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
+ bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
+
+ string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form
+ DirAccess.MakeDirRecursiveAbsolute(batchRoot);
+ DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
+
+ var anchors = CurveAnchors.Default;
+ float sea = 0.15f;
+ int primary = seeds[0];
+
+ GD.Print("==================================================================");
+ GD.Print(" CURVE CONTINUOUS (rev 3) — smooth the upper staircase,");
+ GD.Print(" preserve the lowlands. The staircase rides along as the control.");
+ GD.Print("==================================================================");
+ GD.Print($"MapSize : {mapSize} showpiece {(showpiece ? showSize.ToString() : "off")}");
+ GD.Print($"yardstick : {WorldScale.Describe()}");
+ GD.Print($"seeds : {string.Join(", ", seeds)} (calibration pool: {string.Join(", ", CalibrationSeeds)})");
+ GD.Print($"batch : {batchRoot}");
+ GD.Print("------------------------------------------------------------------");
+ GD.Print(ToolingPaths.Describe());
+ GD.Print("==================================================================");
+
+ // ═══ 0. RE-MEASURE THE KNOTS — task 01's calibration, verbatim (see the type header) ═══
+ GD.Print("\n--- 0. CALIBRATION (the task-01 pool, re-run for bit-identity) ---");
+ var rawPool = new LandHistogram(sea);
+ var pass1 = new Dictionary();
+ foreach (int seed in CalibrationSeeds)
+ {
+ var p1 = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed });
+ pass1[seed] = p1;
+ rawPool.Accumulate(p1.Height, mapSize);
+ GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms");
+ }
+ var knots = new CurveKnots(2, "v2_balanced",
+ rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]),
+ rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]),
+ rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5]));
+
+ GD.Print(" measured vs baked V2Baseline (6-decimal roundings — expect <1e-6):");
+ for (int i = 0; i < 6; i++)
+ GD.Print($" K{i + 1}: measured {knots[i]:G9} baked {CurveKnots.V2Baseline[i]:G9} " +
+ $"delta {knots[i] - CurveKnots.V2Baseline[i]:E2}");
+
+ // ═══ 1. THE VARIANTS ═══
+ var variants = new List<(string label, Action mutate)>
+ {
+ ("staircase", c => { c.CurveMode = CurveModeKind.Staircase; c.ShelfDetail = true; }),
+ ("continuous_default", c => { c.CurveMode = CurveModeKind.Continuous; }),
+ ("continuous_hold_higher", c => { c.CurveMode = CurveModeKind.Continuous; c.LowlandCeilingM = 50f; }),
+ ("continuous_dramatic_peak", c => { c.CurveMode = CurveModeKind.Continuous; c.SummitDrama = 4.5f; }),
+ ("continuous_lifted_WRONG", c => { c.CurveMode = CurveModeKind.LiftedWrong; }),
+ };
+
+ GD.Print("\n--- 1. VARIANTS ---");
+ var results = new Dictionary<(int seed, string label), Pass2Result>();
+ var offs = new Dictionary();
+ var rows = new List();
+ bool notesPrinted = false;
+
+ foreach (int seed in seeds)
+ {
+ Pass1Result p1 = pass1[seed];
+ offs[seed] = Shaping.Shape(p1, BaseConfig(mapSize, seed, knots, anchors, "curve_off", off: true));
+
+ foreach (var (label, mutate) in variants)
+ {
+ var cfg = BaseConfig(mapSize, seed, knots, anchors, label, off: false);
+ mutate(cfg);
+ Pass2Result p2 = Shaping.Shape(p1, cfg);
+ results[(seed, label)] = p2;
+
+ if (!notesPrinted) foreach (string n in p2.Notes) GD.Print(" " + n);
+
+ rows.Add(WriteVariant(batchRoot, p2, sea, anchors, skipRaw));
+ }
+ notesPrinted = true;
+ }
+
+ // ═══ 2. THE ORACLE — before anything is looked at ═══
+ GD.Print("\n--- 2. ORACLE ---");
+ var hard = new List();
+ var soft = new List();
+ ShapingOracle.Check bookend;
+
+ {
+ Pass1Result pp1 = pass1[primary];
+
+ // (a1) curve off == Phase 1's own dump.
+ string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
+ hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
+ offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
+
+ // (a2) staircase == task 01's own dump — the control is the control.
+ string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
+ hard.Add(ShapingOracle.DumpRegression("a2", "staircase mode == task-01 curve_on .f32 dump",
+ results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
+
+ // (b) classify == raw, every seed × every variant.
+ long bFail = 0;
+ foreach (int seed in seeds)
+ foreach (var (label, _) in variants)
+ {
+ var c = ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]);
+ if (!c.Passed) { bFail++; GD.PrintErr($" classify drift: seed {seed} {label}: {c.Detail}"); }
+ }
+ hard.Add(new ShapingOracle.Check
+ {
+ Id = "b", Name = "classify == raw, all seeds × all variants",
+ Passed = bFail == 0,
+ Detail = bFail == 0
+ ? $"bit-identical on {seeds.Length} seeds × {variants.Count} variants"
+ : $"{bFail} (seed, variant) pairs drifted",
+ });
+
+ // (c) monotone — the staircase's sweep and the continuous strict-increase sample
+ // both throw on violation, so reaching here means they passed; state it explicitly.
+ bool cStair = results[(primary, "staircase")].Notes.Exists(n => n.Contains("Monotonicity assertion passed"));
+ bool cCont = results[(primary, "continuous_default")].Notes.Exists(n => n.Contains("strict-increase sample passed"));
+ hard.Add(new ShapingOracle.Check
+ {
+ Id = "c", Name = "monotone — staircase sweep + continuous strict-increase sample",
+ Passed = cStair && cCont,
+ Detail = $"staircase sweep {(cStair ? "confirmed" : "MISSING")}, " +
+ $"continuous sample {(cCont ? "confirmed" : "MISSING")} (both throw-and-refuse on violation)",
+ });
+
+ // (d) ⭐ lowlands preserved — every continuous variant, both seeds. HARD.
+ foreach (int seed in seeds)
+ foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
+ {
+ var c = ShapingOracle.LowlandsPreserved(pass1[seed], results[(seed, "staircase")], results[(seed, label)]);
+ c.Name += $" [seed {seed}]";
+ hard.Add(c);
+ }
+
+ // (d, bookend) the WRONG lift is EXPECTED to fail this — its failure is the point.
+ bookend = ShapingOracle.LowlandsPreserved(pp1, results[(primary, "staircase")],
+ results[(primary, "continuous_lifted_WRONG")]);
+
+ // (e) upper climb profile — SOFT (exploration): warn loudly, do not gate the exit.
+ foreach (int seed in seeds)
+ foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
+ {
+ var c = ShapingOracle.UpperClimbProfile(results[(seed, label)]);
+ c.Name += $" [seed {seed}]";
+ soft.Add(c);
+ }
+
+ // (f) sea identity — every variant, both seeds, per cell. HARD.
+ foreach (int seed in seeds)
+ foreach (var (label, _) in variants)
+ {
+ var c = ShapingOracle.SeaIdentity(offs[seed], results[(seed, label)], sea);
+ c.Name += $" [seed {seed}]";
+ hard.Add(c);
+ }
+ }
+
+ foreach (var c in hard) GD.Print(" " + c);
+ foreach (var c in soft)
+ {
+ if (c.Passed) GD.Print(" " + c);
+ else GD.PrintErr(" ⚠ SLOPE PROFILE: " + c);
+ }
+ GD.Print($" bookend (expected FAIL): {bookend}");
+ if (bookend.Passed)
+ GD.PrintErr(" ⚠⚠ the lifted_WRONG bookend PRESERVED the lowlands — it is not doing its job as a contrast.");
+
+ bool hardOk = hard.TrueForAll(c => c.Passed) && !bookend.Passed;
+ GD.Print($" ORACLE: {(hardOk ? "ALL HARD CHECKS PASS" : "*** HARD FAILURES ***")}" +
+ $"{(soft.TrueForAll(c => c.Passed) ? "" : " (soft slope warnings present — see above)")}");
+
+ // ═══ 3. HISTOGRAMS — the contrast, adjacent by filename ═══
+ GD.Print("\n--- 3. HISTOGRAMS ---");
+ foreach (int seed in seeds)
+ {
+ var rawSeed = new LandHistogram(sea);
+ rawSeed.Accumulate(pass1[seed].Height, mapSize);
+ DrawRawHist(rawSeed, knots, seed, mapSize, batchRoot);
+
+ int order = 1;
+ foreach (var (label, _) in variants)
+ {
+ DrawShapedHist(results[(seed, label)], rawSeed, anchors, seed, mapSize, batchRoot, order++);
+ }
+ }
+
+ // ═══ 4. SHOWPIECE — continuous_default at the big profile ═══
+ string showNote = "skipped (ISLA_SHOWPIECE=0)";
+ if (showpiece)
+ {
+ GD.Print($"\n--- 4. SHOWPIECE at {showSize} (continuous_default, seed {primary}) ---");
+ var cfg = BaseConfig(showSize, primary, knots, anchors, "continuous_default_showpiece", off: false);
+ cfg.CurveMode = CurveModeKind.Continuous;
+
+ Pass1Result p1 = Topography.Generate(cfg);
+ GD.Print($" pass1 {showSize}: h[{p1.HMinSeed:F3} .. {p1.HMaxSeed:F3}] {p1.ElapsedMs} ms");
+
+ Pass2Result big = Shaping.Shape(p1, cfg);
+ foreach (string n in big.Notes) GD.Print(" " + n);
+
+ // The cheap checks that transfer to the big profile: classify fidelity + sea identity.
+ var cb = ShapingOracle.ClassifyFidelity(p1, big);
+ var offBig = Shaping.Shape(p1, BaseConfig(showSize, primary, knots, anchors, "off", off: true));
+ var cf = ShapingOracle.SeaIdentity(offBig, big, sea);
+ GD.Print($" {cb}");
+ GD.Print($" {cf}");
+ if (!cb.Passed || !cf.Passed) hardOk = false;
+
+ rows.Add(WriteVariant(batchRoot, big, sea, anchors, skipRaw));
+ showNote = $"seed {primary} at {showSize} — classify + sea identity re-verified there";
+ }
+
+ WriteIndex(batchRoot, mapSize, showSize, seeds, primary, knots, anchors,
+ results, hard, soft, bookend, rows, hardOk, showNote);
+
+ GD.Print("\n==================================================================");
+ GD.Print($" DONE — {batchRoot}");
+ GD.Print($" ORACLE {(hardOk ? "HARD CHECKS ALL PASS" : "*** HARD FAILURES — see the table ***")}");
+ GD.Print("==================================================================");
+ GetTree().Quit(hardOk ? 0 : 3);
+ }
+
+ // ---- configs ----------------------------------------------------------
+
+ private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
+ string label, bool off) => new TerrainGenConfig
+ {
+ MapSize = mapSize, Seed = seed, VariantLabel = label,
+ Curve = !off, ShelfDetail = false, Knots = k, Anchors = a,
+ // Continuous knob defaults; variant mutators override per axis.
+ //
+ // ⚠ ENV-OVERRIDABLE so the knobs can be probed WITHOUT editing source and rebuilding —
+ // they are "eye-iteration knobs" per the task, and a knob you have to recompile to turn
+ // is not one. A probe run redirects ISLA_OUTPUT_DIR to scratch and sweeps these; the
+ // BATCH variants set them explicitly in their mutators, so a probe env cannot silently
+ // move the batch of record.
+ LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f),
+ ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f),
+ SummitDrama = EnvFloat("ISLA_DRAMA", 2.5f),
+ };
+
+ // ---- output -----------------------------------------------------------
+
+ private static string WriteVariant(string batchRoot, Pass2Result p2, float sea,
+ CurveAnchors anchors, bool skipRaw)
+ {
+ string dir = Path.Combine(batchRoot, $"{p2.Seed}_{LabelOf(p2)}");
+ DirAccess.MakeDirRecursiveAbsolute(dir);
+
+ // Plain data beside the pretty render, always.
+ var (gMin, gMax) = GrayscaleRenderer.SavePng(p2.Height, p2.MapSize, Path.Combine(dir, "grayscale.png"));
+ if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32"));
+
+ // ⚠ HILLSHADED, deliberately — the continuous grade is the first terrain with coherent
+ // shape worth lighting (Phase 1's rule was "flat is the hero" BECAUSE raw noise fuzzes;
+ // that reasoning inverts once the climb is smooth). Subtle settings, and the palette is
+ // the PROVISIONAL even ramp — flagged as such in the palette, the INDEX and the report.
+ var look = new LookConfig
+ {
+ Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven,
+ ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f,
+ HillshadeStrength = 0.30f, SeaLevel = sea,
+ };
+ Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
+ Image withLegend = LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap,
+ LabelOf(p2).ToUpperInvariant());
+ withLegend.SavePng(Path.Combine(dir, "relief.png"));
+
+ float land = p2.LandFraction(sea);
+ GD.Print($" {LabelOf(p2),-26} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
+ $" land {land * 100,5:F1}% {p2.ElapsedMs,5} ms");
+
+ return $"| `{p2.Seed}_{LabelOf(p2)}` | {p2.Seed} | {LabelOf(p2)} | {p2.HMin:F3} | {p2.HMax:F3} | " +
+ $"{land * 100:F1}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
+ }
+
+ /// The variant folder name — read off the result, never re-derived.
+ private static string LabelOf(Pass2Result p2) => p2.VariantLabel;
+
+ private static void DrawRawHist(LandHistogram raw, CurveKnots k, int seed, int mapSize, string batchRoot)
+ {
+ float top = MathF.Ceiling(raw.MaxLand * 20f) / 20f;
+ var display = raw.Rebin((top - raw.SeaLevel) / 360f);
+
+ var o = new HistogramRenderer.Options
+ {
+ Title = $"RAW LAND HEIGHTS - SEED {seed}",
+ Subtitle = "PRE-CURVE. K2 IS THE LOWLAND CEILING - EVERYTHING LEFT OF IT IS PRESERVED.",
+ XAxisLabel = "RAW HEIGHT (PRE-CURVE)",
+ XTop = top,
+ Footer = $"{raw.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
+ };
+ o.Markers.Add(new HistogramRenderer.Marker { Value = k.K2, Label = "K2 LOWLAND CEIL" });
+ o.Bands.Add(new HistogramRenderer.Band
+ {
+ Lo = raw.SeaLevel, Hi = k.K2, Label = "preserved lowland",
+ SharePercent = raw.FractionBelow(k.K2) * 100.0,
+ });
+ o.Bands.Add(new HistogramRenderer.Band
+ {
+ Lo = k.K2, Hi = top, Label = "the climb's input",
+ SharePercent = (1.0 - raw.FractionBelow(k.K2)) * 100.0,
+ });
+
+ HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, $"hist_{seed}_0_raw.png"));
+ GD.Print($" hist_{seed}_0_raw.png");
+ }
+
+ private static void DrawShapedHist(Pass2Result p2, LandHistogram rawSeed, CurveAnchors a,
+ int seed, int mapSize, string batchRoot, int order)
+ {
+ var shaped = new LandHistogram(rawSeed.SeaLevel);
+ shaped.Accumulate(p2.Height, mapSize);
+
+ float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
+ var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
+ string label = LabelOf(p2);
+
+ var o = new HistogramRenderer.Options
+ {
+ Title = $"SHAPED - {label.ToUpperInvariant()} - SEED {seed}",
+ XAxisLabel = "RAW HEIGHT (POST-CURVE)",
+ XTop = top,
+ Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
+ };
+
+ if (p2.Continuous != null)
+ {
+ var c = p2.Continuous;
+ var (onsetRaw, onsetOut) = c.SummitOnsetPoint();
+ o.Subtitle = "TARGET: BOTTOM PILE UNMOVED - ABOVE IT ONE SMOOTH FALLOFF TO A THIN PEAK.";
+ o.Markers.Add(new HistogramRenderer.Marker { Value = c.CeilingOut, Label = $"LOWLAND {c.LowlandCeilingM:F0}M" });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = onsetOut, Label = "SUMMIT ONSET", Strong = false });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
+ o.Bands.Add(new HistogramRenderer.Band
+ {
+ Lo = rawSeed.SeaLevel, Hi = c.CeilingOut, Label = "preserved lowland",
+ SharePercent = rawSeed.FractionBelow(c.CeilingRaw) * 100.0,
+ });
+ o.Bands.Add(new HistogramRenderer.Band
+ {
+ Lo = c.CeilingOut, Hi = onsetOut, Label = "the climb",
+ SharePercent = rawSeed.FractionBetween(c.CeilingRaw, onsetRaw) * 100.0,
+ });
+ o.Bands.Add(new HistogramRenderer.Band
+ {
+ Lo = onsetOut, Hi = top, Label = "summit",
+ SharePercent = (1.0 - rawSeed.FractionBelow(onsetRaw)) * 100.0,
+ });
+ }
+ else if (p2.CurveModeLabel == "staircase")
+ {
+ o.Subtitle = "THE CONTROL: THE M3 STAIRCASE - BENCH AND PLATEAU SPIKES BY DESIGN.";
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.OrangeCeil, Label = "ORANGE", Strong = false });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "RED", Strong = false });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.BenchBase, Label = "BENCH" });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.PlateauBase, Label = "PLATEAU" });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
+ }
+ else
+ {
+ o.Subtitle = "THE WRONG DIRECTION: THE WHOLE ISLAND LIFTED OFF ITS SHORELINE. A BOOKEND.";
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "OLD 30M LINE", Strong = false });
+ o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
+ }
+
+ string file = $"hist_{seed}_{order}_{label}.png";
+ HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file));
+ GD.Print($" {file}");
+ }
+
+ // ---- the index ----------------------------------------------------------
+
+ private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds,
+ int primary, CurveKnots knots, CurveAnchors a,
+ Dictionary<(int, string), Pass2Result> results,
+ List hard, List soft,
+ ShapingOracle.Check bookend, List rows, bool hardOk, string showNote)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# Batch 02 — continuous grade: smooth the upper staircase, preserve the lowlands");
+ sb.AppendLine();
+ sb.AppendLine("**rev 3.** The lowlands are GOOD and are preserved bit-for-bit (oracle d). Everything");
+ sb.AppendLine("above the 30 m flood line is replaced by one smooth monotone climb to the 420 m cap.");
+ sb.AppendLine("The staircase rides along as the control; the WRONG-direction whole-island lift rides");
+ sb.AppendLine("along as a contrast bookend. **Exploration, not convergence** — a tuning pass follows");
+ sb.AppendLine("once a direction is picked.");
+ sb.AppendLine();
+ sb.AppendLine("## ⭐ Open this first");
+ sb.AppendLine();
+ sb.AppendLine($"1. **`{primary}_continuous_default_showpiece/relief.png`** — the centerpiece ({showNote}).");
+ sb.AppendLine($"2. **`hist_{primary}_1_staircase.png` → `hist_{primary}_2_continuous_default.png`** — the");
+ sb.AppendLine(" contrast, adjacent by filename: bottom pile unchanged, the bench/plateau spikes above");
+ sb.AppendLine(" it dissolved into a smooth falloff.");
+ sb.AppendLine($"3. **`hist_{primary}_5_continuous_lifted_WRONG.png`** — the bookend: the whole pile shoved");
+ sb.AppendLine(" up off the shoreline. This is what \"make the island stand up\" would have done.");
+ sb.AppendLine($"4. `{primary}_staircase/relief.png` vs `{primary}_continuous_default/relief.png` — the A/B.");
+ sb.AppendLine();
+ sb.AppendLine("## The variants and their knobs");
+ sb.AppendLine();
+ sb.AppendLine("| Variant | Mode | Knobs | Control points |");
+ sb.AppendLine("|---|---|---|---|");
+ sb.AppendLine("| `staircase` | staircase | task-01 defaults (ShelfDetail on) | the v5 bands |");
+ foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
+ {
+ var c = results[(primary, label)].Continuous;
+ sb.AppendLine($"| `{label}` | continuous | ceiling {c.LowlandCeilingM:F0} m · feather {c.ClimbFeather:F2} · " +
+ $"drama {c.SummitDrama:F2} | {c.DescribeControlPoints()} |");
+ }
+ sb.AppendLine("| `continuous_lifted_WRONG` | lifted | none — an even ×~1.47 lift of all land | n/a |");
+ sb.AppendLine();
+ sb.AppendLine("## ⚠ The palette is PROVISIONAL");
+ sb.AppendLine();
+ sb.AppendLine("`ProvisionalEven` — the CostaRica colours re-spaced **evenly** SEA → 420 m, so equal");
+ sb.AppendLine("colour = equal height and nothing is emphasized. Final palette calibration waits for the");
+ sb.AppendLine("chosen curve profile. **Grayscale + the histograms are the honest instruments.**");
+ sb.AppendLine();
+ sb.AppendLine("## The oracle");
+ sb.AppendLine();
+ sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
+ sb.AppendLine($"**{(hardOk ? "ALL HARD CHECKS PASS" : "⚠⚠ HARD FAILURES — do not judge this batch")}**");
+ sb.AppendLine();
+ sb.AppendLine("Soft slope profile (e) — warn-and-report, exploration:");
+ sb.AppendLine();
+ sb.AppendLine(ShapingOracle.ToMarkdownTable(soft));
+ sb.AppendLine($"**The bookend, expected to FAIL (d):** {bookend.Detail}");
+ sb.AppendLine();
+ sb.AppendLine("## Disposability");
+ sb.AppendLine();
+ sb.AppendLine("| Artifact | Keep? |");
+ sb.AppendLine("|---|---|");
+ sb.AppendLine("| `relief.png`, `hist_*.png`, `INDEX.md` | **keep** — the judging plates and the finding |");
+ sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
+ sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code (it is the byte-level oracle) |");
+ sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
+ sb.AppendLine();
+ sb.AppendLine("## Results");
+ sb.AppendLine();
+ sb.AppendLine("| Folder | Seed | Variant | h min | h max | land % | grayscale range | time |");
+ sb.AppendLine("|---|---|---|---|---|---|---|---|");
+ foreach (string row in rows) sb.AppendLine(row);
+ sb.AppendLine();
+ sb.AppendLine($"Setup: MapSize {mapSize}, showpiece {showSize}, seeds {string.Join(", ", seeds)}, " +
+ $"knots re-measured on the task-01 pool. {WorldScale.Describe()}.");
+
+ 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 float EnvFloat(string k, float fallback)
+ => float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float,
+ System.Globalization.CultureInfo.InvariantCulture, out float 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/CurveContinuousTool.cs.uid b/Tools/Scripts/CurveContinuousTool.cs.uid
new file mode 100644
index 0000000..26a4ee4
--- /dev/null
+++ b/Tools/Scripts/CurveContinuousTool.cs.uid
@@ -0,0 +1 @@
+uid://dmnl1sp8vyioe
diff --git a/Tools/Scripts/Pass2Result.cs b/Tools/Scripts/Pass2Result.cs
index d5cbe7d..d0c7a20 100644
--- a/Tools/Scripts/Pass2Result.cs
+++ b/Tools/Scripts/Pass2Result.cs
@@ -58,6 +58,31 @@ namespace IslaApocalypse.Tools
/// Was the curve applied? The primary A/B gate.
public readonly bool CurveOn;
+ ///
+ /// Which curve shaped the render field: "off", "staircase", "continuous" or "lifted_WRONG".
+ /// A label, not logic — the oracle and the INDEX read it so a result can never be mistaken
+ /// for the wrong mode's.
+ ///
+ public readonly string CurveModeLabel;
+
+ ///
+ /// The batch variant this result belongs to, carried straight from
+ /// .
+ ///
+ /// ⚠ CARRIED, NOT INFERRED. The first cut of the chat2/02 tool reconstructed this from the
+ /// knob values ("ceiling > 31 ⇒ hold_higher"), which works only for exactly today's
+ /// variant set: add a second variant sharing a knob value and two different runs silently
+ /// write to one folder. The label is an identity, so it travels with the result.
+ ///
+ public readonly string VariantLabel;
+
+ ///
+ /// The per-seed continuous spline, when is "continuous" — the
+ /// oracle samples its slopes (check e) and the INDEX prints its control points. Null for
+ /// every other mode.
+ ///
+ public readonly ContinuousCurve Continuous;
+
/// Was shelf detail applied? Requires — it warps the curve's knots.
public readonly bool DetailOn;
@@ -90,7 +115,8 @@ namespace IslaApocalypse.Tools
public readonly List Notes;
public Pass2Result(int mapSize, int seed, float[,] height, float[,] heightClassify,
- bool curveOn, bool detailOn, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
+ bool curveOn, bool detailOn, string curveModeLabel, string variantLabel,
+ ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
List notes)
{
@@ -100,6 +126,9 @@ namespace IslaApocalypse.Tools
HeightClassify = heightClassify;
CurveOn = curveOn;
DetailOn = detailOn;
+ CurveModeLabel = curveModeLabel;
+ VariantLabel = variantLabel;
+ Continuous = continuous;
Knots = knots;
Anchors = anchors;
HMaxSeed = hMaxSeed;
diff --git a/Tools/Scripts/ReliefPalette.cs b/Tools/Scripts/ReliefPalette.cs
index e669106..48c4375 100644
--- a/Tools/Scripts/ReliefPalette.cs
+++ b/Tools/Scripts/ReliefPalette.cs
@@ -46,8 +46,11 @@ namespace IslaApocalypse.Tools
///
public static class ReliefPalette
{
- /// Named palettes. Gated so the look can be A/B'd like any other change.
- public enum Kind { Atlas, Dusk, CostaRica }
+ ///
+ /// Named palettes. Gated so the look can be A/B'd like any other change.
+ /// ⚠ ProvisionalEven is chat2/02's stopgap for the continuous curve — see its array's note.
+ ///
+ public enum Kind { Atlas, Dusk, CostaRica, ProvisionalEven }
///
/// Bathymetric compression constant, in raw height units. The sea ramp is indexed by
@@ -153,6 +156,29 @@ namespace IslaApocalypse.Tools
(1.000f, new Color(0.016f, 0.063f, 0.165f)), // abyss — flat
};
+ // ---- PROVISIONAL EVEN (chat2/02): the CostaRica colours, re-spaced evenly SEA → PEAK_CAP --
+ //
+ // ⚠⚠ PROVISIONAL, AND FLAGGED AS SUCH EVERYWHERE IT APPEARS. The continuous curve spreads
+ // land across the whole 0–420 m band, so the raw-percentile CostaRica stops no longer sit
+ // where the land is. This ramp is deliberately UNOPINIONATED: the same colour sequence at
+ // EVEN height spacing, so equal colour distance = equal height distance and nothing is
+ // emphasized. Final palette calibration waits until a curve profile is CHOSEN — placing
+ // stops on a distribution still being explored would bake taste into the instrument.
+ // Grayscale + the histogram stay the honest instruments; this is only so the relief plates
+ // are readable meanwhile.
+ private static readonly (float h, Color c)[] ProvisionalEvenLand = BuildProvisionalEven();
+
+ private static (float h, Color c)[] BuildProvisionalEven()
+ {
+ // SEA → PEAK_CAP through the single yardstick — no new literal for the cap.
+ float lo = 0.15f;
+ float hi = 0.15f + IslaApocalypse.Core.WorldScale.RawFromMetres(420f);
+ var stops = new (float h, Color c)[CostaRicaLand.Length];
+ for (int i = 0; i < CostaRicaLand.Length; i++)
+ stops[i] = (lo + (hi - lo) * i / (CostaRicaLand.Length - 1), CostaRicaLand[i].c);
+ return stops;
+ }
+
/// The hypsometric tint for a height, before any relief shading.
public static Color Tint(Kind kind, float height, float seaLevel)
{
@@ -169,9 +195,10 @@ namespace IslaApocalypse.Tools
private static ((float h, Color c)[] land, (float h, Color c)[] sea) Ramps(Kind kind) => kind switch
{
- Kind.Dusk => (DuskLand, DuskSea),
- Kind.CostaRica => (CostaRicaLand, CostaRicaSea),
- _ => (AtlasLand, AtlasSea),
+ Kind.Dusk => (DuskLand, DuskSea),
+ Kind.CostaRica => (CostaRicaLand, CostaRicaSea),
+ Kind.ProvisionalEven => (ProvisionalEvenLand, CostaRicaSea), // same sea; land re-spaced
+ _ => (AtlasLand, AtlasSea),
};
/// The land stops of a palette, for drawing a legend.
diff --git a/Tools/Scripts/Shaping.cs b/Tools/Scripts/Shaping.cs
index 844cae0..252e702 100644
--- a/Tools/Scripts/Shaping.cs
+++ b/Tools/Scripts/Shaping.cs
@@ -71,11 +71,21 @@ namespace IslaApocalypse.Tools
if (cfg.ShelfDetail)
notes.Add("[Shaping] shelf detail requested but the curve is off — no-op (detail warps the curve's knots).");
return new Pass2Result(mapSize, p1.Seed, p1.Height, p1.Height,
- curveOn: false, detailOn: false, knots: null, anchors: null, hMaxSeed: p1.HMaxSeed,
+ curveOn: false, detailOn: false, curveModeLabel: "off", variantLabel: cfg.VariantLabel, continuous: null,
+ knots: null, anchors: null, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: p1.HMinSeed, hMax: p1.HMaxSeed,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
}
+ // ═══ WHICH CURVE (chat2/02) ═══
+ //
+ // Staircase falls through to the faithful task-01 path below, UNTOUCHED — it is the
+ // control, and controls do not get refactored while they are being compared against.
+ if (cfg.CurveMode == CurveModeKind.Continuous)
+ return ShapeContinuous(p1, cfg, notes, t0);
+ if (cfg.CurveMode == CurveModeKind.LiftedWrong)
+ return ShapeLifted(p1, cfg, notes, t0);
+
CurveKnots knots = cfg.Knots;
CurveAnchors anchors = cfg.Anchors;
GenerationScale scale = cfg.Scale;
@@ -197,9 +207,128 @@ namespace IslaApocalypse.Tools
// property that makes it an oracle.
return new Pass2Result(mapSize, p1.Seed, height, classify,
- curveOn: true, detailOn: detailOn, knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
+ curveOn: true, detailOn: detailOn, curveModeLabel: "staircase", variantLabel: cfg.VariantLabel, continuous: null,
+ knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: edgeAmpRaw, maxEdgeShiftRaw: maxEdgeShiftRaw, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
}
+
+ ///
+ /// ⭐ THE CONTINUOUS MODE (chat2/02): the staircase's toe+red lowland preserved bit-for-bit,
+ /// everything above it replaced by one smooth monotone Fritsch–Carlson climb.
+ /// → for the shape and its refusals.
+ ///
+ /// ═══ ⚠ WHY THIS LOOP IS SO MUCH SIMPLER THAN THE STAIRCASE'S ═══
+ ///
+ /// No bench/plateau/strength modulation fields — the anchors they modulated are dropped.
+ /// No shelf detail — the flat benches it existed to de-slab no longer exist above the
+ /// lowlands, and the preserved lowland never needed it. Micro-relief/roughness on the
+ /// continuous upper grade is DEFERRED: erosion is the real detail source, and painting
+ /// noise on the climb now would pre-judge what erosion should carve. So: one spline per
+ /// seed, one Apply per column, and the classify field untouched as always.
+ ///
+ private static Pass2Result ShapeContinuous(Pass1Result p1, TerrainGenConfig cfg,
+ List notes, ulong t0)
+ {
+ int mapSize = p1.MapSize;
+ CurveKnots knots = cfg.Knots;
+ CurveAnchors anchors = cfg.Anchors;
+
+ // Same per-seed summit normalization as the staircase — the pass-1/pass-2 boundary
+ // stays hard for the same reason.
+ float spikeMax = HeightCurve.EffectiveSpikeMax(p1.HMaxSeed, knots, anchors);
+
+ // Build throws (refusing the generation) on any config that cannot hit the target
+ // silhouette; the tool's _Ready catches and Quit(2)s.
+ var curve = ContinuousCurve.Build(knots, anchors, spikeMax,
+ cfg.LowlandCeilingM, cfg.ClimbFeather, cfg.SummitDrama);
+
+ // Monotone by construction — and proven anyway, per seed, because "cannot fail" is
+ // exactly the claim worth a millisecond of checking.
+ notes.Add(curve.AssertStrictlyIncreasing());
+ notes.Add($"[Shaping] continuous: {curve.DescribeControlPoints()}");
+
+ if (cfg.ShelfDetail)
+ notes.Add("[Shaping] ⚠ shelf detail requested but FORCED OFF in continuous mode — the flat " +
+ "benches it de-slabbed no longer exist, and detail on the climb is deferred to erosion.");
+
+ var height = new float[mapSize, mapSize];
+ var classify = new float[mapSize, mapSize];
+ float hMin = float.MaxValue, hMax = float.MinValue;
+
+ for (int x = 0; x < mapSize; x++)
+ {
+ for (int y = 0; y < mapSize; y++)
+ {
+ float raw = p1.Height[x, y];
+ classify[x, y] = raw; // ⭐ the oracle field, as always
+
+ float h = curve.Apply(raw); // sea identity + lowland delegation
+ // + climb + tail, all inside
+ if (h < hMin) hMin = h;
+ if (h > hMax) hMax = h;
+ height[x, y] = h;
+ }
+ }
+
+ return new Pass2Result(mapSize, p1.Seed, height, classify,
+ curveOn: true, detailOn: false, curveModeLabel: "continuous", variantLabel: cfg.VariantLabel, continuous: curve,
+ knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
+ edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
+ elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
+ }
+
+ ///
+ /// ⚠⚠ THE DELIBERATELY-WRONG BOOKEND (chat2/02): an even linear remap of ALL land —
+ /// [SEA, spikeMax] → [SEA, PEAK_CAP]. Every land column above sea is lifted by the
+ /// same ×~1.5 factor, which hoists the entire island off its shoreline and destroys the
+ /// broad low plain the developer likes.
+ ///
+ /// It exists so the preserved-lowland variants can be seen AGAINST the mistake — the batch's
+ /// contrast anchor, per the rev-3 task. It is not a candidate, it takes no knobs, and its
+ /// oracle-(d) FAILURE is expected and reported as confirmation, not as a bug.
+ ///
+ private static Pass2Result ShapeLifted(Pass1Result p1, TerrainGenConfig cfg,
+ List notes, ulong t0)
+ {
+ int mapSize = p1.MapSize;
+ CurveKnots knots = cfg.Knots;
+ CurveAnchors anchors = cfg.Anchors;
+
+ float spikeMax = HeightCurve.EffectiveSpikeMax(p1.HMaxSeed, knots, anchors);
+ float scale = (anchors.PeakCap - anchors.Sea) / (spikeMax - anchors.Sea);
+
+ notes.Add($"[Shaping] ⚠⚠ LIFTED_WRONG: even remap [sea, {spikeMax:F4}] → [sea, {anchors.PeakCap:F4}] " +
+ $"(×{scale:F3} on every land height) — the WRONG direction, kept as the contrast bookend.");
+
+ var height = new float[mapSize, mapSize];
+ var classify = new float[mapSize, mapSize];
+ float hMin = float.MaxValue, hMax = float.MinValue;
+
+ for (int x = 0; x < mapSize; x++)
+ {
+ for (int y = 0; y < mapSize; y++)
+ {
+ float raw = p1.Height[x, y];
+ classify[x, y] = raw;
+
+ float h;
+ if (raw <= anchors.Sea) h = raw; // even the wrong direction keeps the
+ // coastline — sea identity is not optional
+ else if (raw >= spikeMax) h = anchors.PeakCap + (raw - spikeMax) * anchors.TailSlope;
+ else h = anchors.Sea + (raw - anchors.Sea) * scale;
+
+ if (h < hMin) hMin = h;
+ if (h > hMax) hMax = h;
+ height[x, y] = h;
+ }
+ }
+
+ return new Pass2Result(mapSize, p1.Seed, height, classify,
+ curveOn: true, detailOn: false, curveModeLabel: "lifted_WRONG", variantLabel: cfg.VariantLabel, continuous: null,
+ knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
+ edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
+ elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
+ }
}
}
diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs
index d626d28..e7a9527 100644
--- a/Tools/Scripts/ShapingOracle.cs
+++ b/Tools/Scripts/ShapingOracle.cs
@@ -224,6 +224,131 @@ namespace IslaApocalypse.Tools
return shares;
}
+ ///
+ /// A bit-regression against any `.f32` dump, with the caller naming the check — the
+ /// chat2/02 generalization of , used to hold the
+ /// staircase mode against task 01's own batch output. The same rule applies: a missing dump
+ /// is INCONCLUSIVE and counted as a failure, never as a pass.
+ ///
+ public static Check DumpRegression(string id, string name, float[,] current, float[,] dump,
+ int mapSize, string dumpPath)
+ {
+ var c = new Check { Id = id, Name = name };
+
+ if (dump == null)
+ {
+ c.Passed = false;
+ c.Detail = $"INCONCLUSIVE — no readable dump at {dumpPath} for this seed/size. Not counted as a pass.";
+ return c;
+ }
+
+ var (differing, firstDiff) = CompareBitwise(dump, current, mapSize);
+ c.Passed = differing == 0;
+ c.Detail = c.Passed
+ ? $"bit-identical to {dumpPath} over {(long)mapSize * mapSize:N0} cells"
+ : $"{differing:N0} cells differ from {dumpPath} — {firstDiff}";
+ return c;
+ }
+
+ ///
+ /// (d) ⭐ LOWLANDS PRESERVED — the rev-3 task's load-bearing check. For every cell whose RAW
+ /// height is at or below K2 (the toe+red band the developer likes), the continuous mode's
+ /// output must be BIT-IDENTICAL to the staircase's. This mechanically enforces "do not lift
+ /// the lowlands": the low pile cannot move if its every column is the same float.
+ ///
+ /// The identity is by construction — the continuous curve DELEGATES to the staircase's own
+ /// toe+red code path below K2 — and this check is what keeps that construction honest
+ /// against refactoring, float re-association, or a ceiling knob bug.
+ ///
+ /// ⚠ Run against the LIFTED_WRONG bookend this check is EXPECTED to fail — the caller
+ /// reports that failure as confirmation of the wrong direction, not as a defect.
+ ///
+ public static Check LowlandsPreserved(Pass1Result p1, Pass2Result staircase, Pass2Result variant)
+ {
+ var c = new Check { Id = "d", Name = $"lowlands (raw ≤ K2) bit-identical to staircase [{variant.VariantLabel}]" };
+
+ float k2 = staircase.Knots.K2;
+ long compared = 0, differing = 0;
+ string first = null;
+
+ for (int x = 0; x < p1.MapSize; x++)
+ {
+ for (int y = 0; y < p1.MapSize; y++)
+ {
+ if (p1.Height[x, y] > k2) continue;
+ compared++;
+ int ba = BitConverter.SingleToInt32Bits(staircase.Height[x, y]);
+ int bb = BitConverter.SingleToInt32Bits(variant.Height[x, y]);
+ if (ba == bb) continue;
+ differing++;
+ first ??= $"first at [{x},{y}] raw {p1.Height[x, y]:G9}: " +
+ $"staircase {staircase.Height[x, y]:G9} vs {variant.Height[x, y]:G9}";
+ }
+ }
+
+ c.Passed = differing == 0;
+ c.Detail = c.Passed
+ ? $"bit-identical over all {compared:N0} lowland cells (raw ≤ K2 = {k2:F6})"
+ : $"{differing:N0} of {compared:N0} lowland cells differ — {first}";
+ return c;
+ }
+
+ ///
+ /// (e) UPPER CONTINUOUS — sample the climb's slope densely; no near-flat anywhere (a bench
+ /// reborn), no cliff below the summit onset (the summit itself may steepen — that is the
+ /// dramatic peak). ⚠ SOFT: this is an exploration batch, so a violation warns loudly and
+ /// lands in the report rather than failing the run — the tool decides the exit code.
+ ///
+ public static Check UpperClimbProfile(Pass2Result p2)
+ {
+ var c = new Check { Id = "e", Name = $"upper climb continuous — no flats, no low-mid cliffs [{p2.VariantLabel}]" };
+
+ if (p2.Continuous == null)
+ {
+ c.Passed = false;
+ c.Detail = "no continuous spline on this result — wrong mode handed in.";
+ return c;
+ }
+
+ var (minN, minAt, maxN, maxAt, nearFlat, cliff) = p2.Continuous.SampleClimbSlopes();
+ c.Passed = !nearFlat && !cliff;
+ c.Detail = $"slope (normalized, 1 = climb average): min {minN:F3} at raw {minAt:F4}" +
+ $"{(nearFlat ? " ⚠ NEAR-FLAT (a bench reborn)" : "")}, " +
+ $"max below onset {maxN:F3} at raw {maxAt:F4}" +
+ $"{(cliff ? " ⚠ CLIFF below the summit onset" : "")}";
+ return c;
+ }
+
+ ///
+ /// (f) SEA IDENTITY — per cell, not per count: a column is land in the variant exactly when
+ /// it is land with the curve off. Counting alone could hide two errors that cancel; this
+ /// cannot. The coastline is the one thing every mode, including the wrong one, must keep.
+ ///
+ public static Check SeaIdentity(Pass2Result off, Pass2Result variant, float seaLevel)
+ {
+ var c = new Check { Id = "f", Name = $"sea identity — per-cell landness unchanged [{variant.VariantLabel}]" };
+
+ long mismatches = 0;
+ string first = null;
+ for (int x = 0; x < off.MapSize; x++)
+ {
+ for (int y = 0; y < off.MapSize; y++)
+ {
+ bool a = off.Height[x, y] >= seaLevel;
+ bool b = variant.Height[x, y] >= seaLevel;
+ if (a == b) continue;
+ mismatches++;
+ first ??= $"first at [{x},{y}]: off {off.Height[x, y]:G9} vs {variant.Height[x, y]:G9}";
+ }
+ }
+
+ c.Passed = mismatches == 0;
+ c.Detail = c.Passed
+ ? $"per-cell landness identical over {(long)off.MapSize * off.MapSize:N0} cells"
+ : $"{mismatches:N0} cells changed sides of the waterline — {first}";
+ return c;
+ }
+
/// Render the whole oracle as a markdown table for the INDEX and the report.
public static string ToMarkdownTable(IEnumerable checks)
{
diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs
index 9e67d9c..855840f 100644
--- a/Tools/Scripts/TerrainGenConfig.cs
+++ b/Tools/Scripts/TerrainGenConfig.cs
@@ -2,6 +2,22 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
+ ///
+ /// Which redistribution curve pass 2a applies (chat2/02). One seam, three occupants:
+ ///
+ /// Staircase ⭐ the faithful v5 port (task 01) — toe/red/riser/bench/riser/plateau/spike.
+ /// THE CONTROL. Bit-identical to task 01's output, always present in a batch.
+ /// Continuous ⭐ the rev-3 redesign — the staircase's toe+red lowland PRESERVED bit-for-bit,
+ /// everything above it replaced by one smooth monotone climb to the 420 m cap.
+ /// → Core/ContinuousCurve.
+ /// LiftedWrong ⚠⚠ DELIBERATELY THE WRONG DIRECTION — an even linear remap of ALL land onto
+ /// [SEA, PEAK_CAP], which lifts the entire island off its shoreline and destroys
+ /// the low plain the developer likes. It exists as a CONTRAST BOOKEND so the
+ /// preserved-lowland variants can be judged against the mistake, and for no other
+ /// purpose. Do not ship it, do not tune it, do not "fix" it.
+ ///
+ public enum CurveModeKind { Staircase, Continuous, LiftedWrong }
+
///
/// The generator's configuration, including the per-element ABLATION TOGGLES.
///
@@ -98,6 +114,44 @@ namespace IslaApocalypse.Tools
///
public bool Curve = true;
+ ///
+ /// ⭐ WHICH curve (chat2/02). → .
+ ///
+ /// Default CONTINUOUS per the rev-3 task — the exploration direction. Tools that exist to
+ /// reproduce the task-01 staircase (CurveBaselineTool) set Staircase EXPLICITLY, so the
+ /// default changing does not silently move a control batch.
+ ///
+ public CurveModeKind CurveMode = CurveModeKind.Continuous;
+
+ // ---- the continuous climb's knobs — ALL act above the lowland ceiling only ----
+
+ ///
+ /// How high the preserved lowland holds before the climb takes over, in METRES of output
+ /// height above sea. Default 30 = RED_CEIL, the flood line — the exact top of the
+ /// staircase's toe+red band, so nothing at all is re-mapped below it.
+ ///
+ /// ⚠ Constrained to [30, 80] m by : below 30 would cut
+ /// the preserved band; at ~100 it could preserve a flat bench, the artifact this mode
+ /// exists to remove. Raising it extends the red band's gentle grade linearly before the
+ /// climb begins. THE PRIMARY VARIANT AXIS.
+ ///
+ public float LowlandCeilingM = 30f;
+
+ ///
+ /// The shape of the climb's departure from the lowland, 0..1: how long it hugs the red
+ /// band's exit slope before steepening. Replaces the old ambiguous "bow". Acts only above
+ /// the ceiling; CANNOT touch the low band.
+ ///
+ public float ClimbFeather = 0.4f;
+
+ ///
+ /// The summit's steepening, ≥ 1: the secant slope of the top 15 % of the climb, in units of
+ /// the climb's average grade. 1 = a ramp (refused); 2.5 = the default pointed peak; higher =
+ /// more dramatic. The peak reads pointy, never a needle-on-a-hump — there is no plateau
+ /// under it any more.
+ ///
+ public float SummitDrama = 2.5f;
+
///
/// ⭐ Pass 2a rung 2: the shelf detail passes — micro-relief skin + shelf-edge knot warp.
/// ⚠ REQUIRES : the edge warp slides the CURVE's knots, so with no curve