islaApocalypse/Tools/Scripts/HeightCurve.cs
beezm 492b56a87c feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10)
PASS B, replacing the reverted incision. The developer's sketch asked for
the shelf/riser BOUNDARY to be organic, not for water: notches, coves and
small peninsulas where a flat shelf meets its riser, instead of the clean
oval contour the curve produces.

Mechanism (the task's preferred "boundary warp", in its most
monotonic-safe form): every shelf/riser boundary is the contour where the
raw height crosses K3, K4 or K5, so the boundary is warped by SLIDING
THOSE THREE KNOTS per column -- edgeShift = simplex(resolvedSeed + 7507,
12 per island width) x ShelfEdgeVariation. The contours then wander in and
out of the terrain instead of tracing an iso-height line. Noise-warped by
construction, so there is no grid direction for an artifact to line up on
-- the failure mode of the thing this replaces.

Why slide knots rather than perturb a weight or the input height:
monotonicity becomes structural instead of conditional. The curve is
strictly monotonic for ANY ordered knot set, so no derivative bound, no
amplitude-vs-feather-width tuning, no way for a dial to invert a column.
MaxEdgeShift keeps the set ordered (half the smallest margin to a fixed
knot = 12.3 m of input height for the v5 knots); the config dial is
clamped to it, loudly. AssertMonotonic now sweeps 24 corners -- the 8
modulation extremes x {-max, 0, +max} shift -- and checks the bound first.

K1, K2 and K6 never move, which buys the guarantees exactly rather than
statistically: below K2 and above K6 a warped column is bit-identical to
an unwarped one, so the red ceiling still floors every shelf edge (storm
ladder safe), the 420 m cap still caps, and the toe and summit spikes are
untouched. The block slides rigidly, so the bench and mid-riser keep their
exact widths -- shelf interiors stay flat, riser interiors keep their
profile, and only the foothill riser and plateau stretch to absorb it.
The micro-relief mask takes the same shift, so pass A's skin follows the
shelf wherever pass B moved its edge.

Dial ShelfEdgeVariation (default 5 m of INPUT height -- a boundary
displacement, not an elevation change) under the existing TerrainDetail
gate; both passes stay one judged unit. TDTL v2 body records relief and
edge amp/frequency/seed-offset plus the applied clamp bound; writer,
parser and harness follow. No blueprint was written between the revert
and this commit, so v2 only ever means relief + edge warp on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:31:41 -04:00

219 lines
9.1 KiB
C#
Raw Permalink 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>
/// One preset's input knots for the v5 curve. Two presets exist for the task-09
/// taste batch — COMPACT (maximum lowland, cordillera-from-plains) and BALANCED
/// (the task-07/08 lineage, gradual highland approach). The loser retires after
/// the developer's gate; the winner becomes plain "v5".
/// </summary>
public sealed class CurveKnots
{
public readonly byte PresetId;
public readonly string Name;
public readonly float K1, K2, K3, K4, K5, K6;
public CurveKnots(byte id, string name, float k1, float k2, float k3, float k4, float k5, float k6)
{
PresetId = id; Name = name;
K1 = k1; K2 = k2; K3 = k3; K4 = k4; K5 = k5; K6 = k6;
}
}
/// <summary>
/// The height-redistribution curve, v5 (terrain-water task 09) — v4's spatially
/// modulated shelves plus the THREE CORNER FIXES, with PRESET-PARAMETERIZED knots.
/// Pure, static, monotonic; every per-column input and the knot set are explicit
/// PARAMETERS (D-035).
///
/// The corner fixes (both presets — the naturalness work):
/// 1. Riser endpoint slope floor 0.2 → 0.1 (blend 0.1u + 0.9·smoothstep): climbs
/// decelerate into shelves and accelerate out of them — no machined edges.
/// 2. Summit-spike base floor 0.1 → 0.05 (blend 0.05u + 0.95·u⁴): the spike
/// leaves the plateau gently — no hard skirt under the peaks.
/// 3. SHELF_SPAN_MIN 2 m → 6 m: pronounced shelves keep a gentle tilt — flat to
/// build on, never snooker-table flat.
///
/// Presets (input land-fraction targets; knots calibrated 2026-08-08 from the same
/// pooled batch-04 flat-sea land CDF as tasks 05/07, 340,618,126 samples; achieved
/// fractions exact by construction):
/// COMPACT — 65/12/7/4/6/3/3 (orange/red/foothill-riser/bench/mid-riser/plateau/spike)
/// BALANCED — 60/13/10/5/8/3/1
///
/// Unchanged from v4: storm-ladder anchors, bench 100±12 m, plateau 220±20 m,
/// strength modulation (span max 25 m), 420 m cap, per-seed spike normalization,
/// modulation fields/seed offsets, the classify-map invariant.
///
/// The curve SHAPE is frozen at v5. Task 10 adds no band, anchor or slope — only
/// a per-column `edgeShift` parameter on Apply, which slides the shelf/riser knot
/// block K3/K4/K5 so those three boundaries stop being clean iso-height contours.
/// It is a new input to the same curve, not a new curve.
/// </summary>
public static class HeightCurve
{
public const ushort VERSION = 5;
// The task-09 taste gate's WINNER: BALANCED (id 2). COMPACT retired with the
// verdict; its knots survive only in the task-09 report/batch for the record.
public static readonly CurveKnots V5 = new CurveKnots(2, "balanced",
0.515899f, 0.612157f, 0.710472f, 0.784045f, 0.962922f, 1.119118f); // P60/73/83/88/96/99
// 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;
public const float TAIL_SLOPE = 0.25f;
public const float SPIKE_MIN_SPAN = 0.01f;
// Modulated shelf anchors (unchanged from v4).
public const float BENCH_BASE = SEA + 100f / 251f;
public const float BENCH_AMP = 12f / 251f;
public const float PLATEAU_BASE = SEA + 220f / 251f;
public const float PLATEAU_AMP = 20f / 251f;
// Corner fix 3: pronounced shelves keep ~6 m of tilt across the shelf band.
public const float SHELF_SPAN_MIN = 6f / 251f; // ≈ 0.0239 (v4: 0.008 ≈ 2 m)
public const float SHELF_SPAN_MAX = 0.10f;
// Modulation-field derivation (unchanged from v4).
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;
public const float STRENGTH_FREQ_ISLANDS = 5.0f;
public static float EffectiveSpikeMax(float hMaxSeed, CurveKnots k)
{
return Mathf.Max(hMaxSeed, k.K6 + SPIKE_MIN_SPAN);
}
public static float ShelfSpan(float strength01)
{
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
}
/// <summary>
/// The curve for ONE column. <paramref name="edgeShift"/> (task 10 pass B) slides
/// the shelf/riser knot BLOCK — K3/K4/K5 — up or down by a per-column amount,
/// leaving K1/K2/K6 fixed. Every shelf↔riser boundary is the contour where the
/// raw height crosses one of those three knots, so shifting them makes those
/// contours wander instead of tracing a clean iso-height line: the shelf edge
/// scallops. Because the block moves rigidly, the bench and mid-riser bands keep
/// their exact widths (their interior shapes are translated, not distorted); only
/// the foothill riser and the plateau stretch or compress to absorb the shift.
/// Monotonicity is structural, not conditional — the curve is monotonic for ANY
/// strictly ordered knot set, and TerrainDetailPass.MaxEdgeShift keeps the set
/// ordered by construction. Below K2 and above K6 the output is bit-identical to
/// an unwarped column, which is what makes the red-ceiling floor and the 420 m
/// peak cap exact under the warp.
/// </summary>
public static float Apply(float h, float hMaxSeed,
float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k,
float edgeShift)
{
if (h <= SEA) return h;
float k3 = k.K3 + edgeShift, k4 = k.K4 + edgeShift, k5 = k.K5 + edgeShift;
float u, s;
if (h < k.K1)
{
u = (h - SEA) / (k.K1 - SEA);
s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
return SEA + s * (ORANGE_CEIL - SEA);
}
if (h < k.K2)
{
u = (h - k.K1) / (k.K2 - k.K1);
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
}
if (h < k3)
{
u = (h - k.K2) / (k3 - k.K2);
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // foothill riser — corner fix 1
return RED_CEIL + s * (benchLo - RED_CEIL);
}
if (h < k4)
{
u = (h - k3) / (k4 - k3);
return benchLo + u * benchSpan; // bench (min span 6 m — fix 3)
}
float benchTop = benchLo + benchSpan;
if (h < k5)
{
u = (h - k4) / (k5 - k4);
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // mid riser — corner fix 1
return benchTop + s * (plateauLo - benchTop);
}
if (h < k.K6)
{
u = (h - k5) / (k.K6 - k5);
return plateauLo + u * plateauSpan; // plateau
}
float plateauTop = plateauLo + plateauSpan;
float spikeMax = EffectiveSpikeMax(hMaxSeed, k);
if (h < spikeMax)
{
u = (h - k.K6) / (spikeMax - k.K6);
s = 0.05f * u + 0.95f * (u * u * u * u); // summit spike — corner fix 2
return plateauTop + s * (PEAK_CAP - plateauTop);
}
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
}
/// <summary>
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve for
/// the selected preset: all 8 modulation-extreme corners × the shelf-edge warp
/// extremes (±maxEdgeShift and 0) × per-seed spikeMax — 24 corners. The corner
/// fixes lower the slope floors (risers 0.1, spike base 0.05) and the warp
/// squeezes the foothill riser and the plateau; the sweep proves every slope
/// stays strictly positive at the extremes of both. Also checks the knot set
/// itself stays strictly ordered under the warp. Loud throw on failure.
/// </summary>
public static void AssertMonotonic(float hMaxSeed, CurveKnots k, float maxEdgeShift)
{
if (maxEdgeShift < 0f || k.K2 + maxEdgeShift >= k.K3 || k.K5 + maxEdgeShift >= k.K6)
throw new System.InvalidOperationException(
$"[HeightCurve] EDGE-SHIFT BOUND VIOLATION: maxEdgeShift={maxEdgeShift} does not keep K2<K3±d and K5±d<K6 (preset {k.Name}). Refusing to generate.");
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 };
float[] edgeShifts = maxEdgeShift > 0f
? new float[] { -maxEdgeShift, 0f, maxEdgeShift }
: new float[] { 0f };
foreach (float bl in benchLos)
{
foreach (float pl in plateauLos)
{
foreach (float sp in spans)
{
foreach (float es in edgeShifts)
{
float prevH = -7f;
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k, es);
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, k, es);
if (v <= prev)
throw new System.InvalidOperationException(
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}, edgeShift={es}): {v} <= {prev}. Refusing to generate.");
prev = v;
prevH = h;
}
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed, k) + 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} preset '{k.Name}', 8 modulation corners × edge shifts ±{maxEdgeShift:F6}, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6}).");
}
}