feat: droplet hydraulic erosion — carve+deposit, three governors, sea clamp (terrain-water task 17, Phase C0)

Erosion: "off"|"v1" gate (default off, pending the developer's gate). Output-only:
applied to the render map after detail, before the crater carve; the classify path
reads pre-erosion heights, so biomes/water stay byte-identical (verified: OFF run
bit-identical to the task-14 reference; ON run BIOM/WBID identical to OFF).
Governors: droplet count / lifetime / per-cell carve cap (ledger-enforced, thrown
on violation). Sea clamp: below-sea cells read-only, land never carved below
sea+margin — rendered coastline provably fixed (asserted per generation). Crater
excluded at 1.2×R. Deterministic from resolvedSeed+9271 (PCG32). New EROS
blueprint section (writer/parser/harness/format doc). Supersedes the reverted D8
approach with the organic droplet model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-10 02:42:26 -04:00
parent 34b0e1c9e3
commit e1e1fe3227
8 changed files with 611 additions and 22 deletions

View file

@ -122,6 +122,7 @@ public partial class RoundTripHarness : Node
ok &= CompareWater(a, b);
ok &= CompareTerrainCurve(a, b);
ok &= CompareTerrainDetail(a, b);
ok &= CompareErosion(a, b);
if (ok)
GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " +
@ -241,6 +242,33 @@ public partial class RoundTripHarness : Node
return true;
}
private bool CompareErosion(WorldBlueprint a, WorldBlueprint b)
{
if (a.Erosion == null && b.Erosion == null)
{
GD.Print("[Harness] EROS: absent in source — nothing to compare (and none reappeared).");
return true;
}
if (a.Erosion == null || b.Erosion == null)
{
GD.PrintErr("[Harness] EROS presence mismatch between source and reread.");
return false;
}
var ea = a.Erosion; var eb = b.Erosion;
bool same = ea.Version == eb.Version
&& ea.DropletCount == eb.DropletCount && ea.Lifetime == eb.Lifetime
&& ea.BrushRadius == eb.BrushRadius && ea.SeedOffset == eb.SeedOffset;
float[] fa = { ea.CarveCapM, ea.SeaMarginM, ea.Inertia, ea.CapacityFactor, ea.MinSlopeM,
ea.ErodeRate, ea.DepositRate, ea.Evaporation, ea.Gravity, ea.CraterExclFactor };
float[] fb = { eb.CarveCapM, eb.SeaMarginM, eb.Inertia, eb.CapacityFactor, eb.MinSlopeM,
eb.ErodeRate, eb.DepositRate, eb.Evaporation, eb.Gravity, eb.CraterExclFactor };
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] EROS fields differ."); return false; }
GD.Print($"[Harness] EROS equal (erosion v{ea.Version}).");
return true;
}
private bool CompareRoads(string tier, List<Vector2[]> a, List<Vector2[]> b)
{
if (a.Count != b.Count)