feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10)

PASS B, replacing the reverted incision. The developer's sketch asked for
the shelf/riser BOUNDARY to be organic, not for water: notches, coves and
small peninsulas where a flat shelf meets its riser, instead of the clean
oval contour the curve produces.

Mechanism (the task's preferred "boundary warp", in its most
monotonic-safe form): every shelf/riser boundary is the contour where the
raw height crosses K3, K4 or K5, so the boundary is warped by SLIDING
THOSE THREE KNOTS per column -- edgeShift = simplex(resolvedSeed + 7507,
12 per island width) x ShelfEdgeVariation. The contours then wander in and
out of the terrain instead of tracing an iso-height line. Noise-warped by
construction, so there is no grid direction for an artifact to line up on
-- the failure mode of the thing this replaces.

Why slide knots rather than perturb a weight or the input height:
monotonicity becomes structural instead of conditional. The curve is
strictly monotonic for ANY ordered knot set, so no derivative bound, no
amplitude-vs-feather-width tuning, no way for a dial to invert a column.
MaxEdgeShift keeps the set ordered (half the smallest margin to a fixed
knot = 12.3 m of input height for the v5 knots); the config dial is
clamped to it, loudly. AssertMonotonic now sweeps 24 corners -- the 8
modulation extremes x {-max, 0, +max} shift -- and checks the bound first.

K1, K2 and K6 never move, which buys the guarantees exactly rather than
statistically: below K2 and above K6 a warped column is bit-identical to
an unwarped one, so the red ceiling still floors every shelf edge (storm
ladder safe), the 420 m cap still caps, and the toe and summit spikes are
untouched. The block slides rigidly, so the bench and mid-riser keep their
exact widths -- shelf interiors stay flat, riser interiors keep their
profile, and only the foothill riser and plateau stretch to absorb it.
The micro-relief mask takes the same shift, so pass A's skin follows the
shelf wherever pass B moved its edge.

Dial ShelfEdgeVariation (default 5 m of INPUT height -- a boundary
displacement, not an elevation change) under the existing TerrainDetail
gate; both passes stay one judged unit. TDTL v2 body records relief and
edge amp/frequency/seed-offset plus the applied clamp bound; writer,
parser and harness follow. No blueprint was written between the revert
and this commit, so v2 only ever means relief + edge warp on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-08 20:31:41 -04:00
parent 3b6d166458
commit 492b56a87c
7 changed files with 182 additions and 69 deletions

View file

@ -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)

View file

@ -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)
{

View file

@ -89,16 +89,19 @@ namespace IslaApocalypse.Core
/// <summary>
/// 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.
/// </summary>
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;
}