using Godot;
///
/// The height-redistribution curve, v3 — the TERRACED ASCENT (terrain-water task 07).
/// Pure, static, monotonic piecewise map over raw blueprint heights (D-035: numbers
/// in, numbers out; the per-seed spike maximum is an explicit PARAMETER).
///
/// v3 (developer's task-06 ground-test verdict — floor and 420 m ceiling frozen, the
/// ascent between them rebuilt): v2 spent ~87 % of the vertical budget in the last
/// ~4 % of input, reading as flat-then-wall. v3 climbs in TERRACES — two benches,
/// three risers — and relocates the white mountain-town plateau from 50 m to 220 m:
///
/// h ≤ sea (0.15) identity — water and the below-sea world untouched
/// sea → K1 toe, ease-out — the lowlands (orange coverage 59 %)
/// K1 → K2 linear rise into the red band (storm anchors frozen)
/// K2 → K3 FOOTHILL RISER — smoothstep climb to the 100 m bench
/// K3 → K4 BENCH 1 — near-flat walkable foothill shelf (~2 m rise)
/// K4 → K5 MID RISER — the big middle climb to the white plateau
/// K5 → K6 WHITE PLATEAU — near-flat shelf at 220 m (mountain
/// towns / snow band, where it was always intended)
/// K6 → spikeMax SUMMIT SPIKE — per-seed normalized (hMaxSeed), stiff
/// ease-in kept from v2: spires off the plateau to 420 m
/// h > spikeMax linear tail (strict monotonicity, no clamp)
///
/// Input knots recalibrated 2026-08-08 from the SAME pooled batch-04 flat-sea land
/// CDF as v1/v2 (340,618,126 samples): P59/P72/P82/P87/P95/P98, giving pooled land
/// fractions orange 59 / red 13 / foothill-riser 10 / bench 5 / mid-riser 8 /
/// plateau 3 / spike 2 (exact by construction). Per-seed proportions vary — the
/// wrinkle variety.
///
/// Strictly monotonic (every segment's normalized slope bounded below by a positive
/// constant); asserted numerically per generation against the EFFECTIVE per-seed
/// curve once hMaxSeed is known.
///
public static class HeightCurve
{
public const ushort VERSION = 3;
// Input knots — v3 calibration (see class header). K1..K4 are serialized in
// TCRV's four knot slots; K5/K6 are version constants documented in
// BLUEPRINT_FORMAT.md (the TCRV version byte selects the anchor set).
public const float K1 = 0.509179f; // P59 — orange coverage boundary
public const float K2 = 0.604081f; // P72 — red top
public const float K3 = 0.698485f; // P82 — foothill riser top
public const float K4 = 0.767213f; // P87 — bench 1 top
public const float K5 = 0.930304f; // P95 — mid riser top
public const float K6 = 1.050720f; // P98 — plateau top / spike foot
// Output anchors — storm ladder (frozen) + the terraces.
public const float SEA = 0.15f;
public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling (frozen)
public const float RED_CEIL = 0.27f; // biblical ceiling (frozen)
public const float FOOTHILL_BENCH = SEA + 100f / 251f; // ≈ 0.54841 (100 m above sea)
public const float BENCH_STEP = 0.008f; // ~2 m rise across each bench
public const float FOOTHILL_TOP = FOOTHILL_BENCH + BENCH_STEP;
public const float PLATEAU = SEA + 220f / 251f; // ≈ 1.02649 (220 m — the white plateau)
public const float PLATEAU_TOP = PLATEAU + BENCH_STEP;
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 (frozen from v2)
public const float TAIL_SLOPE = 0.25f;
// Degenerate/near-flat guard (unchanged from v2): spike domain floored at
// K6 + SPIKE_MIN_SPAN so it stays positive and monotonic on any input.
public const float SPIKE_MIN_SPAN = 0.01f;
public static float EffectiveSpikeMax(float hMaxSeed)
{
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN);
}
/// Raw pre-curve height.
/// The seed's raw pre-curve maximum — per-seed spike normalizer.
public static float Apply(float h, float hMaxSeed)
{
if (h <= SEA) return h;
float u, s;
if (h < K1)
{
u = (h - SEA) / (K1 - SEA);
s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out toe, slope ≥ 0.3
return SEA + s * (ORANGE_CEIL - SEA);
}
if (h < K2)
{
u = (h - K1) / (K2 - K1);
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear rise
}
if (h < K3)
{
u = (h - K2) / (K3 - K2);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser, slope ≥ 0.2
return RED_CEIL + s * (FOOTHILL_BENCH - RED_CEIL);
}
if (h < K4)
{
u = (h - K3) / (K4 - K3);
return FOOTHILL_BENCH + u * BENCH_STEP; // bench 1, near-flat
}
if (h < K5)
{
u = (h - K4) / (K5 - K4);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser, slope ≥ 0.2
return FOOTHILL_TOP + s * (PLATEAU - FOOTHILL_TOP);
}
if (h < K6)
{
u = (h - K5) / (K6 - K5);
return PLATEAU + u * BENCH_STEP; // white plateau, near-flat
}
float spikeMax = EffectiveSpikeMax(hMaxSeed);
if (h < spikeMax)
{
u = (h - K6) / (spikeMax - K6);
s = 0.1f * u + 0.9f * (u * u * u * u); // summit spike (v2 shape kept), slope ≥ 0.1
return PLATEAU_TOP + s * (PEAK_CAP - PLATEAU_TOP);
}
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
}
///
/// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once
/// per generation after hMaxSeed is known. Loud throw, refuses to generate.
///
public static void AssertMonotonic(float hMaxSeed)
{
float prevH = -7f;
float prev = Apply(prevH, hMaxSeed);
// Only strictly increasing float32 samples are compared (task-05 incident fix).
void Check(double hd)
{
float h = (float)hd;
if (h <= prevH) return;
float v = Apply(h, hMaxSeed);
if (v <= prev)
throw new System.InvalidOperationException(
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}): {v} <= {prev}. Refusing to generate.");
prev = v;
prevH = h;
}
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h);
for (double h = 0.10; h <= top; h += 0.0001) Check(h);
for (double h = top + 0.05; h <= top + 6.0; h += 0.05) Check(h);
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
}
}