diff --git a/Core/Scripts/BlueprintWriter.cs b/Core/Scripts/BlueprintWriter.cs
index 56bc07c..4ed218d 100644
--- a/Core/Scripts/BlueprintWriter.cs
+++ b/Core/Scripts/BlueprintWriter.cs
@@ -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)
diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs
index d03b34b..6030555 100644
--- a/Core/Scripts/ConfigManager.cs
+++ b/Core/Scripts/ConfigManager.cs
@@ -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}'.");
}
diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs
index c40ec40..00e03a6 100644
--- a/Core/Scripts/MapDataParser.cs
+++ b/Core/Scripts/MapDataParser.cs
@@ -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;
}
diff --git a/Tools/Scripts/HeightCurve.cs b/Tools/Scripts/HeightCurve.cs
index aacfaf3..fd6f1ed 100644
--- a/Tools/Scripts/HeightCurve.cs
+++ b/Tools/Scripts/HeightCurve.cs
@@ -1,75 +1,89 @@
using Godot;
///
-/// 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).
+/// The height-redistribution curve, v4 — SPATIALLY MODULATED SHELVES (terrain-water
+/// task 08). Pure, static, monotonic piecewise map; every per-column input is an
+/// explicit PARAMETER (D-035): the seed spike max and the four modulated shelf
+/// values. The generator samples the modulation fields; this class never touches
+/// noise.
///
-/// 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:
+/// v4 (developer's task-07 gate finding: the terraces work, but uniform anchors put
+/// a flat ring at exactly 100 m and exactly 220 m on every mountain — the bathtub
+/// rings): the bench and plateau OUTPUT anchors become smooth spatial fields —
+/// benchLo = 100 m ± 12 m and plateauLo = 220 m ± 20 m via two decorrelated
+/// very-low-frequency noise fields — and a third field modulates SHELF STRENGTH,
+/// blending each shelf between "pronounced flat" (output span ~2 m) and "barely a
+/// hint" (~25 m of gentle slope), so not every flank at shelf height develops the
+/// full terrace. Shelves stay locally flat; the two magic altitudes stop existing.
///
-/// 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)
+/// Structure otherwise v3's, unchanged: identity ≤ sea, frozen toe/rise (storm
+/// ladder), foothill riser → bench → mid riser → plateau → per-seed-normalized
+/// stiff spike (u⁴, 420 m cap) → tail. Input knots are v3's calibration.
///
-/// 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.
+/// ORDERING SAFETY BY CONSTRUCTION (asserted): with the amplitudes below, at every
+/// column: red ceiling 0.27 < benchLo−… (bench min 0.5006), bench top max 0.6962 <
+/// plateauLo min 0.9468, plateau top max 1.2062 < peak cap 1.8287. The numeric
+/// assertion additionally sweeps all 8 modulation-extreme corners per generation.
///
public static class HeightCurve
{
- public const ushort VERSION = 3;
+ public const ushort VERSION = 4;
- // 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
+ // Input knots — v3 calibration (pooled batch-04 land CDF, P59/72/82/87/95/98).
+ public const float K1 = 0.509179f;
+ public const float K2 = 0.604081f;
+ public const float K3 = 0.698485f;
+ public const float K4 = 0.767213f;
+ public const float K5 = 0.930304f;
+ public const float K6 = 1.050720f;
- // Output anchors — storm ladder (frozen) + the terraces.
+ // Fixed output anchors — storm ladder + ceiling (frozen).
public const float SEA = 0.15f;
- 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 ORANGE_CEIL = 0.206f;
+ public const float RED_CEIL = 0.27f;
+ public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869
public const float TAIL_SLOPE = 0.25f;
-
- // 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;
+ // Modulated shelf anchors: base ± amplitude (raw units; 251 m per unit).
+ public const float BENCH_BASE = SEA + 100f / 251f; // ≈ 0.54841 (100 m)
+ public const float BENCH_AMP = 12f / 251f; // ± 12 m
+ public const float PLATEAU_BASE = SEA + 220f / 251f; // ≈ 1.02649 (220 m)
+ public const float PLATEAU_AMP = 20f / 251f; // ± 20 m
+
+ // Shelf strength: output span of each shelf segment, blended by the strength
+ // field. Strong (t=1) → SPAN_MIN (~2 m, pronounced flat). Weak (t=0) →
+ // 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;
+
+ // Modulation-field derivation (generator-side, recorded in TCRV): field seed =
+ // RESOLVED WorldSeed + offset; frequency = (periods per island width) / MapSize.
+ public const int BENCH_SEED_OFFSET = 7101;
+ public const int PLATEAU_SEED_OFFSET = 7207;
+ public const int STRENGTH_SEED_OFFSET = 7303;
+ public const float ELEV_FREQ_ISLANDS = 3.0f; // ~3 undulations across the island
+ public const float STRENGTH_FREQ_ISLANDS = 5.0f; // finer patchiness for shelf strength
+
public static float EffectiveSpikeMax(float hMaxSeed)
{
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN);
}
+ /// Shelf output span for a strength sample t ∈ [0,1].
+ public static float ShelfSpan(float strength01)
+ {
+ return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
+ }
+
/// Raw pre-curve height.
- /// The seed's raw pre-curve maximum — per-seed spike normalizer.
- public static float Apply(float h, float hMaxSeed)
+ /// Seed's raw pre-curve maximum (per-seed spike normalizer).
+ /// This column's bench anchor (BENCH_BASE ± BENCH_AMP).
+ /// This column's bench output span (ShelfSpan of the strength field).
+ /// This column's plateau anchor (PLATEAU_BASE ± PLATEAU_AMP).
+ /// This column's plateau output span.
+ public static float Apply(float h, float hMaxSeed,
+ float benchLo, float benchSpan, float plateauLo, float plateauSpan)
{
if (h <= SEA) return h;
@@ -77,72 +91,89 @@ public static class HeightCurve
if (h < K1)
{
u = (h - SEA) / (K1 - SEA);
- s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out toe, slope ≥ 0.3
+ s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
return SEA + s * (ORANGE_CEIL - SEA);
}
if (h < K2)
{
u = (h - K1) / (K2 - K1);
- return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear rise
+ return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
}
if (h < K3)
{
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);
+ s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser
+ return RED_CEIL + s * (benchLo - RED_CEIL);
}
if (h < K4)
{
u = (h - K3) / (K4 - K3);
- return FOOTHILL_BENCH + u * BENCH_STEP; // bench 1, near-flat
+ return benchLo + u * benchSpan; // bench — modulated
}
+ float benchTop = benchLo + benchSpan;
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);
+ s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser
+ return benchTop + s * (plateauLo - benchTop);
}
if (h < K6)
{
u = (h - K5) / (K6 - K5);
- return PLATEAU + u * BENCH_STEP; // white plateau, near-flat
+ return plateauLo + u * plateauSpan; // plateau — modulated
}
+ float plateauTop = plateauLo + plateauSpan;
float spikeMax = EffectiveSpikeMax(hMaxSeed);
if (h < spikeMax)
{
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);
+ s = 0.1f * u + 0.9f * (u * u * u * u); // summit spike (v2/v3 shape)
+ return plateauTop + s * (PEAK_CAP - plateauTop);
}
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
}
///
- /// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once
- /// per generation after hMaxSeed is known. Loud throw, refuses to generate.
+ /// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve:
+ /// sweeps the full domain at every one of the 8 modulation-extreme corners
+ /// (bench anchor ±, plateau anchor ±, strength min/max) with the per-seed
+ /// spikeMax — the adversarial corner set for the ordering constraints. Loud
+ /// throw, refuses to generate.
///
public static void AssertMonotonic(float hMaxSeed)
{
- float prevH = -7f;
- float prev = Apply(prevH, hMaxSeed);
+ float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP };
+ float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP };
+ float[] spans = { SHELF_SPAN_MIN, SHELF_SPAN_MAX };
- // Only strictly increasing float32 samples are compared (task-05 incident fix).
- void Check(double hd)
+ foreach (float bl in benchLos)
{
- float h = (float)hd;
- if (h <= prevH) return;
- float v = Apply(h, hMaxSeed);
- if (v <= prev)
- throw new System.InvalidOperationException(
- $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}): {v} <= {prev}. Refusing to generate.");
- prev = v;
- prevH = h;
- }
+ foreach (float pl in plateauLos)
+ {
+ foreach (float sp in spans)
+ {
+ float prevH = -7f;
+ float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp);
- 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);
- for (double h = top + 0.05; h <= top + 6.0; h += 0.05) Check(h);
- GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
+ void Check(double hd)
+ {
+ float h = (float)hd;
+ if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
+ float v = Apply(h, hMaxSeed, bl, sp, pl, sp);
+ if (v <= prev)
+ throw new System.InvalidOperationException(
+ $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
+ prev = v;
+ prevH = h;
+ }
+
+ double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
+ 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 = 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}).");
}
}
diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs
index 6547792..b311779 100644
--- a/Tools/Scripts/MapGenerator.cs
+++ b/Tools/Scripts/MapGenerator.cs
@@ -37,6 +37,15 @@ public partial class MapGenerator : TextureRect
private float[,] _heightMapClassify;
private bool _curveOn;
+ // Curve-v4 shelf-modulation fields (task 08): two decorrelated elevation fields
+ // (bench 100±12 m, plateau 220±20 m) plus a strength field blending each shelf
+ // between pronounced-flat and barely-a-hint. Seeds derive from the RESOLVED
+ // noise seed + fixed offsets (no config knob); frequencies scale with MapSize.
+ // Sampled per column in pass 2 only — the classify path never sees them.
+ private FastNoiseLite _benchNoise;
+ private FastNoiseLite _plateauNoise;
+ private FastNoiseLite _strengthNoise;
+
// 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
// GenerateTopography pass 1; recorded in TCRV (effective, guard applied).
@@ -75,7 +84,7 @@ public partial class MapGenerator : TextureRect
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
_heightMap = new float[MapSize, MapSize];
- _curveOn = ConfigManager.TerrainCurve == "v3";
+ _curveOn = ConfigManager.TerrainCurve == "v4";
// (The monotonicity assertion now runs inside GenerateTopography, against the
// effective per-seed curve, once hMaxSeed is known.)
_heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
@@ -86,11 +95,18 @@ public partial class MapGenerator : TextureRect
float scaleFactor = MapSize / 1024f; // Equals 4
- // --- MASTER SEED & DETAILED NOISE (Restored!) ---
+ // --- MASTER SEED & DETAILED NOISE (Restored!) ---
_noise = new FastNoiseLite();
- _noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed;
+ _noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed;
_noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
- _noise.Frequency = 0.004f / scaleFactor;
+ _noise.Frequency = 0.004f / scaleFactor;
+
+ if (_curveOn)
+ {
+ _benchNoise = MakeModulationNoise(HeightCurve.BENCH_SEED_OFFSET, HeightCurve.ELEV_FREQ_ISLANDS);
+ _plateauNoise = MakeModulationNoise(HeightCurve.PLATEAU_SEED_OFFSET, HeightCurve.ELEV_FREQ_ISLANDS);
+ _strengthNoise = MakeModulationNoise(HeightCurve.STRENGTH_SEED_OFFSET, HeightCurve.STRENGTH_FREQ_ISLANDS);
+ }
// THE CRATER FIX: Push it into the ocean (scales via percentage of MapSize!)
// Supposedly! We will have to test this manually on other map sizes to confirm the crater is properly scaled and submerged on the north coast!
@@ -256,17 +272,23 @@ public partial class MapGenerator : TextureRect
TrailRoads = _trailPaths,
WaterBodyIds = _waterBodyIds,
WaterBodies = _waterBodies ?? new List(),
- // 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).
+ // TCRV under curve v4: knot slots carry K1..K4 (K5/K6 are version constants);
+ // the bench slots carry the two BASE anchors; the v4 extension record carries
+ // the modulation parameters (amplitudes, spans, frequencies, seed offsets) —
+ // the blueprint stays self-describing.
TerrainCurve = _curveOn ? new TerrainCurveInfo
{
Version = HeightCurve.VERSION,
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.FOOTHILL_BENCH, PlateauHi = HeightCurve.PLATEAU,
- PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE
+ PlateauLo = HeightCurve.BENCH_BASE, PlateauHi = HeightCurve.PLATEAU_BASE,
+ PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE,
+ BenchAmp = HeightCurve.BENCH_AMP, PlateauAmp = HeightCurve.PLATEAU_AMP,
+ ShelfSpanMin = HeightCurve.SHELF_SPAN_MIN, ShelfSpanMax = HeightCurve.SHELF_SPAN_MAX,
+ ElevFreqIslands = HeightCurve.ELEV_FREQ_ISLANDS, StrengthFreqIslands = HeightCurve.STRENGTH_FREQ_ISLANDS,
+ BenchSeedOffset = HeightCurve.BENCH_SEED_OFFSET, PlateauSeedOffset = HeightCurve.PLATEAU_SEED_OFFSET,
+ StrengthSeedOffset = HeightCurve.STRENGTH_SEED_OFFSET
} : null,
FormatVersion = 2,
Params = new BlueprintParams
@@ -438,7 +460,21 @@ public partial class MapGenerator : TextureRect
{
float raw = _heightMap[x, y];
float classifyH = raw;
- float curvedH = _curveOn ? HeightCurve.Apply(raw, _hMaxSeed) : raw;
+ float curvedH;
+ if (_curveOn)
+ {
+ // v4: per-column shelf modulation — anchors and strength from the
+ // low-frequency fields; ordering safety by construction (amplitudes
+ // bounded; asserted at all 8 field-extreme corners per generation).
+ 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 shelfSpan = HeightCurve.ShelfSpan((_strengthNoise.GetNoise2D(x, y) + 1f) * 0.5f);
+ curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan);
+ }
+ else
+ {
+ curvedH = raw;
+ }
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
@@ -523,6 +559,15 @@ public partial class MapGenerator : TextureRect
// grid and the water-body grid holds BY CONSTRUCTION, not by parallel
// implementations agreeing.
// =====================================================================
+ private FastNoiseLite MakeModulationNoise(int seedOffset, float periodsPerIsland)
+ {
+ var n = new FastNoiseLite();
+ n.Seed = _noise.Seed + seedOffset; // deterministic from the RESOLVED seed
+ n.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
+ n.Frequency = periodsPerIsland / MapSize; // frequency stated in island-widths
+ return n;
+ }
+
private bool IsWaterPixel(int x, int y) => _heightMapClassify[x, y] < GetSeaLevel(_tempMap[x, y]);
private bool IsOceanPixel(int x, int y) => IsWaterPixel(x, y) && _isTrueOcean[x, y];
private bool IsLakePixel(int x, int y) => IsWaterPixel(x, y) && !_isTrueOcean[x, y];
@@ -824,12 +869,14 @@ 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.FOOTHILL_TOP) tint = brown; // foothill riser + 100 m bench
- else if (h < HeightCurve.PLATEAU) tint = darkBrown; // the mid riser
+ // v4 note: shelf anchors are spatially modulated, so these tint bands
+ // use the base±amplitude envelopes — approximate banding, visualization only.
+ else if (h < HeightCurve.BENCH_BASE + HeightCurve.BENCH_AMP) tint = brown; // riser + bench envelope
+ else if (h < HeightCurve.PLATEAU_BASE - HeightCurve.PLATEAU_AMP) tint = darkBrown; // mid riser
else
{
- // 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);
+ // Plateau envelope through the summit spike to the 420 m cap.
+ float t2 = Mathf.Clamp((h - (HeightCurve.PLATEAU_BASE - HeightCurve.PLATEAU_AMP)) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU_BASE + HeightCurve.PLATEAU_AMP), 0f, 1f);
tint = grey.Lerp(white, t2);
}
@@ -1373,40 +1420,40 @@ public partial class MapGenerator : TextureRect
}
private void SetBaseAStarWeights(AStarGrid2D astar) {
- for (int x = 0; x < MapSize; x++) {
- for (int y = 0; y < MapSize; y++) {
- Biome b = _biomeMap[x, y];
-
- // 1. CRITICAL FIX: Make the crater physically impassable first!
- if (b == Biome.Crater) {
- astar.SetPointSolid(new Vector2I(x, y), true);
- continue; // "Skip the rest, go to next pixel"
- }
+ for (int x = 0; x < MapSize; x++) {
+ for (int y = 0; y < MapSize; y++) {
+ Biome b = _biomeMap[x, y];
+
+ // 1. CRITICAL FIX: Make the crater physically impassable first!
+ if (b == Biome.Crater) {
+ astar.SetPointSolid(new Vector2I(x, y), true);
+ continue; // "Skip the rest, go to next pixel"
+ }
- // 2. Existing water/mainland checks
- if (b == Biome.Ocean || b == Biome.Lake || !_isMainland[x, y]) {
- astar.SetPointSolid(new Vector2I(x, y), true);
- continue; // "Skip the rest, go to next pixel"
- }
-
- // 3. We only reach this point if it's mainland AND not a crater!
- float h = _heightMap[x, y];
- float localSea = GetSeaLevel(_tempMap[x, y]);
- float normalizedElevation = Mathf.Clamp((h - localSea) / (1.0f - localSea), 0.0f, 1.0f);
+ // 2. Existing water/mainland checks
+ if (b == Biome.Ocean || b == Biome.Lake || !_isMainland[x, y]) {
+ astar.SetPointSolid(new Vector2I(x, y), true);
+ continue; // "Skip the rest, go to next pixel"
+ }
+
+ // 3. We only reach this point if it's mainland AND not a crater!
+ float h = _heightMap[x, y];
+ float localSea = GetSeaLevel(_tempMap[x, y]);
+ float normalizedElevation = Mathf.Clamp((h - localSea) / (1.0f - localSea), 0.0f, 1.0f);
- // Simple, steep curve to avoid mountains
- float weight = 1.0f + Mathf.Pow(normalizedElevation, 3.0f) * 400.0f;
+ // Simple, steep curve to avoid mountains
+ float weight = 1.0f + Mathf.Pow(normalizedElevation, 3.0f) * 400.0f;
- // Push off the beach (not an insane wall, just enough to prefer grass)
- if (b == Biome.Beach) weight += 15.0f;
-
- // Let it path through the wasteland normally
- if (b == Biome.Wasteland) weight += 3.0f;
+ // Push off the beach (not an insane wall, just enough to prefer grass)
+ if (b == Biome.Beach) weight += 15.0f;
+
+ // Let it path through the wasteland normally
+ if (b == Biome.Wasteland) weight += 3.0f;
- astar.SetPointWeightScale(new Vector2I(x, y), weight);
- }
- }
- }
+ astar.SetPointWeightScale(new Vector2I(x, y), weight);
+ }
+ }
+ }
private void PenalizePath(AStarGrid2D astar, Vector2[] path, int radius)
{
@@ -1419,7 +1466,7 @@ public partial class MapGenerator : TextureRect
int ny = Mathf.Clamp((int)p.Y + dy, 0, MapSize - 1);
var cell = new Vector2I(nx, ny);
if (!astar.IsPointSolid(cell)) {
- // The true Iron Curtain
+ // The true Iron Curtain
astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + 10000f);
}
}
@@ -1494,67 +1541,67 @@ public partial class MapGenerator : TextureRect
{
float scaleMod = MapSize / 1024f; // equals 4 at 4096
- // Trails: Thin brown
- foreach (var path in _trailPaths) DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true);
-
- // Rugged: Thicker dark brown
- foreach (var path in _ruggedPaths) DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true);
+ // Trails: Thin brown
+ foreach (var path in _trailPaths) DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true);
+
+ // Rugged: Thicker dark brown
+ foreach (var path in _ruggedPaths) DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true);
- // Main Roads: Solid Black
- foreach (var path in _branchPaths) DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true);
+ // Main Roads: Solid Black
+ foreach (var path in _branchPaths) DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true);
- // Highway: Thick Red
- foreach (var path in _highwayPaths) DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true);
+ // Highway: Thick Red
+ foreach (var path in _highwayPaths) DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true);
- // Draw Towns
- foreach (var town in _towns) {
- float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f);
- radius *= scaleMod; // Scale the circles up!
-
- Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange);
- if (town.Tier == TownTier.IslandLoot) c = Colors.Red;
- if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; }
+ // Draw Towns
+ foreach (var town in _towns) {
+ float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f);
+ radius *= scaleMod; // Scale the circles up!
+
+ Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange);
+ if (town.Tier == TownTier.IslandLoot) c = Colors.Red;
+ if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; }
- DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black);
- DrawCircle(town.Position, radius, c);
- }
+ DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black);
+ DrawCircle(town.Position, radius, c);
+ }
}
}
// Place this at the very bottom of the file!
public partial class MapDrawProxy : Control
{
- public MapGenerator Source;
+ public MapGenerator Source;
- public override void _Draw()
- {
- if (Source == null) return;
-
- float scaleMod = Source.MapSize / 1024f;
+ public override void _Draw()
+ {
+ if (Source == null) return;
+
+ float scaleMod = Source.MapSize / 1024f;
- foreach (var path in Source._trailPaths)
- DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true);
+ foreach (var path in Source._trailPaths)
+ DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true);
- foreach (var path in Source._ruggedPaths)
- DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true);
+ foreach (var path in Source._ruggedPaths)
+ DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true);
- foreach (var path in Source._branchPaths)
- DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true);
+ foreach (var path in Source._branchPaths)
+ DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true);
- foreach (var path in Source._highwayPaths)
- DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true);
+ foreach (var path in Source._highwayPaths)
+ DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true);
- foreach (var town in Source._towns)
- {
- float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f);
- radius *= scaleMod;
+ foreach (var town in Source._towns)
+ {
+ float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f);
+ radius *= scaleMod;
- Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange);
- if (town.Tier == TownTier.IslandLoot) c = Colors.Red;
- if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; }
+ Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange);
+ if (town.Tier == TownTier.IslandLoot) c = Colors.Red;
+ if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; }
- DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black);
- DrawCircle(town.Position, radius, c);
- }
- }
-}
\ No newline at end of file
+ DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black);
+ DrawCircle(town.Position, radius, c);
+ }
+ }
+}
diff --git a/Tools/Scripts/RoundTripHarness.cs b/Tools/Scripts/RoundTripHarness.cs
index 7b2393a..d912926 100644
--- a/Tools/Scripts/RoundTripHarness.cs
+++ b/Tools/Scripts/RoundTripHarness.cs
@@ -200,8 +200,12 @@ public partial class RoundTripHarness : Node
}
var ca = a.TerrainCurve; var cb = b.TerrainCurve;
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[] 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[] 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.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset };
+ 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.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset };
for (int i = 0; i < fa.Length; i++)
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }