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;
///
/// ⭐ The measured calibration this climb was shaped from (chat2/03), or null when the climb
/// came from chat2/02's ANALYTIC feather/drama points.
///
/// Non-null is the current default: "the staircase's mountain with the terraces melted out".
/// Null survives so the 02 curve stays reproducible as a contrast variant — it is the "before"
/// in the three-way histogram story, not a fallback.
///
public readonly ClimbCalibration Calibration;
/// Where the summit begins, normalized — the calibration's onset when calibrated, else the constant.
public float EffectiveSummitOnset => Calibration?.SummitOnsetU ?? SummitOnset;
// 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, ClimbCalibration calibration = null)
{
Knots = k; Anchors = a;
CeilingRaw = ceilingRaw; CeilingOut = ceilingOut; SpikeMax = spikeMax;
JoinSlopeRaw = joinSlopeRaw;
LowlandCeilingM = lowlandCeilingM; ClimbFeather = climbFeather; SummitDrama = summitDrama;
_x = x; _y = y; _m = m; Calibration = calibration;
}
///
/// ⭐⭐ THE CALIBRATED CLIMB (chat2/03) — control points MEASURED from the staircase's
/// above-ceiling elevation distribution rather than invented from two shape knobs.
/// → for the method and for the chat2/02 mistake it corrects.
///
/// Everything outside the climb is identical to : the same lowland
/// handover pinned to the exact anchors, the same C¹ join to the red band's exit slope, the
/// same per-seed , the same tail. Only the interior shape changes.
///
/// ⚠ THIS PATH DOES NOT REQUIRE STRICTLY-INCREASING SECANTS, and that is deliberate. The 02
/// analytic path enforced a convex control polygon as its no-magnet rule. A curve calibrated
/// to real terrain is WAVY — gentler where the staircase had a bench, steeper through its
/// risers — so convexity is the wrong invariant here. The no-magnet guarantee instead comes
/// from , which floors every grade: the
/// curve may slow down, but never to a bench.
///
public static ContinuousCurve BuildCalibrated(CurveKnots k, CurveAnchors a, float spikeMax,
float lowlandCeilingM, ClimbCalibration calibration)
{
if (calibration == null) throw new ArgumentNullException(nameof(calibration));
var (ceilingRaw, ceilingOut, redSlope) = ResolveHandover(k, a, lowlandCeilingM);
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.");
float spanRaw = spikeMax - ceilingRaw;
float spanOut = a.PeakCap - ceilingOut;
int n = calibration.U.Length;
var x = new float[n];
var y = new float[n];
for (int i = 0; i < n; i++)
{
x[i] = ceilingRaw + calibration.U[i] * spanRaw;
y[i] = ceilingOut + calibration.V[i] * spanOut;
}
float[] m = FritschCarlsonTangents(x, y, startTangent: redSlope);
return new ContinuousCurve(k, a, ceilingRaw, ceilingOut, spikeMax, redSlope,
lowlandCeilingM, climbFeather: float.NaN, summitDrama: float.NaN, x, y, m, calibration);
}
///
/// Where the preserved lowland hands over to the climb, and the red band's exit slope.
///
/// ⚠ THE FLOOD LINE IS PINNED TO THE EXACT ANCHORS, and "30 m" is NOMINAL: RED_CEIL − SEA is
/// 0.12 raw = 30.12 m. Any requested ceiling at or below the red ceiling hands over at
/// EXACTLY (K2, RED_CEIL) — no derived floats — so the linear extension is empty by
/// construction and the preserved toe+red band can never be cut by a rounding. (chat2/02's
/// first run refused its own default over that 0.12 m gap; pinning is the fix, not a wider
/// tolerance.)
///
private static (float ceilingRaw, float ceilingOut, float redSlope) ResolveHandover(
CurveKnots k, CurveAnchors a, float lowlandCeilingM)
{
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.");
float redCeilM = WorldScale.MetresFromRaw(a.RedCeil - a.Sea);
if (lowlandCeilingM <= redCeilM + 0.01f)
return (k.K2, a.RedCeil, redSlope);
float ceilingOut = a.Sea + WorldScale.RawFromMetres(lowlandCeilingM);
return (k.K2 + (ceilingOut - a.RedCeil) / redSlope, ceilingOut, redSlope);
}
///
/// 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 — shared with BuildCalibrated ----
var (ceilingRaw, ceilingOut, redSlope) = ResolveHandover(k, a, lowlandCeilingM);
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, {KnobSummary()}). 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 + EffectiveSummitOnset * 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 + EffectiveSummitOnset * (SpikeMax - CeilingRaw);
return (r, Apply(r));
}
///
/// The shaping knobs, named for whichever path built this curve — chat2/02's analytic
/// feather/drama or chat2/03's measured lift/sharpness. ⚠ The analytic fields are NaN on a
/// calibrated curve, so nothing may print them unconditionally.
///
public string KnobSummary() => Calibration != null
? $"lift {Calibration.MountainLift:F2} sharp {Calibration.PeakSharpness:F2} (calibrated)"
: $"feather {ClimbFeather:F2} drama {SummitDrama:F2} (analytic 02)";
/// 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 {KnobSummary()} · 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();
}
}
}