feat: curve v3 — terraced ascent, two benches, white plateau at 220 m (terrain-water task 07)
Per the task-06 ground-test verdict (floor and 420 m ceiling frozen; v2's ascent read flat-then-wall — 87% of the vertical budget in the last 4% of input): the ascent is rebuilt as a terraced climb. Knots recalibrated from the same pooled batch-04 land CDF at P59/72/82/87/95/98 (K=0.509179/0.604081/0.698485/0.767213/0.930304/ 1.050720), pooled land fractions exactly 59/13/10/5/8/3/2 (orange/red/ foothill-riser/bench/mid-riser/plateau/spike). Segments: frozen toe and rise (storm anchors), smoothstep foothill riser to a near-flat 100 m bench, smoothstep mid riser to the near-flat white plateau at 220 m (relocated from 50 m — mountain towns/snow belong there), per-seed-normalized u4 summit spike to 420 m (both retained from v2), linear tail. Per-seed effective-curve monotonicity assertion retained. TerrainCurve gate: "off"|"v3" (default v3); v1/v2 retired with a loud config error (old blueprints regenerable). TCRV: knot slots carry K1..K4 (K5/K6 are version constants), PlateauLo/Hi carry the two bench anchors; version byte selects the semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5720f05a5b
commit
ae7431ac87
3 changed files with 102 additions and 83 deletions
|
|
@ -26,12 +26,12 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
|||
public static string SeaLevelModel = "flat";
|
||||
public static float SeaLevelValue = 0.15f;
|
||||
|
||||
// Height-redistribution curve (tasks 05/06, graduation M-7): "v2" applies the
|
||||
// calibrated storm-ladder curve with the per-seed peak spike (HeightCurve.cs);
|
||||
// "off" is the raw legacy profile. "v1" was dropped with the v2 recalibration
|
||||
// (task-05 blueprints are regenerable). Biome classification is curve-invariant
|
||||
// by construction either way. Default: v2.
|
||||
public static string TerrainCurve = "v2";
|
||||
// Height-redistribution curve (tasks 05/06/07, graduation M-7): "v3" applies
|
||||
// the terraced-ascent curve with the per-seed peak spike (HeightCurve.cs);
|
||||
// "off" is the raw legacy profile. "v1"/"v2" were retired by their successor
|
||||
// recalibrations (old blueprints are regenerable). Biome classification is
|
||||
// curve-invariant by construction either way. Default: v3.
|
||||
public static string TerrainCurve = "v3";
|
||||
|
||||
public static void LoadConfig()
|
||||
{
|
||||
|
|
@ -106,10 +106,10 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
|||
if (data.ContainsKey("TerrainCurve"))
|
||||
{
|
||||
string curve = (string)data["TerrainCurve"];
|
||||
if (curve == "off" || curve == "v2")
|
||||
if (curve == "off" || curve == "v3")
|
||||
TerrainCurve = curve;
|
||||
else if (curve == "v1")
|
||||
GD.PrintErr($"[ConfigManager] TerrainCurve 'v1' was retired by the v2 recalibration (task 06). Keeping '{TerrainCurve}' — use \"v2\" or \"off\".");
|
||||
else if (curve == "v1" || curve == "v2")
|
||||
GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v3, task 07). Keeping '{TerrainCurve}' — use \"v3\" or \"off\".");
|
||||
else
|
||||
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,117 +1,132 @@
|
|||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// The height-redistribution curve, v2 (terrain-water task 06) — a pure, static,
|
||||
/// monotonic piecewise map over raw blueprint heights (D-035: numbers in, numbers
|
||||
/// out; the per-seed spike maximum is an explicit PARAMETER, not hidden state).
|
||||
/// 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).
|
||||
///
|
||||
/// v2 changes (developer's task-05 hillshade-gate verdict; everything below the
|
||||
/// plateau step is behaviorally byte-identical to v1):
|
||||
/// - PER-SEED SPIKE NORMALIZATION: the spike's input domain runs from t4 to the
|
||||
/// current seed's own raw pre-curve maximum (hMaxSeed), so every island's
|
||||
/// tallest pixel reaches the ceiling — v1 mapped against the pooled
|
||||
/// calibration max and mid-range seeds topped out at 110–175 m.
|
||||
/// - STIFFER SPIKE: ease-in 0.1u + 0.9·u⁴ (was 0.2u + 0.8·u³) — a wall, not a ramp.
|
||||
/// - PEAK CEILING 420 m above sea (was 220 m).
|
||||
/// 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:
|
||||
///
|
||||
/// Lower knots/bands are v1's, calibrated 2026-08-07 from batch 04's ten flat-sea
|
||||
/// heightmaps (pooled above-sea land CDF, 340,618,126 samples): P75/P90/P93/P96.
|
||||
/// 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)
|
||||
///
|
||||
/// Shape (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):
|
||||
/// h ≤ sea (0.15) identity — water and the below-sea world untouched
|
||||
/// sea → t1 smooth toe, ease-out blend (gentle rolling, never flat)
|
||||
/// t1 → t2 linear rise into the red band
|
||||
/// t2 → t3 smooth shoulder up to the plateau shelf
|
||||
/// t3 → t4 near-flat plateau step (small positive slope)
|
||||
/// t4 → spikeMax accelerating u⁴ spike to the 420 m peak cap (per-seed domain)
|
||||
/// h > spikeMax linear tail (strict monotonicity, no clamp; reachable only
|
||||
/// in the degenerate near-flat guard case)
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class HeightCurve
|
||||
{
|
||||
public const ushort VERSION = 2;
|
||||
public const ushort VERSION = 3;
|
||||
|
||||
// Input knots — v1 calibration, unchanged (see class header).
|
||||
public const float T1 = 0.628736f; // P75 — orange coverage boundary
|
||||
public const float T2 = 0.819152f; // P90
|
||||
public const float T3 = 0.879340f; // P93
|
||||
public const float T4 = 0.962922f; // P96
|
||||
// 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 bands — the storm ladder. Lower anchors unchanged from v1.
|
||||
// Output anchors — storm ladder (frozen) + the terraces.
|
||||
public const float SEA = 0.15f;
|
||||
public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling
|
||||
public const float RED_CEIL = 0.27f; // biblical ceiling
|
||||
public const float PLATEAU_LO = SEA + 50f / 251f; // ≈ 0.34924 (50 m above sea)
|
||||
public const float PLATEAU_HI = PLATEAU_LO + 0.02f; // ≈ 0.36924 (~5 m step relief)
|
||||
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 (420 m above sea; v1: 220 m)
|
||||
public const float TAIL_SLOPE = 0.25f; // above spikeMax (degenerate guard only)
|
||||
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: the spike domain is [T4, max(hMaxSeed, T4 + SPIKE_MIN_SPAN)],
|
||||
// so a pathological seed whose raw max sits at or below t4 still yields a positive,
|
||||
// monotonic domain (its cap is then simply never reached; heights above spikeMax — none in
|
||||
// practice — would ride the tail).
|
||||
// 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;
|
||||
|
||||
/// <summary>The effective spike-domain top for a seed's raw maximum, guard applied.</summary>
|
||||
public static float EffectiveSpikeMax(float hMaxSeed)
|
||||
{
|
||||
return Mathf.Max(hMaxSeed, T4 + SPIKE_MIN_SPAN);
|
||||
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN);
|
||||
}
|
||||
|
||||
/// <param name="h">Raw pre-curve height.</param>
|
||||
/// <param name="hMaxSeed">The seed's raw pre-curve maximum (post noise/falloff/Trench/spine,
|
||||
/// pre-carve) — the same field the curve consumes. Makes the map seed-dependent (v2).</param>
|
||||
/// <param name="hMaxSeed">The seed's raw pre-curve maximum — per-seed spike normalizer.</param>
|
||||
public static float Apply(float h, float hMaxSeed)
|
||||
{
|
||||
if (h <= SEA) return h;
|
||||
|
||||
float u, s;
|
||||
if (h < T1)
|
||||
if (h < K1)
|
||||
{
|
||||
u = (h - SEA) / (T1 - SEA);
|
||||
s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out, slope ≥ 0.3
|
||||
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 < T2)
|
||||
if (h < K2)
|
||||
{
|
||||
u = (h - T1) / (T2 - T1);
|
||||
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear
|
||||
u = (h - K1) / (K2 - K1);
|
||||
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear rise
|
||||
}
|
||||
if (h < T3)
|
||||
if (h < K3)
|
||||
{
|
||||
u = (h - T2) / (T3 - T2);
|
||||
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // smoothstep blend, slope ≥ 0.2
|
||||
return RED_CEIL + s * (PLATEAU_LO - RED_CEIL);
|
||||
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 < T4)
|
||||
if (h < K4)
|
||||
{
|
||||
u = (h - T3) / (T4 - T3);
|
||||
return PLATEAU_LO + u * (PLATEAU_HI - PLATEAU_LO); // near-flat, small positive slope
|
||||
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 - T4) / (spikeMax - T4);
|
||||
s = 0.1f * u + 0.9f * (u * u * u * u); // ease-in u⁴ wall, slope ≥ 0.1
|
||||
return PLATEAU_HI + s * (PEAK_CAP - PLATEAU_HI);
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once
|
||||
/// per generation after hMaxSeed is known, before the curve pass. A violation is
|
||||
/// a build bug, not a data condition — fail loudly and refuse to generate.
|
||||
/// per generation after hMaxSeed is known. Loud throw, refuses to generate.
|
||||
/// </summary>
|
||||
public static void AssertMonotonic(float hMaxSeed)
|
||||
{
|
||||
float prevH = -7f;
|
||||
float prev = Apply(prevH, hMaxSeed);
|
||||
|
||||
// Successive double samples can round to the SAME float32 — only strictly
|
||||
// increasing float samples are compared (task-05 incident fix, kept).
|
||||
// Only strictly increasing float32 samples are compared (task-05 incident fix).
|
||||
void Check(double hd)
|
||||
{
|
||||
float h = (float)hd;
|
||||
|
|
@ -124,8 +139,6 @@ public static class HeightCurve
|
|||
prevH = h;
|
||||
}
|
||||
|
||||
// Coarse below the identity region, fine through every knot, out past the
|
||||
// per-seed spike top and the tail.
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public partial class MapGenerator : TextureRect
|
|||
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
|
||||
|
||||
_heightMap = new float[MapSize, MapSize];
|
||||
_curveOn = ConfigManager.TerrainCurve == "v2";
|
||||
_curveOn = ConfigManager.TerrainCurve == "v3";
|
||||
// (The monotonicity assertion now runs inside GenerateTopography, against the
|
||||
// effective per-seed curve, once hMaxSeed is known.)
|
||||
_heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
|
||||
|
|
@ -256,13 +256,16 @@ public partial class MapGenerator : TextureRect
|
|||
TrailRoads = _trailPaths,
|
||||
WaterBodyIds = _waterBodyIds,
|
||||
WaterBodies = _waterBodies ?? new List<WaterBodyInfo>(),
|
||||
// TCRV under curve v3: the four knot slots carry K1..K4 (K5/K6 are version
|
||||
// constants, documented in BLUEPRINT_FORMAT.md); PlateauLo/Hi carry the two
|
||||
// bench anchors (foothill 100 m / white plateau 220 m).
|
||||
TerrainCurve = _curveOn ? new TerrainCurveInfo
|
||||
{
|
||||
Version = HeightCurve.VERSION,
|
||||
T1 = HeightCurve.T1, T2 = HeightCurve.T2, T3 = HeightCurve.T3, T4 = HeightCurve.T4,
|
||||
SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed (v2)
|
||||
T1 = HeightCurve.K1, T2 = HeightCurve.K2, T3 = HeightCurve.K3, T4 = HeightCurve.K4,
|
||||
SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed
|
||||
Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
|
||||
PlateauLo = HeightCurve.PLATEAU_LO, PlateauHi = HeightCurve.PLATEAU_HI,
|
||||
PlateauLo = HeightCurve.FOOTHILL_BENCH, PlateauHi = HeightCurve.PLATEAU,
|
||||
PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE
|
||||
} : null,
|
||||
FormatVersion = 2,
|
||||
|
|
@ -800,7 +803,8 @@ public partial class MapGenerator : TextureRect
|
|||
Color shallowSea = new Color(0.25f, 0.45f, 0.65f);
|
||||
Color green = new Color(0.44f, 0.62f, 0.36f); // orange band terrain: lowland green
|
||||
Color tan = new Color(0.76f, 0.70f, 0.46f); // red band: tan
|
||||
Color brown = new Color(0.55f, 0.41f, 0.28f); // shoulder + plateau: brown
|
||||
Color brown = new Color(0.55f, 0.41f, 0.28f); // foothill riser + bench: brown
|
||||
Color darkBrown = new Color(0.42f, 0.32f, 0.24f); // mid riser: darker brown
|
||||
Color white = new Color(0.97f, 0.97f, 0.98f); // peaks
|
||||
Color grey = new Color(0.72f, 0.72f, 0.70f);
|
||||
|
||||
|
|
@ -820,10 +824,12 @@ public partial class MapGenerator : TextureRect
|
|||
}
|
||||
else if (h < HeightCurve.ORANGE_CEIL) tint = green;
|
||||
else if (h < HeightCurve.RED_CEIL) tint = tan;
|
||||
else if (h < HeightCurve.PLATEAU_HI) tint = brown;
|
||||
else if (h < HeightCurve.FOOTHILL_TOP) tint = brown; // foothill riser + 100 m bench
|
||||
else if (h < HeightCurve.PLATEAU) tint = darkBrown; // the mid riser
|
||||
else
|
||||
{
|
||||
float t2 = Mathf.Clamp((h - HeightCurve.PLATEAU_HI) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU_HI), 0f, 1f);
|
||||
// White plateau (220 m) through the summit spike to the 420 m cap.
|
||||
float t2 = Mathf.Clamp((h - HeightCurve.PLATEAU) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU), 0f, 1f);
|
||||
tint = grey.Lerp(white, t2);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue