diff --git a/Core/Scripts/BlueprintWriter.cs b/Core/Scripts/BlueprintWriter.cs
index e7ff721..25a7854 100644
--- a/Core/Scripts/BlueprintWriter.cs
+++ b/Core/Scripts/BlueprintWriter.cs
@@ -158,6 +158,9 @@ namespace IslaApocalypse.Core
writer.Write(d.Version); // u16
writer.Write(d.ReliefAmpM); writer.Write(d.ReliefFreqIslands); // 2 × f32
writer.Write(d.ReliefSeedOffset); // i32
+ writer.Write(d.EdgeAmpM); writer.Write(d.EdgeFreqIslands); // 2 × f32
+ writer.Write(d.EdgeSeedOffset); // i32
+ writer.Write(d.EdgeMaxShiftM); // f32
}
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs
index d0387b5..78bf933 100644
--- a/Core/Scripts/ConfigManager.cs
+++ b/Core/Scripts/ConfigManager.cs
@@ -33,11 +33,16 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
// are retired. Default: v5.
public static string TerrainCurve = "v5";
- // Terrain detail passes (task 10): "v1" = shelf micro-relief (requires the
- // curve; no-op when it is off), "off" disables it. ShelfReliefAmp is the
- // micro-relief amplitude in metres. Defaults: v1, 3 m.
+ // Terrain detail passes (task 10): "v1" = shelf micro-relief + shelf-edge
+ // variation as one judged unit (requires the curve; no-op when it is off);
+ // "off" disables both. ShelfReliefAmp is the micro-relief amplitude in metres
+ // of OUTPUT height. ShelfEdgeVariation is the shelf-edge warp amplitude in
+ // metres of INPUT height — how far the shelf/riser boundary contour is
+ // displaced, not an elevation change; it is clamped at load time to the
+ // largest shift that keeps the curve's knots ordered. Defaults: v1, 3 m, 5 m.
public static string TerrainDetail = "v1";
public static float ShelfReliefAmp = 3.0f;
+ public static float ShelfEdgeVariation = 5.0f;
public static void LoadConfig()
{
@@ -137,6 +142,10 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
{
ShelfReliefAmp = (float)data["ShelfReliefAmp"];
}
+ if (data.ContainsKey("ShelfEdgeVariation"))
+ {
+ ShelfEdgeVariation = (float)data["ShelfEdgeVariation"];
+ }
switch (profile)
{
diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs
index c5f7754..78243ff 100644
--- a/Core/Scripts/MapDataParser.cs
+++ b/Core/Scripts/MapDataParser.cs
@@ -89,16 +89,19 @@ namespace IslaApocalypse.Core
///
/// The terrain detail passes that shaped this blueprint's HGTS (v2 TDTL section,
- /// terrain-water task 10): the shelf micro-relief parameters. Null when detail
- /// was off. Metadata only — heights are already detailed. Body version 1 (the
- /// reverted relief + D8-incision layout) is longer and differently shaped; the
- /// parser refuses it rather than misreading it.
+ /// terrain-water task 10): shelf micro-relief + shelf-edge variation parameters.
+ /// Null when detail was off. Metadata only — heights are already detailed.
+ /// Version 1 (relief + the reverted D8 incision) never left the task-10 batch
+ /// tree; the parser rejects it rather than misreading its longer payload.
///
public class TerrainDetailInfo
{
public ushort Version;
public float ReliefAmpM, ReliefFreqIslands;
public int ReliefSeedOffset;
+ public float EdgeAmpM, EdgeFreqIslands;
+ public int EdgeSeedOffset;
+ public float EdgeMaxShiftM;
}
public class WorldBlueprint
@@ -454,14 +457,19 @@ namespace IslaApocalypse.Core
d.Version = reader.ReadUInt16();
if (d.Version != BlueprintFormat.TDTL_VERSION)
{
- // Skip the body and leave TerrainDetail null. The heights are still
- // whatever they are — we simply refuse to describe them wrongly.
- GD.PrintErr($"[MapDataParser] ⚠ TDTL body version {d.Version} is not the current {BlueprintFormat.TDTL_VERSION} — section skipped, detail metadata unavailable.");
+ // A TDTL v1 payload (the reverted relief+incision layout) is LONGER and
+ // laid out differently; reading it as v2 would silently mint plausible
+ // nonsense. Skip the body and leave TerrainDetail null — the heights are
+ // still whatever they are, we just refuse to describe them wrongly.
+ GD.PrintErr($"[MapDataParser] ⚠ TDTL version {d.Version} is not the current {BlueprintFormat.TDTL_VERSION} — section skipped, detail metadata unavailable.");
reader.BaseStream.Seek((long)payloadLength - 2L, SeekOrigin.Current);
return true;
}
d.ReliefAmpM = reader.ReadSingle(); d.ReliefFreqIslands = reader.ReadSingle();
d.ReliefSeedOffset = reader.ReadInt32();
+ d.EdgeAmpM = reader.ReadSingle(); d.EdgeFreqIslands = reader.ReadSingle();
+ d.EdgeSeedOffset = reader.ReadInt32();
+ d.EdgeMaxShiftM = reader.ReadSingle();
blueprint.TerrainDetail = d;
return true;
}
diff --git a/Tools/Scripts/HeightCurve.cs b/Tools/Scripts/HeightCurve.cs
index 2e18ab0..591a950 100644
--- a/Tools/Scripts/HeightCurve.cs
+++ b/Tools/Scripts/HeightCurve.cs
@@ -42,6 +42,11 @@ public sealed class CurveKnots
/// Unchanged from v4: storm-ladder anchors, bench 100±12 m, plateau 220±20 m,
/// strength modulation (span max 25 m), 420 m cap, per-seed spike normalization,
/// modulation fields/seed offsets, the classify-map invariant.
+///
+/// The curve SHAPE is frozen at v5. Task 10 adds no band, anchor or slope — only
+/// a per-column `edgeShift` parameter on Apply, which slides the shelf/riser knot
+/// block K3/K4/K5 so those three boundaries stop being clean iso-height contours.
+/// It is a new input to the same curve, not a new curve.
///
public static class HeightCurve
{
@@ -87,11 +92,29 @@ public static class HeightCurve
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
}
+ ///
+ /// The curve for ONE column. (task 10 pass B) slides
+ /// the shelf/riser knot BLOCK — K3/K4/K5 — up or down by a per-column amount,
+ /// leaving K1/K2/K6 fixed. Every shelf↔riser boundary is the contour where the
+ /// raw height crosses one of those three knots, so shifting them makes those
+ /// contours wander instead of tracing a clean iso-height line: the shelf edge
+ /// scallops. Because the block moves rigidly, the bench and mid-riser bands keep
+ /// their exact widths (their interior shapes are translated, not distorted); only
+ /// the foothill riser and the plateau stretch or compress to absorb the shift.
+ /// Monotonicity is structural, not conditional — the curve is monotonic for ANY
+ /// strictly ordered knot set, and TerrainDetailPass.MaxEdgeShift keeps the set
+ /// ordered by construction. Below K2 and above K6 the output is bit-identical to
+ /// an unwarped column, which is what makes the red-ceiling floor and the 420 m
+ /// peak cap exact under the warp.
+ ///
public static float Apply(float h, float hMaxSeed,
- float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k)
+ float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k,
+ float edgeShift)
{
if (h <= SEA) return h;
+ float k3 = k.K3 + edgeShift, k4 = k.K4 + edgeShift, k5 = k.K5 + edgeShift;
+
float u, s;
if (h < k.K1)
{
@@ -104,27 +127,27 @@ public static class HeightCurve
u = (h - k.K1) / (k.K2 - k.K1);
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
}
- if (h < k.K3)
+ if (h < k3)
{
- u = (h - k.K2) / (k.K3 - k.K2);
+ u = (h - k.K2) / (k3 - k.K2);
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // foothill riser — corner fix 1
return RED_CEIL + s * (benchLo - RED_CEIL);
}
- if (h < k.K4)
+ if (h < k4)
{
- u = (h - k.K3) / (k.K4 - k.K3);
+ u = (h - k3) / (k4 - k3);
return benchLo + u * benchSpan; // bench (min span 6 m — fix 3)
}
float benchTop = benchLo + benchSpan;
- if (h < k.K5)
+ if (h < k5)
{
- u = (h - k.K4) / (k.K5 - k.K4);
+ u = (h - k4) / (k5 - k4);
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // mid riser — corner fix 1
return benchTop + s * (plateauLo - benchTop);
}
if (h < k.K6)
{
- u = (h - k.K5) / (k.K6 - k.K5);
+ u = (h - k5) / (k.K6 - k5);
return plateauLo + u * plateauSpan; // plateau
}
float plateauTop = plateauLo + plateauSpan;
@@ -140,15 +163,25 @@ public static class HeightCurve
///
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve for
- /// the selected preset: all 8 modulation-extreme corners × per-seed spikeMax.
- /// The corner fixes lower the slope floors (risers 0.1, spike base 0.05) — the
- /// sweep proves they stay strictly positive everywhere. Loud throw on failure.
+ /// the selected preset: all 8 modulation-extreme corners × the shelf-edge warp
+ /// extremes (±maxEdgeShift and 0) × per-seed spikeMax — 24 corners. The corner
+ /// fixes lower the slope floors (risers 0.1, spike base 0.05) and the warp
+ /// squeezes the foothill riser and the plateau; the sweep proves every slope
+ /// stays strictly positive at the extremes of both. Also checks the knot set
+ /// itself stays strictly ordered under the warp. Loud throw on failure.
///
- public static void AssertMonotonic(float hMaxSeed, CurveKnots k)
+ public static void AssertMonotonic(float hMaxSeed, CurveKnots k, float maxEdgeShift)
{
+ if (maxEdgeShift < 0f || k.K2 + maxEdgeShift >= k.K3 || k.K5 + maxEdgeShift >= k.K6)
+ throw new System.InvalidOperationException(
+ $"[HeightCurve] EDGE-SHIFT BOUND VIOLATION: maxEdgeShift={maxEdgeShift} does not keep K2 0f
+ ? new float[] { -maxEdgeShift, 0f, maxEdgeShift }
+ : new float[] { 0f };
foreach (float bl in benchLos)
{
@@ -156,28 +189,31 @@ public static class HeightCurve
{
foreach (float sp in spans)
{
- float prevH = -7f;
- float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k);
-
- void Check(double hd)
+ foreach (float es in edgeShifts)
{
- float h = (float)hd;
- if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
- float v = Apply(h, hMaxSeed, bl, sp, pl, sp, k);
- if (v <= prev)
- throw new System.InvalidOperationException(
- $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
- prev = v;
- prevH = h;
- }
+ float prevH = -7f;
+ float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k, es);
- 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 = 0.10; hh <= top; hh += 0.0001) Check(hh);
- for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh);
+ 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, k, es);
+ if (v <= prev)
+ throw new System.InvalidOperationException(
+ $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}, edgeShift={es}): {v} <= {prev}. Refusing to generate.");
+ prev = v;
+ prevH = h;
+ }
+
+ 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 = 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} preset '{k.Name}', 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6}).");
+ GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION} preset '{k.Name}', 8 modulation corners × edge shifts ±{maxEdgeShift:F6}, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6}).");
}
}
diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs
index e4aecb1..9ae2d6e 100644
--- a/Tools/Scripts/MapGenerator.cs
+++ b/Tools/Scripts/MapGenerator.cs
@@ -49,11 +49,16 @@ public partial class MapGenerator : TextureRect
// v5: the selected knot preset; null = curve off.
private CurveKnots _curveKnots;
- // Task-10 detail pass (shelf micro-relief): gated by TerrainDetail, active only
- // with the curve on (the mask is curve-band defined). The relief noise seeds
- // from resolvedSeed + 7409.
+ // Task-10 detail passes (shelf micro-relief + shelf-edge variation): gated by
+ // TerrainDetail, active only with the curve on (both are defined in terms of
+ // the curve's bands). The relief noise seeds from resolvedSeed + 7409, the
+ // edge-warp noise from resolvedSeed + 7507. _edgeAmpRaw is the config dial in
+ // raw height units, already clamped to _maxEdgeShiftRaw.
private bool _detailOn;
private FastNoiseLite _reliefNoise;
+ private FastNoiseLite _edgeNoise;
+ private float _edgeAmpRaw;
+ private float _maxEdgeShiftRaw;
// 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
@@ -119,7 +124,23 @@ public partial class MapGenerator : TextureRect
}
_detailOn = _curveOn && ConfigManager.TerrainDetail == "v1";
if (_detailOn)
+ {
_reliefNoise = MakeModulationNoise(TerrainDetailPass.RELIEF_SEED_OFFSET, TerrainDetailPass.RELIEF_FREQ_ISLANDS);
+ _edgeNoise = MakeModulationNoise(TerrainDetailPass.EDGE_SEED_OFFSET, TerrainDetailPass.EDGE_FREQ_ISLANDS);
+
+ // The edge warp slides K3/K4/K5; the dial is clamped to the largest shift
+ // that keeps the knot set strictly ordered, so monotonicity can never be
+ // a tuning question. Clamping is loud — a silently ignored dial is worse
+ // than a refused one.
+ _maxEdgeShiftRaw = TerrainDetailPass.MaxEdgeShift(_curveKnots);
+ _edgeAmpRaw = Mathf.Max(ConfigManager.ShelfEdgeVariation, 0f) / 251f;
+ if (_edgeAmpRaw > _maxEdgeShiftRaw)
+ {
+ GD.PrintErr($"[MapGenerator] ShelfEdgeVariation {ConfigManager.ShelfEdgeVariation:F2} m exceeds the preset's safe bound {_maxEdgeShiftRaw * 251f:F2} m — clamping.");
+ _edgeAmpRaw = _maxEdgeShiftRaw;
+ }
+ GD.Print($"[MapGenerator] TerrainDetail v1: relief ±{ConfigManager.ShelfReliefAmp:F1} m @ {TerrainDetailPass.RELIEF_FREQ_ISLANDS:F0}/island, edge warp ±{_edgeAmpRaw * 251f:F2} m of input height @ {TerrainDetailPass.EDGE_FREQ_ISLANDS:F0}/island (bound {_maxEdgeShiftRaw * 251f:F2} m).");
+ }
else if (ConfigManager.TerrainDetail == "v1" && !_curveOn)
GD.Print("[MapGenerator] TerrainDetail v1 requires the curve — no-op with TerrainCurve off.");
@@ -314,7 +335,11 @@ public partial class MapGenerator : TextureRect
Version = TerrainDetailPass.VERSION,
ReliefAmpM = ConfigManager.ShelfReliefAmp,
ReliefFreqIslands = TerrainDetailPass.RELIEF_FREQ_ISLANDS,
- ReliefSeedOffset = TerrainDetailPass.RELIEF_SEED_OFFSET
+ ReliefSeedOffset = TerrainDetailPass.RELIEF_SEED_OFFSET,
+ EdgeAmpM = _edgeAmpRaw * 251f, // as APPLIED (post-clamp), not as configured
+ EdgeFreqIslands = TerrainDetailPass.EDGE_FREQ_ISLANDS,
+ EdgeSeedOffset = TerrainDetailPass.EDGE_SEED_OFFSET,
+ EdgeMaxShiftM = _maxEdgeShiftRaw * 251f
} : null,
FormatVersion = 2,
Params = new BlueprintParams
@@ -471,16 +496,20 @@ public partial class MapGenerator : TextureRect
// 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
// curve — after hMaxSeed is known, before any pixel is curved.
- if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed, _curveKnots);
+ if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed, _curveKnots, _detailOn ? _edgeAmpRaw : 0f);
- // --- PASS 2: curve (task 05/06) + crater carve ---
+ // --- PASS 2: curve (task 05/06) + detail (task 10) + crater carve ---
// Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so
// the carve cuts into curved terrain and the rim/bowl shape is untouched by
// the curve. Identity at and below sea + this ordering preserve the
// Trench/ocean-border guarantee and the crater by construction. classifyH
// stays uncurved — see _heightMapClassify; hMaxSeed never touches it.
// classify stays RAW; curved gets the v5 curve plus, when TerrainDetail is on,
- // the shelf-ness-weighted noise skin (risers and peaks untouched).
+ // the shelf-edge warp (pass B — the knot block slides per column, so the
+ // shelf/riser boundary contours scallop) and the shelf-ness-weighted noise
+ // skin (pass A — risers and peaks untouched). Both are read-only consumers of
+ // `raw`; neither can move a column below the red ceiling or above the cap,
+ // because K2 and K6 never move and the curve is monotonic between them.
float reliefAmpRaw = ConfigManager.ShelfReliefAmp / 251f;
float physicalCraterRadius = _impactRadius * 0.80f;
for (int x = 0; x < MapSize; x++)
@@ -492,16 +521,19 @@ public partial class MapGenerator : TextureRect
float curvedH;
if (_curveOn)
{
- // per-column shelf modulation (v4) — anchors and strength from the
- // low-frequency fields; ordering safety by construction.
+ // 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, _curveKnots);
+
+ float edgeShift = _detailOn ? _edgeNoise.GetNoise2D(x, y) * _edgeAmpRaw : 0f;
+ curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan, _curveKnots, edgeShift);
if (_detailOn)
{
- float wShelf = TerrainDetailPass.ShelfWeight(raw, _curveKnots);
+ float wShelf = TerrainDetailPass.ShelfWeight(raw, _curveKnots, edgeShift);
if (wShelf > 0f)
curvedH += _reliefNoise.GetNoise2D(x, y) * reliefAmpRaw * wShelf;
}
@@ -512,10 +544,8 @@ public partial class MapGenerator : TextureRect
}
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
- // Both maps are carved from LOCALS and written once each, so the
- // curve-off aliasing (classify IS the height array) cannot double-carve
- // — the failure mode that the separate carve loop introduced and that
- // the task-10 continuity oracle caught (fix 1b98fb5, now structural).
+ // Both maps are carved from LOCALS and written once, so the curve-off
+ // aliasing (classify and height are the same array) cannot double-carve.
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
diff --git a/Tools/Scripts/RoundTripHarness.cs b/Tools/Scripts/RoundTripHarness.cs
index 803a16c..1d57827 100644
--- a/Tools/Scripts/RoundTripHarness.cs
+++ b/Tools/Scripts/RoundTripHarness.cs
@@ -229,8 +229,8 @@ public partial class RoundTripHarness : Node
return false;
}
var da = a.TerrainDetail; var db = b.TerrainDetail;
- float[] fa = { da.Version, da.ReliefAmpM, da.ReliefFreqIslands, da.ReliefSeedOffset };
- float[] fb = { db.Version, db.ReliefAmpM, db.ReliefFreqIslands, db.ReliefSeedOffset };
+ float[] fa = { da.Version, da.ReliefAmpM, da.ReliefFreqIslands, da.ReliefSeedOffset, da.EdgeAmpM, da.EdgeFreqIslands, da.EdgeSeedOffset, da.EdgeMaxShiftM };
+ float[] fb = { db.Version, db.ReliefAmpM, db.ReliefFreqIslands, db.ReliefSeedOffset, db.EdgeAmpM, db.EdgeFreqIslands, db.EdgeSeedOffset, db.EdgeMaxShiftM };
for (int i = 0; i < fa.Length; i++)
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i]))
{
diff --git a/Tools/Scripts/TerrainDetailPass.cs b/Tools/Scripts/TerrainDetailPass.cs
index 4a9d70c..3b6f439 100644
--- a/Tools/Scripts/TerrainDetailPass.cs
+++ b/Tools/Scripts/TerrainDetailPass.cs
@@ -8,16 +8,19 @@ using Godot;
/// default 3 m) weighted by shelf-ness, so the compressed shelves get their
/// rolling texture back while risers and peaks stay untouched.
///
-/// Output-height only; the classify map never sees it.
+/// PASS B — shelf-edge variation: a per-column shift of the shelf/riser KNOT
+/// BLOCK (K3/K4/K5) by a low-frequency noise field, so the boundary where a
+/// shelf meets its riser wanders in and out instead of tracing a clean height
+/// contour — organic notches, coves and peninsulas at the shelf edge.
///
-/// NOTE — what this file deliberately no longer contains: the task-10 draft's D8
-/// steepest-descent flow routing, accumulation and drainage incision. It shipped,
-/// and it produced the canonical grid artifact — thousands of straight,
-/// disconnected, pooling scratches running along the eight D8 neighbour
-/// directions, because per-cell steepest descent on a regular grid can only ever
-/// route along those eight headings. It is reverted whole. Rivers and erosion are
-/// Phase C work: a hydraulic-erosion pass over FINAL terrain, not a routing carve
-/// on a grid mid-pipeline.
+/// Both are output-height only; the classify map never sees either of them.
+///
+/// NOTE — what this file deliberately does NOT contain: the task-10 draft's D8
+/// flow routing / accumulation / drainage incision. It shipped, produced the
+/// canonical grid artifact (thousands of straight, disconnected, pooling
+/// scratches along the D8 neighbour directions) and was reverted whole. Rivers
+/// and erosion are Phase C work — a hydraulic-erosion pass over FINAL terrain,
+/// not a per-cell steepest-descent carve on a grid.
///
public static class TerrainDetailPass
{
@@ -29,14 +32,38 @@ public static class TerrainDetailPass
public const float RELIEF_FREQ_ISLANDS = 40f; // ~40 undulations per island width (~200 m features)
public const int RELIEF_SEED_OFFSET = 7409;
+ // Pass B — shelf-edge variation. The amplitude is stated in metres of INPUT
+ // height (raw × 251): it is how far, in raw-height terms, a shelf boundary
+ // contour is displaced — not an output elevation change. Lateral wander on
+ // the map is that displacement divided by the local raw gradient.
+ public const float EDGE_AMP_DEFAULT_M = 5f; // config dial: ShelfEdgeVariation (metres of input height)
+ public const float EDGE_FREQ_ISLANDS = 12f; // ~12 undulations per island width — a handful of notches
+ public const int EDGE_SEED_OFFSET = 7507; // per shelf perimeter, not hundreds of teeth
+ public const float EDGE_SAFETY_FRACTION = 0.5f; // shift ≤ half the smallest margin to a fixed knot
+
+ ///
+ /// The largest per-column knot shift this preset can take while keeping the
+ /// knot set strictly ordered (K2 < K3+d, K5+d < K6) with margin to spare.
+ /// K1/K2/K6 never move, so the toe, the orange/red bands and the summit spike
+ /// are bit-identical whatever the warp does — which is what makes the
+ /// red-ceiling and peak-height guarantees exact rather than statistical.
+ ///
+ public static float MaxEdgeShift(CurveKnots k)
+ {
+ return EDGE_SAFETY_FRACTION * Mathf.Min(k.K3 - k.K2, k.K6 - k.K5);
+ }
+
///
/// Shelf-ness weight from the RAW input height: 1 mid-shelf, feathering to 0
/// through the risers (feather extends 30 % of the band half-width past each
- /// shelf edge). Covers both shelves.
+ /// shelf edge). Covers both shelves. is the same
+ /// per-column warp the curve is evaluated with, so the micro-relief skin
+ /// follows the shelf wherever pass B has moved its boundary.
///
- public static float ShelfWeight(float raw, CurveKnots k)
+ public static float ShelfWeight(float raw, CurveKnots k, float edgeShift)
{
- return Mathf.Max(BandBump(raw, k.K3, k.K4), BandBump(raw, k.K5, k.K6));
+ return Mathf.Max(BandBump(raw, k.K3 + edgeShift, k.K4 + edgeShift),
+ BandBump(raw, k.K5 + edgeShift, k.K6));
}
private static float BandBump(float h, float lo, float hi)