feat: curve v4 — spatially modulated shelves (terrain-water task 08)

Per the task-07 gate: the terraces work but uniform anchors put a flat
ring at exactly 100 m and 220 m on every mountain. v4 keeps v3's
structure (frozen toe/rise, three risers, per-seed u4 spike to 420 m)
and turns the shelf anchors into smooth spatial fields: bench
100+-12 m and plateau 220+-20 m via two decorrelated very-low-freq
Simplex fields (~3 undulations per island width), plus a strength
field (~5/island) blending each shelf's output span between ~2 m
(pronounced flat) and ~25 m (barely a hint) — the optional strength
modulation shipped, ordering-safe by construction (worst-case bench
top 0.6962 < plateau min 0.9468). Field seeds derive from the RESOLVED
noise seed + fixed offsets (7101/7207/7303) — no config knob. Apply is
per-column with all modulated values as pure parameters (D-035); the
classify path never sees them.

Monotonicity assertion now sweeps all 8 modulation-extreme corners x
per-seed spikeMax. TerrainCurve gate "off"|"v4" (default v4);
v1-v3 retired loudly. TCRV extended (+36 B when version>=4:
amplitudes, span range, frequencies, seed offsets — length-framed
section makes the layout change safe; parser dispatches on the curve
version byte; harness compares the extension).

Note: MapGenerator.cs also carries the developer's editor whitespace
normalization (spaces->tabs, git diff -w empty before this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-08 03:30:27 -04:00
parent a046486a58
commit 4d2e024b2f
6 changed files with 292 additions and 183 deletions

View file

@ -132,6 +132,16 @@ namespace IslaApocalypse.Core
writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil);
writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap);
writer.Write(c.TailSlope);
// v4+ extension: shelf-modulation parameters.
if (c.Version >= 4)
{
writer.Write(c.BenchAmp); writer.Write(c.PlateauAmp);
writer.Write(c.ShelfSpanMin); writer.Write(c.ShelfSpanMax);
writer.Write(c.ElevFreqIslands); writer.Write(c.StrengthFreqIslands);
writer.Write(c.BenchSeedOffset); writer.Write(c.PlateauSeedOffset);
writer.Write(c.StrengthSeedOffset);
}
}
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)

View file

@ -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/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";
// Height-redistribution curve (tasks 05–08, graduation M-7): "v4" applies the
// terraced-ascent curve with spatially modulated shelves and the per-seed peak
// spike (HeightCurve.cs); "off" is the raw legacy profile. "v1"–"v3" were
// retired by successor recalibrations (old blueprints are regenerable). Biome
// classification is curve-invariant by construction either way. Default: v4.
public static string TerrainCurve = "v4";
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 == "v3")
if (curve == "off" || curve == "v4")
TerrainCurve = curve;
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 if (curve == "v1" || curve == "v2" || curve == "v3")
GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v4, task 08). Keeping '{TerrainCurve}' — use \"v4\" or \"off\".");
else
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
}

View file

@ -74,6 +74,12 @@ namespace IslaApocalypse.Core
// constants alone, which is exactly why it is recorded here.
public float T1, T2, T3, T4, SpikeMax;
public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands
// v4 extension — shelf-modulation parameters (record layout versioned by the
// curve Version byte; zero-valued when parsing an older record).
public float BenchAmp, PlateauAmp, ShelfSpanMin, ShelfSpanMax;
public float ElevFreqIslands, StrengthFreqIslands;
public int BenchSeedOffset, PlateauSeedOffset, StrengthSeedOffset;
}
public class WorldBlueprint
@ -399,6 +405,17 @@ namespace IslaApocalypse.Core
c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle();
c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle();
c.TailSlope = reader.ReadSingle();
// v4+ extension: shelf-modulation parameters (older records simply end here —
// the section is length-framed, so the version byte is the dispatcher).
if (c.Version >= 4)
{
c.BenchAmp = reader.ReadSingle(); c.PlateauAmp = reader.ReadSingle();
c.ShelfSpanMin = reader.ReadSingle(); c.ShelfSpanMax = reader.ReadSingle();
c.ElevFreqIslands = reader.ReadSingle(); c.StrengthFreqIslands = reader.ReadSingle();
c.BenchSeedOffset = reader.ReadInt32(); c.PlateauSeedOffset = reader.ReadInt32();
c.StrengthSeedOffset = reader.ReadInt32();
}
blueprint.TerrainCurve = c;
return true;
}