using System;
namespace IslaApocalypse.Core
{
///
/// ⭐⭐ THE HEIGHT-REDISTRIBUTION CURVE — pass 2's first act, and the shape of the island's
/// elevation profile. Ported from REFERENCE:Tools/Scripts/HeightCurve.cs (v5) at tag
/// pre-rewrite-reference (ab78883). → D-050 ("port, don't re-derive").
///
/// ═══ WHAT IT IS FOR ═══
///
/// Raw fractal noise is Gaussian-ish: almost all land sits in a narrow mid-band and there is no
/// coastal plain, no shelf, no distinguishable summit. The curve REDISTRIBUTES that distribution
/// onto a designed elevation profile — a wide low plain, two shelves, risers between them, and a
/// thin summit band under a hard cap. It changes WHERE heights land, never WHICH pixel is higher
/// than which: the curve is strictly monotonic, so the terrain's topology is untouched.
///
/// ═══ THE SEVEN BANDS (input knot → output anchor) ═══
///
/// band input output shape
/// ─────────────── ─────────── ──────────────────────────── ──────────────────────────
/// toe / orange [SEA, K1) Sea → OrangeCeil 0.3u + 0.7·u(2−u) ease-out
/// red [K1, K2) Orange → RedCeil linear
/// foothill riser [K2, k3) RedCeil → benchLo 0.1u + 0.9·smoothstep
/// bench [k3, k4) benchLo → benchTop linear
/// mid riser [k4, k5) benchTop → plateauLo 0.1u + 0.9·smoothstep
/// plateau [k5, K6) plateauLo→ plateauTop linear
/// summit spike [K6, sMax) plateauTop → PeakCap 0.05u + 0.95·u⁴
/// tail [sMax, ∞) PeakCap + (h−sMax)·TailSlope linear
///
/// The riser and spike blends are the reference's "corner fixes" and are FROZEN: the 0.1 riser
/// floor makes climbs decelerate into shelves and accelerate out of them (no machined edges), and
/// the 0.05 spike floor lets the summit leave the plateau gently (no hard skirt under the peaks).
///
/// ═══ ⚠⚠ THE LOAD-BEARING LINE ═══
///
/// if (h <= a.Sea) return h;
///
/// THE CURVE IS IDENTITY AT AND BELOW SEA. Everything downstream rests on it: the waterline
/// cannot move, the Trench's ocean-border guarantee survives, and — when water lands — the
/// classify/render split agrees everywhere outside the crater, because a monotonic curve that
/// fixes sea means Apply(raw) < sea exactly when raw < sea. Delete this line
/// and the whole separability argument goes with it.
///
/// ═══ ⚠ SEED-DEPENDENT BY CONSTRUCTION ═══
///
/// The summit spike maps [K6, hMaxSeed] onto the peak band, so the curve cannot be
/// evaluated until pass 1 has scanned every pixel. That is why the pass-1/pass-2 boundary is a
/// hard one and not an interleave. → ,
/// Tools/Pass1Result.HMaxSeed.
///
/// ═══ PORT NOTES ═══
///
/// • Engine-free: the reference used Godot.Mathf only for arithmetic, so this lives in
/// Core/ as a named C++-candidate seam (D-049, D-060). reproduces
/// Mathf.Lerp's exact expression, so the port is bit-faithful and not merely equivalent.
/// • Anchors are a parameter object () rather than consts, so the
/// storm-ladder values are A/B-able from config. CurveAnchors.Default reproduces the
/// reference's constants exactly.
/// • The reference's two-preset machinery (COMPACT vs BALANCED) is NOT carried: COMPACT was
/// retired by the task-09 verdict and exists only in that task's report. One knot set, named.
/// • returns its confirmation line instead of printing it — Core
/// has no GD.Print. The caller logs it.
///
/// ⚠⚠ THE SHAPE IS FROZEN AT v5. This port adds no band, no anchor and no slope. Reshaping is a
/// later, gated task; if you are here to steepen something, you are in the wrong file.
///
public static class HeightCurve
{
/// Curve body version — the identity of the segment layout, not of the knots.
public const ushort Version = 5;
///
/// Mathf.Lerp, reproduced as the reference's engine wrote it:
/// from + (to - from) * weight. ⚠ Written out rather than "simplified" because a
/// different association of the same algebra is a different float32 result, and this port's
/// fidelity claim is bit-level.
///
private static float Lerp(float from, float to, float weight) => from + (to - from) * weight;
///
/// The per-seed summit ceiling: the raw height the spike band's top maps to PeakCap.
///
/// Max(hMaxSeed, K6 + SpikeMinSpan) — the floor guarantees a non-degenerate band on a
/// seed whose map-wide maximum lands at or below K6, which would otherwise divide by zero.
///
public static float EffectiveSpikeMax(float hMaxSeed, CurveKnots k, CurveAnchors a)
=> MathF.Max(hMaxSeed, k.K6 + a.SpikeMinSpan);
///
/// Shelf band width from the per-column strength field.
///
/// ⚠ THE LERP IS INVERTED, AND THAT IS THE REFERENCE'S INTENT: higher "strength" means a
/// MORE PRONOUNCED shelf, which means a NARROWER input band mapped across the same output
/// span — i.e. flatter ground. strength 0 → SpanMax, strength 1 → SpanMin.
///
public static float ShelfSpan(float strength01, CurveAnchors a)
=> Lerp(a.ShelfSpanMax, a.ShelfSpanMin, Math.Clamp(strength01, 0f, 1f));
///
/// The curve, for ONE column.
///
/// Raw pre-curve height.
/// The map-wide raw maximum for this seed. → Pass1Result.HMaxSeed.
/// This column's bench floor (base ± the anchor field).
/// This column's bench output span.
/// This column's plateau floor.
/// This column's plateau output span.
/// The input knot set.
/// The output anchors.
///
/// The shelf-edge warp (TerrainDetailPass pass B): slides the K3/K4/K5 BLOCK for this
/// column. K1/K2/K6 never move, which is what keeps the red-ceiling floor and the peak cap
/// EXACT under the warp rather than statistical. Zero when detail is off.
///
public static float Apply(float h, float hMaxSeed,
float benchLo, float benchSpan, float plateauLo, float plateauSpan,
CurveKnots k, CurveAnchors a, float edgeShift)
{
// ⭐ IDENTITY AT AND BELOW SEA. See the type header — this line is the invariant.
if (h <= a.Sea) return h;
// The knot BLOCK slides rigidly: bench and mid-riser keep their exact widths (their
// interiors are translated, not distorted); only the foothill riser and the plateau
// stretch or compress to absorb the shift.
float k3 = k.K3 + edgeShift, k4 = k.K4 + edgeShift, k5 = k.K5 + edgeShift;
float u, s;
if (h < k.K1)
{
u = (h - a.Sea) / (k.K1 - a.Sea);
s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
return a.Sea + s * (a.OrangeCeil - a.Sea);
}
if (h < k.K2)
{
u = (h - k.K1) / (k.K2 - k.K1);
return a.OrangeCeil + u * (a.RedCeil - a.OrangeCeil); // 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 a.RedCeil + s * (benchLo - a.RedCeil);
}
if (h < k4)
{
u = (h - k3) / (k4 - k3);
return benchLo + u * benchSpan; // bench — corner fix 3 floors the span
}
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, a);
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 * (a.PeakCap - plateauTop);
}
return a.PeakCap + (h - spikeMax) * a.TailSlope; // gentle tail, not a clip
}
///
/// Per-generation numeric strict-monotonicity proof of the EFFECTIVE curve — run once per
/// seed, between the two passes, before any pixel is curved.
///
/// ═══ WHY A NUMERIC SWEEP AND NOT AN ARGUMENT ═══
///
/// Monotonicity is structural in the algebra, but the curve as EVALUATED depends on three
/// per-column fields and a per-column warp, and the corner fixes lowered the slope floors
/// (risers 0.1, spike base 0.05) while the warp squeezes the foothill riser and the plateau.
/// "It should be fine" is not the standard: the sweep proves every slope stays strictly
/// positive at the extremes of BOTH, on this seed's actual spikeMax.
///
/// 24 corners: 2 bench extremes × 2 plateau extremes × 2 span extremes × 3 edge shifts
/// (−max, 0, +max).
///
/// ⚠ THROWS AND REFUSES on violation, rather than warning. A non-monotonic curve inverts
/// terrain — a peak becomes a pit — and that is not something to discover in a render.
///
/// A one-line confirmation for the run log. Core cannot print; the caller does.
public static string AssertMonotonic(float hMaxSeed, CurveKnots k, CurveAnchors a, float maxEdgeShift)
{
if (!k.IsStrictlyOrdered)
throw new InvalidOperationException(
$"[HeightCurve] KNOT ORDER VIOLATION: {k} is not strictly ascending. Refusing to generate.");
if (maxEdgeShift < 0f || k.K2 + maxEdgeShift >= k.K3 || k.K5 + maxEdgeShift >= k.K6)
throw new 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 = { a.BenchBase - a.BenchAmp, a.BenchBase + a.BenchAmp };
float[] plateauLos = { a.PlateauBase - a.PlateauAmp, a.PlateauBase + a.PlateauAmp };
float[] spans = { a.ShelfSpanMin, a.ShelfSpanMax };
float[] edgeShifts = maxEdgeShift > 0f
? new[] { -maxEdgeShift, 0f, maxEdgeShift }
: new[] { 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, a, es);
void Check(double hd)
{
float h = (float)hd;
// Dedupe float32 samples: a fine double-precision step can land on the same
// float twice, and "not greater" is not a violation when it is the same input.
if (h <= prevH) return;
float v = Apply(h, hMaxSeed, bl, sp, pl, sp, k, a, es);
if (v <= prev)
throw new 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 = Math.Max(2.0, EffectiveSpikeMax(hMaxSeed, k, a) + 0.5);
for (double hh = -7.0 + 0.01; hh < 0.10; hh += 0.01) Check(hh); // the below-sea identity run
for (double hh = 0.10; hh <= top; hh += 0.0001) Check(hh); // every band, finely
for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh); // the tail
}
return $"[HeightCurve] Monotonicity assertion passed (v{Version} preset '{k.Name}', " +
$"8 modulation corners × edge shifts ±{maxEdgeShift:F6}, " +
$"effective spikeMax {EffectiveSpikeMax(hMaxSeed, k, a):F6}).";
}
}
}