feat: curve v5 — corner fixes + COMPACT/BALANCED taste presets (terrain-water task 09)

The three corner fixes (both presets): riser endpoint slope floors
0.2 -> 0.1 (0.1u + 0.9*smoothstep — climbs decelerate into shelves and
accelerate out); summit-spike base floor 0.1 -> 0.05
(0.05u + 0.95*u^4 — no hard skirt under the peaks); SHELF_SPAN_MIN
2 m -> 6 m (pronounced shelves keep a gentle tilt).

Knots are preset-parameterized (CurveKnots, explicit parameter — pure,
D-035), calibrated from the same pooled batch-04 CDF:
COMPACT  P65/77/84/88/94/97 -> 65/12/7/4/6/3/3 land split
BALANCED P60/73/83/88/96/99 -> 60/13/10/5/8/3/1
Gate: "off"|"v5-compact"|"v5-balanced" (batch-only tri-state;
interim default v5-balanced pending the developer's verdict — the
winner becomes plain "v5" in a follow-up commit); v1-v4 retired
loudly. Assertion runs per preset (8 corners x per-seed spikeMax; the
lowered floors stay strictly positive). TCRV v5 extension (+9 B):
preset id + K5/K6 — the effective curve is unambiguous from the
record; old records parse (version-dispatched); harness compares.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-08 04:11:53 -04:00
parent 68f7bca8e0
commit 02577872f2
6 changed files with 137 additions and 101 deletions

View file

@ -142,6 +142,13 @@ namespace IslaApocalypse.Core
writer.Write(c.BenchSeedOffset); writer.Write(c.PlateauSeedOffset); writer.Write(c.BenchSeedOffset); writer.Write(c.PlateauSeedOffset);
writer.Write(c.StrengthSeedOffset); writer.Write(c.StrengthSeedOffset);
} }
// v5+ extension: knot preset id + K5/K6.
if (c.Version >= 5)
{
writer.Write(c.PresetId);
writer.Write(c.K5); writer.Write(c.K6);
}
} }
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp) private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)

View file

@ -26,12 +26,13 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
public static string SeaLevelModel = "flat"; public static string SeaLevelModel = "flat";
public static float SeaLevelValue = 0.15f; public static float SeaLevelValue = 0.15f;
// Height-redistribution curve (tasks 0508, graduation M-7): "v4" applies the // Height-redistribution curve (tasks 0509, graduation M-7): the task-09 taste
// terraced-ascent curve with spatially modulated shelves and the per-seed peak // batch runs a tri-state — "v5-compact" (maximum lowland) vs "v5-balanced"
// spike (HeightCurve.cs); "off" is the raw legacy profile. "v1""v3" were // (task-07/08 lineage), both with the corner fixes; "off" is the raw legacy
// retired by successor recalibrations (old blueprints are regenerable). Biome // profile. The loser retires after the developer's gate and the winner becomes
// classification is curve-invariant by construction either way. Default: v4. // plain "v5". "v1""v4" retired. INTERIM default pending the verdict:
public static string TerrainCurve = "v4"; // v5-balanced.
public static string TerrainCurve = "v5-balanced";
public static void LoadConfig() public static void LoadConfig()
{ {
@ -106,10 +107,10 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
if (data.ContainsKey("TerrainCurve")) if (data.ContainsKey("TerrainCurve"))
{ {
string curve = (string)data["TerrainCurve"]; string curve = (string)data["TerrainCurve"];
if (curve == "off" || curve == "v4") if (curve == "off" || curve == "v5-compact" || curve == "v5-balanced")
TerrainCurve = curve; TerrainCurve = curve;
else if (curve == "v1" || curve == "v2" || curve == "v3") else if (curve == "v1" || curve == "v2" || curve == "v3" || curve == "v4")
GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v4, task 08). Keeping '{TerrainCurve}' — use \"v4\" or \"off\"."); GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v5, task 09). Keeping '{TerrainCurve}' — use \"v5-compact\", \"v5-balanced\" or \"off\".");
else else
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'."); GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
} }

View file

@ -80,6 +80,11 @@ namespace IslaApocalypse.Core
public float BenchAmp, PlateauAmp, ShelfSpanMin, ShelfSpanMax; public float BenchAmp, PlateauAmp, ShelfSpanMin, ShelfSpanMax;
public float ElevFreqIslands, StrengthFreqIslands; public float ElevFreqIslands, StrengthFreqIslands;
public int BenchSeedOffset, PlateauSeedOffset, StrengthSeedOffset; public int BenchSeedOffset, PlateauSeedOffset, StrengthSeedOffset;
// v5 extension — knot preset (1 = compact, 2 = balanced) and the two knots the
// four base slots cannot carry; with these the effective curve is unambiguous.
public byte PresetId;
public float K5, K6;
} }
public class WorldBlueprint public class WorldBlueprint
@ -416,6 +421,11 @@ namespace IslaApocalypse.Core
c.BenchSeedOffset = reader.ReadInt32(); c.PlateauSeedOffset = reader.ReadInt32(); c.BenchSeedOffset = reader.ReadInt32(); c.PlateauSeedOffset = reader.ReadInt32();
c.StrengthSeedOffset = reader.ReadInt32(); c.StrengthSeedOffset = reader.ReadInt32();
} }
if (c.Version >= 5)
{
c.PresetId = reader.ReadByte();
c.K5 = reader.ReadSingle(); c.K6 = reader.ReadSingle();
}
blueprint.TerrainCurve = c; blueprint.TerrainCurve = c;
return true; return true;
} }

View file

@ -1,146 +1,151 @@
using Godot; using Godot;
/// <summary> /// <summary>
/// The height-redistribution curve, v4 — SPATIALLY MODULATED SHELVES (terrain-water /// One preset's input knots for the v5 curve. Two presets exist for the task-09
/// task 08). Pure, static, monotonic piecewise map; every per-column input is an /// taste batch — COMPACT (maximum lowland, cordillera-from-plains) and BALANCED
/// explicit PARAMETER (D-035): the seed spike max and the four modulated shelf /// (the task-07/08 lineage, gradual highland approach). The loser retires after
/// values. The generator samples the modulation fields; this class never touches /// the developer's gate; the winner becomes plain "v5".
/// noise. /// </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).
/// ///
/// v4 (developer's task-07 gate finding: the terraces work, but uniform anchors put /// The corner fixes (both presets — the naturalness work):
/// a flat ring at exactly 100 m and exactly 220 m on every mountain — the bathtub /// 1. Riser endpoint slope floor 0.2 → 0.1 (blend 0.1u + 0.9·smoothstep): climbs
/// rings): the bench and plateau OUTPUT anchors become smooth spatial fields — /// decelerate into shelves and accelerate out of them — no machined edges.
/// benchLo = 100 m ± 12 m and plateauLo = 220 m ± 20 m via two decorrelated /// 2. Summit-spike base floor 0.1 → 0.05 (blend 0.05u + 0.95·u⁴): the spike
/// very-low-frequency noise fields — and a third field modulates SHELF STRENGTH, /// leaves the plateau gently — no hard skirt under the peaks.
/// blending each shelf between "pronounced flat" (output span ~2 m) and "barely a /// 3. SHELF_SPAN_MIN 2 m → 6 m: pronounced shelves keep a gentle tilt — flat to
/// hint" (~25 m of gentle slope), so not every flank at shelf height develops the /// build on, never snooker-table flat.
/// full terrace. Shelves stay locally flat; the two magic altitudes stop existing.
/// ///
/// Structure otherwise v3's, unchanged: identity ≤ sea, frozen toe/rise (storm /// Presets (input land-fraction targets; knots calibrated 2026-08-08 from the same
/// ladder), foothill riser → bench → mid riser → plateau → per-seed-normalized /// pooled batch-04 flat-sea land CDF as tasks 05/07, 340,618,126 samples; achieved
/// stiff spike (u⁴, 420 m cap) → tail. Input knots are v3's calibration. /// 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
/// ///
/// ORDERING SAFETY BY CONSTRUCTION (asserted): with the amplitudes below, at every /// Unchanged from v4: storm-ladder anchors, bench 100±12 m, plateau 220±20 m,
/// column: red ceiling 0.27 < benchLo… (bench min 0.5006), bench top max 0.6962 < /// strength modulation (span max 25 m), 420 m cap, per-seed spike normalization,
/// plateauLo min 0.9468, plateau top max 1.2062 < peak cap 1.8287. The numeric /// modulation fields/seed offsets, the classify-map invariant.
/// assertion additionally sweeps all 8 modulation-extreme corners per generation.
/// </summary> /// </summary>
public static class HeightCurve public static class HeightCurve
{ {
public const ushort VERSION = 4; public const ushort VERSION = 5;
// Input knots — v3 calibration (pooled batch-04 land CDF, P59/72/82/87/95/98). public static readonly CurveKnots COMPACT = new CurveKnots(1, "compact",
public const float K1 = 0.509179f; 0.550982f, 0.646528f, 0.723145f, 0.784045f, 0.903025f, 1.003079f); // P65/77/84/88/94/97
public const float K2 = 0.604081f;
public const float K3 = 0.698485f; public static readonly CurveKnots BALANCED = new CurveKnots(2, "balanced",
public const float K4 = 0.767213f; 0.515899f, 0.612157f, 0.710472f, 0.784045f, 0.962922f, 1.119118f); // P60/73/83/88/96/99
public const float K5 = 0.930304f;
public const float K6 = 1.050720f;
// Fixed output anchors — storm ladder + ceiling (frozen). // Fixed output anchors — storm ladder + ceiling (frozen).
public const float SEA = 0.15f; public const float SEA = 0.15f;
public const float ORANGE_CEIL = 0.206f; public const float ORANGE_CEIL = 0.206f;
public const float RED_CEIL = 0.27f; public const float RED_CEIL = 0.27f;
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 public const float PEAK_CAP = SEA + 420f / 251f;
public const float TAIL_SLOPE = 0.25f; public const float TAIL_SLOPE = 0.25f;
public const float SPIKE_MIN_SPAN = 0.01f; public const float SPIKE_MIN_SPAN = 0.01f;
// Modulated shelf anchors: base ± amplitude (raw units; 251 m per unit). // Modulated shelf anchors (unchanged from v4).
public const float BENCH_BASE = SEA + 100f / 251f; // ≈ 0.54841 (100 m) public const float BENCH_BASE = SEA + 100f / 251f;
public const float BENCH_AMP = 12f / 251f; // ± 12 m public const float BENCH_AMP = 12f / 251f;
public const float PLATEAU_BASE = SEA + 220f / 251f; // ≈ 1.02649 (220 m) public const float PLATEAU_BASE = SEA + 220f / 251f;
public const float PLATEAU_AMP = 20f / 251f; // ± 20 m public const float PLATEAU_AMP = 20f / 251f;
// Shelf strength: output span of each shelf segment, blended by the strength // Corner fix 3: pronounced shelves keep ~6 m of tilt across the shelf band.
// field. Strong (t=1) → SPAN_MIN (~2 m, pronounced flat). Weak (t=0) → public const float SHELF_SPAN_MIN = 6f / 251f; // ≈ 0.0239 (v4: 0.008 ≈ 2 m)
// 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; public const float SHELF_SPAN_MAX = 0.10f;
// Modulation-field derivation (generator-side, recorded in TCRV): field seed = // Modulation-field derivation (unchanged from v4).
// RESOLVED WorldSeed + offset; frequency = (periods per island width) / MapSize.
public const int BENCH_SEED_OFFSET = 7101; public const int BENCH_SEED_OFFSET = 7101;
public const int PLATEAU_SEED_OFFSET = 7207; public const int PLATEAU_SEED_OFFSET = 7207;
public const int STRENGTH_SEED_OFFSET = 7303; public const int STRENGTH_SEED_OFFSET = 7303;
public const float ELEV_FREQ_ISLANDS = 3.0f; // ~3 undulations across the island public const float ELEV_FREQ_ISLANDS = 3.0f;
public const float STRENGTH_FREQ_ISLANDS = 5.0f; // finer patchiness for shelf strength public const float STRENGTH_FREQ_ISLANDS = 5.0f;
public static float EffectiveSpikeMax(float hMaxSeed) public static float EffectiveSpikeMax(float hMaxSeed, CurveKnots k)
{ {
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN); return Mathf.Max(hMaxSeed, k.K6 + SPIKE_MIN_SPAN);
} }
/// <summary>Shelf output span for a strength sample t ∈ [0,1].</summary>
public static float ShelfSpan(float strength01) public static float ShelfSpan(float strength01)
{ {
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f)); 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, public static float Apply(float h, float hMaxSeed,
float benchLo, float benchSpan, float plateauLo, float plateauSpan) float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k)
{ {
if (h <= SEA) return h; if (h <= SEA) return h;
float u, s; float u, s;
if (h < K1) if (h < k.K1)
{ {
u = (h - SEA) / (K1 - SEA); u = (h - SEA) / (k.K1 - SEA);
s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
return SEA + s * (ORANGE_CEIL - SEA); return SEA + s * (ORANGE_CEIL - SEA);
} }
if (h < K2) if (h < k.K2)
{ {
u = (h - K1) / (K2 - K1); u = (h - k.K1) / (k.K2 - k.K1);
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
} }
if (h < K3) if (h < k.K3)
{ {
u = (h - K2) / (K3 - K2); u = (h - k.K2) / (k.K3 - k.K2);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // foothill riser — corner fix 1
return RED_CEIL + s * (benchLo - RED_CEIL); return RED_CEIL + s * (benchLo - RED_CEIL);
} }
if (h < K4) if (h < k.K4)
{ {
u = (h - K3) / (K4 - K3); u = (h - k.K3) / (k.K4 - k.K3);
return benchLo + u * benchSpan; // bench — modulated return benchLo + u * benchSpan; // bench (min span 6 m — fix 3)
} }
float benchTop = benchLo + benchSpan; float benchTop = benchLo + benchSpan;
if (h < K5) if (h < k.K5)
{ {
u = (h - K4) / (K5 - K4); u = (h - k.K4) / (k.K5 - k.K4);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // mid riser — corner fix 1
return benchTop + s * (plateauLo - benchTop); return benchTop + s * (plateauLo - benchTop);
} }
if (h < K6) if (h < k.K6)
{ {
u = (h - K5) / (K6 - K5); u = (h - k.K5) / (k.K6 - k.K5);
return plateauLo + u * plateauSpan; // plateau — modulated return plateauLo + u * plateauSpan; // plateau
} }
float plateauTop = plateauLo + plateauSpan; float plateauTop = plateauLo + plateauSpan;
float spikeMax = EffectiveSpikeMax(hMaxSeed); float spikeMax = EffectiveSpikeMax(hMaxSeed, k);
if (h < spikeMax) if (h < spikeMax)
{ {
u = (h - K6) / (spikeMax - K6); u = (h - k.K6) / (spikeMax - k.K6);
s = 0.1f * u + 0.9f * (u * u * u * u); // summit spike (v2/v3 shape) s = 0.05f * u + 0.95f * (u * u * u * u); // summit spike — corner fix 2
return plateauTop + s * (PEAK_CAP - plateauTop); return plateauTop + s * (PEAK_CAP - plateauTop);
} }
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE; return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
} }
/// <summary> /// <summary>
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve: /// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve for
/// sweeps the full domain at every one of the 8 modulation-extreme corners /// the selected preset: all 8 modulation-extreme corners × per-seed spikeMax.
/// (bench anchor ±, plateau anchor ±, strength min/max) with the per-seed /// The corner fixes lower the slope floors (risers 0.1, spike base 0.05) — the
/// spikeMax — the adversarial corner set for the ordering constraints. Loud /// sweep proves they stay strictly positive everywhere. Loud throw on failure.
/// throw, refuses to generate.
/// </summary> /// </summary>
public static void AssertMonotonic(float hMaxSeed) public static void AssertMonotonic(float hMaxSeed, CurveKnots k)
{ {
float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP }; float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP };
float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP }; float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP };
@ -153,27 +158,27 @@ public static class HeightCurve
foreach (float sp in spans) foreach (float sp in spans)
{ {
float prevH = -7f; float prevH = -7f;
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp); float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k);
void Check(double hd) void Check(double hd)
{ {
float h = (float)hd; float h = (float)hd;
if (h <= prevH) return; // dedupe float32 samples (task-05 fix) if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
float v = Apply(h, hMaxSeed, bl, sp, pl, sp); float v = Apply(h, hMaxSeed, bl, sp, pl, sp, k);
if (v <= prev) if (v <= prev)
throw new System.InvalidOperationException( throw new System.InvalidOperationException(
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate."); $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
prev = v; prev = v;
prevH = h; prevH = h;
} }
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5); 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 = -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 = 0.10; hh <= top; hh += 0.0001) Check(hh);
for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) 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})."); GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION} preset '{k.Name}', 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6}).");
} }
} }

View file

@ -46,6 +46,10 @@ public partial class MapGenerator : TextureRect
private FastNoiseLite _plateauNoise; private FastNoiseLite _plateauNoise;
private FastNoiseLite _strengthNoise; private FastNoiseLite _strengthNoise;
// v5: the selected knot preset (task-09 taste batch: COMPACT or BALANCED);
// null = curve off.
private CurveKnots _curveKnots;
// The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine, // The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine,
// pre-carve) — the v2 curve's per-seed spike normalizer. Computed in // pre-carve) — the v2 curve's per-seed spike normalizer. Computed in
// GenerateTopography pass 1; recorded in TCRV (effective, guard applied). // GenerateTopography pass 1; recorded in TCRV (effective, guard applied).
@ -84,7 +88,10 @@ public partial class MapGenerator : TextureRect
this.CustomMinimumSize = new Vector2(MapSize, MapSize); this.CustomMinimumSize = new Vector2(MapSize, MapSize);
_heightMap = new float[MapSize, MapSize]; _heightMap = new float[MapSize, MapSize];
_curveOn = ConfigManager.TerrainCurve == "v4"; _curveKnots = ConfigManager.TerrainCurve == "v5-compact" ? HeightCurve.COMPACT
: ConfigManager.TerrainCurve == "v5-balanced" ? HeightCurve.BALANCED
: null;
_curveOn = _curveKnots != null;
// (The monotonicity assertion now runs inside GenerateTopography, against the // (The monotonicity assertion now runs inside GenerateTopography, against the
// effective per-seed curve, once hMaxSeed is known.) // effective per-seed curve, once hMaxSeed is known.)
_heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap; _heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
@ -276,11 +283,14 @@ public partial class MapGenerator : TextureRect
// the bench slots carry the two BASE anchors; the v4 extension record carries // the bench slots carry the two BASE anchors; the v4 extension record carries
// the modulation parameters (amplitudes, spans, frequencies, seed offsets) — // the modulation parameters (amplitudes, spans, frequencies, seed offsets) —
// the blueprint stays self-describing. // the blueprint stays self-describing.
// TCRV v5: knot slots carry the PRESET's K1..K4; the extension carries the
// modulation params plus preset id and K5/K6 — the effective curve is
// unambiguous from the record alone.
TerrainCurve = _curveOn ? new TerrainCurveInfo TerrainCurve = _curveOn ? new TerrainCurveInfo
{ {
Version = HeightCurve.VERSION, Version = HeightCurve.VERSION,
T1 = HeightCurve.K1, T2 = HeightCurve.K2, T3 = HeightCurve.K3, T4 = HeightCurve.K4, T1 = _curveKnots.K1, T2 = _curveKnots.K2, T3 = _curveKnots.K3, T4 = _curveKnots.K4,
SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed, _curveKnots), // per-seed
Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL, Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
PlateauLo = HeightCurve.BENCH_BASE, PlateauHi = HeightCurve.PLATEAU_BASE, PlateauLo = HeightCurve.BENCH_BASE, PlateauHi = HeightCurve.PLATEAU_BASE,
PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE, PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE,
@ -288,7 +298,8 @@ public partial class MapGenerator : TextureRect
ShelfSpanMin = HeightCurve.SHELF_SPAN_MIN, ShelfSpanMax = HeightCurve.SHELF_SPAN_MAX, ShelfSpanMin = HeightCurve.SHELF_SPAN_MIN, ShelfSpanMax = HeightCurve.SHELF_SPAN_MAX,
ElevFreqIslands = HeightCurve.ELEV_FREQ_ISLANDS, StrengthFreqIslands = HeightCurve.STRENGTH_FREQ_ISLANDS, ElevFreqIslands = HeightCurve.ELEV_FREQ_ISLANDS, StrengthFreqIslands = HeightCurve.STRENGTH_FREQ_ISLANDS,
BenchSeedOffset = HeightCurve.BENCH_SEED_OFFSET, PlateauSeedOffset = HeightCurve.PLATEAU_SEED_OFFSET, BenchSeedOffset = HeightCurve.BENCH_SEED_OFFSET, PlateauSeedOffset = HeightCurve.PLATEAU_SEED_OFFSET,
StrengthSeedOffset = HeightCurve.STRENGTH_SEED_OFFSET StrengthSeedOffset = HeightCurve.STRENGTH_SEED_OFFSET,
PresetId = _curveKnots.PresetId, K5 = _curveKnots.K5, K6 = _curveKnots.K6
} : null, } : null,
FormatVersion = 2, FormatVersion = 2,
Params = new BlueprintParams Params = new BlueprintParams
@ -445,7 +456,7 @@ public partial class MapGenerator : TextureRect
// The v2 curve is SEED-DEPENDENT: its spike maps [t4, hMaxSeed] onto the peak // The v2 curve is SEED-DEPENDENT: its spike maps [t4, hMaxSeed] onto the peak
// band, so the monotonicity assertion must run against the EFFECTIVE per-seed // band, so the monotonicity assertion must run against the EFFECTIVE per-seed
// curve — after hMaxSeed is known, before any pixel is curved. // curve — after hMaxSeed is known, before any pixel is curved.
if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed); if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed, _curveKnots);
// --- PASS 2: curve (task 05/06) + crater carve --- // --- PASS 2: curve (task 05/06) + crater carve ---
// Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so // Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so
@ -469,7 +480,7 @@ public partial class MapGenerator : TextureRect
float benchLo = HeightCurve.BENCH_BASE + _benchNoise.GetNoise2D(x, y) * HeightCurve.BENCH_AMP; float benchLo = HeightCurve.BENCH_BASE + _benchNoise.GetNoise2D(x, y) * HeightCurve.BENCH_AMP;
float plateauLo = HeightCurve.PLATEAU_BASE + _plateauNoise.GetNoise2D(x, y) * HeightCurve.PLATEAU_AMP; float plateauLo = HeightCurve.PLATEAU_BASE + _plateauNoise.GetNoise2D(x, y) * HeightCurve.PLATEAU_AMP;
float shelfSpan = HeightCurve.ShelfSpan((_strengthNoise.GetNoise2D(x, y) + 1f) * 0.5f); float shelfSpan = HeightCurve.ShelfSpan((_strengthNoise.GetNoise2D(x, y) + 1f) * 0.5f);
curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan); curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan, _curveKnots);
} }
else else
{ {

View file

@ -202,10 +202,12 @@ public partial class RoundTripHarness : Node
bool same = ca.Version == cb.Version; bool same = ca.Version == cb.Version;
float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope, float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope,
ca.BenchAmp, ca.PlateauAmp, ca.ShelfSpanMin, ca.ShelfSpanMax, ca.ElevFreqIslands, ca.StrengthFreqIslands, ca.BenchAmp, ca.PlateauAmp, ca.ShelfSpanMin, ca.ShelfSpanMax, ca.ElevFreqIslands, ca.StrengthFreqIslands,
ca.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset }; ca.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset,
ca.PresetId, ca.K5, ca.K6 };
float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope, float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope,
cb.BenchAmp, cb.PlateauAmp, cb.ShelfSpanMin, cb.ShelfSpanMax, cb.ElevFreqIslands, cb.StrengthFreqIslands, cb.BenchAmp, cb.PlateauAmp, cb.ShelfSpanMin, cb.ShelfSpanMax, cb.ElevFreqIslands, cb.StrengthFreqIslands,
cb.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset }; cb.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset,
cb.PresetId, cb.K5, cb.K6 };
for (int i = 0; i < fa.Length; i++) for (int i = 0; i < fa.Length; i++)
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false; if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; } if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }