islaApocalypse/Tools/Scripts/HeightCurve.cs
beezm 4d2e024b2f feat: curve v4 — spatially modulated shelves (terrain-water task 08)
Per the task-07 gate: the terraces work but uniform anchors put a flat
ring at exactly 100 m and 220 m on every mountain. v4 keeps v3's
structure (frozen toe/rise, three risers, per-seed u4 spike to 420 m)
and turns the shelf anchors into smooth spatial fields: bench
100+-12 m and plateau 220+-20 m via two decorrelated very-low-freq
Simplex fields (~3 undulations per island width), plus a strength
field (~5/island) blending each shelf's output span between ~2 m
(pronounced flat) and ~25 m (barely a hint) — the optional strength
modulation shipped, ordering-safe by construction (worst-case bench
top 0.6962 < plateau min 0.9468). Field seeds derive from the RESOLVED
noise seed + fixed offsets (7101/7207/7303) — no config knob. Apply is
per-column with all modulated values as pure parameters (D-035); the
classify path never sees them.

Monotonicity assertion now sweeps all 8 modulation-extreme corners x
per-seed spikeMax. TerrainCurve gate "off"|"v4" (default v4);
v1-v3 retired loudly. TCRV extended (+36 B when version>=4:
amplitudes, span range, frequencies, seed offsets — length-framed
section makes the layout change safe; parser dispatches on the curve
version byte; harness compares the extension).

Note: MapGenerator.cs also carries the developer's editor whitespace
normalization (spaces->tabs, git diff -w empty before this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 03:30:27 -04:00

179 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Godot;
/// <summary>
/// The height-redistribution curve, v4 — SPATIALLY MODULATED SHELVES (terrain-water
/// task 08). Pure, static, monotonic piecewise map; every per-column input is an
/// explicit PARAMETER (D-035): the seed spike max and the four modulated shelf
/// values. The generator samples the modulation fields; this class never touches
/// noise.
///
/// v4 (developer's task-07 gate finding: the terraces work, but uniform anchors put
/// a flat ring at exactly 100 m and exactly 220 m on every mountain — the bathtub
/// rings): the bench and plateau OUTPUT anchors become smooth spatial fields —
/// benchLo = 100 m ± 12 m and plateauLo = 220 m ± 20 m via two decorrelated
/// very-low-frequency noise fields — and a third field modulates SHELF STRENGTH,
/// blending each shelf between "pronounced flat" (output span ~2 m) and "barely a
/// hint" (~25 m of gentle slope), so not every flank at shelf height develops the
/// full terrace. Shelves stay locally flat; the two magic altitudes stop existing.
///
/// Structure otherwise v3's, unchanged: identity ≤ sea, frozen toe/rise (storm
/// ladder), foothill riser → bench → mid riser → plateau → per-seed-normalized
/// stiff spike (u⁴, 420 m cap) → tail. Input knots are v3's calibration.
///
/// ORDERING SAFETY BY CONSTRUCTION (asserted): with the amplitudes below, at every
/// column: red ceiling 0.27 < benchLo… (bench min 0.5006), bench top max 0.6962 <
/// plateauLo min 0.9468, plateau top max 1.2062 < peak cap 1.8287. The numeric
/// assertion additionally sweeps all 8 modulation-extreme corners per generation.
/// </summary>
public static class HeightCurve
{
public const ushort VERSION = 4;
// Input knots — v3 calibration (pooled batch-04 land CDF, P59/72/82/87/95/98).
public const float K1 = 0.509179f;
public const float K2 = 0.604081f;
public const float K3 = 0.698485f;
public const float K4 = 0.767213f;
public const float K5 = 0.930304f;
public const float K6 = 1.050720f;
// Fixed output anchors — storm ladder + ceiling (frozen).
public const float SEA = 0.15f;
public const float ORANGE_CEIL = 0.206f;
public const float RED_CEIL = 0.27f;
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869
public const float TAIL_SLOPE = 0.25f;
public const float SPIKE_MIN_SPAN = 0.01f;
// Modulated shelf anchors: base ± amplitude (raw units; 251 m per unit).
public const float BENCH_BASE = SEA + 100f / 251f; // ≈ 0.54841 (100 m)
public const float BENCH_AMP = 12f / 251f; // ± 12 m
public const float PLATEAU_BASE = SEA + 220f / 251f; // ≈ 1.02649 (220 m)
public const float PLATEAU_AMP = 20f / 251f; // ± 20 m
// Shelf strength: output span of each shelf segment, blended by the strength
// field. Strong (t=1) → SPAN_MIN (~2 m, pronounced flat). Weak (t=0) →
// SPAN_MAX (~25 m, barely a hint of a shelf).
public const float SHELF_SPAN_MIN = 0.008f;
public const float SHELF_SPAN_MAX = 0.10f;
// Modulation-field derivation (generator-side, recorded in TCRV): field seed =
// RESOLVED WorldSeed + offset; frequency = (periods per island width) / MapSize.
public const int BENCH_SEED_OFFSET = 7101;
public const int PLATEAU_SEED_OFFSET = 7207;
public const int STRENGTH_SEED_OFFSET = 7303;
public const float ELEV_FREQ_ISLANDS = 3.0f; // ~3 undulations across the island
public const float STRENGTH_FREQ_ISLANDS = 5.0f; // finer patchiness for shelf strength
public static float EffectiveSpikeMax(float hMaxSeed)
{
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN);
}
/// <summary>Shelf output span for a strength sample t ∈ [0,1].</summary>
public static float ShelfSpan(float strength01)
{
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
}
/// <param name="h">Raw pre-curve height.</param>
/// <param name="hMaxSeed">Seed's raw pre-curve maximum (per-seed spike normalizer).</param>
/// <param name="benchLo">This column's bench anchor (BENCH_BASE ± BENCH_AMP).</param>
/// <param name="benchSpan">This column's bench output span (ShelfSpan of the strength field).</param>
/// <param name="plateauLo">This column's plateau anchor (PLATEAU_BASE ± PLATEAU_AMP).</param>
/// <param name="plateauSpan">This column's plateau output span.</param>
public static float Apply(float h, float hMaxSeed,
float benchLo, float benchSpan, float plateauLo, float plateauSpan)
{
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)); // frozen ease-out toe
return SEA + s * (ORANGE_CEIL - SEA);
}
if (h < K2)
{
u = (h - K1) / (K2 - K1);
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
}
if (h < K3)
{
u = (h - K2) / (K3 - K2);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser
return RED_CEIL + s * (benchLo - RED_CEIL);
}
if (h < K4)
{
u = (h - K3) / (K4 - K3);
return benchLo + u * benchSpan; // bench — modulated
}
float benchTop = benchLo + benchSpan;
if (h < K5)
{
u = (h - K4) / (K5 - K4);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser
return benchTop + s * (plateauLo - benchTop);
}
if (h < K6)
{
u = (h - K5) / (K6 - K5);
return plateauLo + u * plateauSpan; // plateau — modulated
}
float plateauTop = plateauLo + plateauSpan;
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/v3 shape)
return plateauTop + s * (PEAK_CAP - plateauTop);
}
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
}
/// <summary>
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve:
/// sweeps the full domain at every one of the 8 modulation-extreme corners
/// (bench anchor ±, plateau anchor ±, strength min/max) with the per-seed
/// spikeMax — the adversarial corner set for the ordering constraints. Loud
/// throw, refuses to generate.
/// </summary>
public static void AssertMonotonic(float hMaxSeed)
{
float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP };
float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP };
float[] spans = { SHELF_SPAN_MIN, SHELF_SPAN_MAX };
foreach (float bl in benchLos)
{
foreach (float pl in plateauLos)
{
foreach (float sp in spans)
{
float prevH = -7f;
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp);
void Check(double hd)
{
float h = (float)hd;
if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
float v = Apply(h, hMaxSeed, bl, sp, pl, sp);
if (v <= prev)
throw new System.InvalidOperationException(
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
prev = v;
prevH = h;
}
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
for (double hh = -7.0 + 0.01; hh < 0.10; hh += 0.01) Check(hh);
for (double hh = 0.10; hh <= top; hh += 0.0001) Check(hh);
for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh);
}
}
}
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
}
}