feat: river bed carving with SHORT/LOWGROUND lowland routing A/B (terrain-water task 22, C0b part 2a)

Executes the frozen task-21b plan as real terrain — the first river pass to
modify the render map. NO WATER (part 2b waters the gated routing style).

RiverCarvePass (D-035 numeric): reruns DrainageAnalysis in-pipeline
(deterministic — same seed, same eroded surface, same plan), routes each routed
giant's lowland reach to the nearest ocean by deterministic Dijkstra under two
config-gated cost models — 'short' (distance + uphill penalty; heads direct) vs
'lowground' (cost ~ elevation above sea; follows the lowest ground and
meanders) — then carves every promoted course as a parabolic channel with a
smoothstep shoulder. Width/depth grow downstream with sqrt(drainage); bed
profile forced monotone non-increasing toward the outlet (water must flow) and
clamped to sea + RiverSeaMargin EVERYWHERE, with below-sea cells read-only and
the crater core excluded — the erosion flood-guard discipline, asserted per
generation by the water-pixel A/B (throws on any change).

Pipeline slot: AFTER GenerateTowns (towns read the render map for land/slope/
water checks — carving first would move towns and destabilise every A/B) and
BEFORE roads (a full run's A* should see the beds).

ISLA_SERVER_CONFIG env override added to ConfigManager: batch/A-B tooling loads
an alternate config file and the developer's live ServerConfig.json is never
written by tooling again (the task-19 restore near-miss class is retired).

Measured, seed 1280587109 (both variants): oracle md5-identical to baseline
(BIOM/WBID bitwise equal), 0 newly-below-sea cells, below-sea untouched, island
top 457.65 m exact, lowest carved cell exactly sea+margin (37.85 m), and
Rivers='off' is bitwise identical to the task-19 blueprint. All routed giants
reach the ocean in both styles. Wander (polyline/straight): SHORT 1.02-1.06 vs
LOWGROUND 1.24-1.38; SHORT carves 811k m3 (cuts through rises, max cumulative
cut 14.6 m), LOWGROUND 481k m3 (goes around, 14.3 m). Pass cost ~25 s (plan
21 s, routing 1.4-2.5 s, carve 0.3 s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-11 19:16:37 -04:00
parent a53d4e8512
commit 6960c2a2e8
3 changed files with 507 additions and 0 deletions

View file

@ -105,6 +105,24 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
public const float CRATER_EROSION_CORE_DEFAULT = 0.80f;
public const float CRATER_EROSION_FEATHER_DEFAULT = 1.05f;
// Rivers (task 22, C0b part 2a): carve the frozen task-21b river plan's
// beds into the RENDER map — ocean trunks, routed giants (lowland reach to
// the sea), lake-enders. NO WATER yet (part 2b). "off" until the routing-
// style gate; the A/B batch turns it on explicitly. RiverRoutingStyle picks
// the lowland routing for routed giants: "short" heads direct (lightly
// terrain-aware), "lowground" follows the lowest ground and wanders like a
// real river — the task-22 gate decides which ships; "lowground" is the
// provisional default pending that verdict. Width/depth scales are taste
// dials on the flow-proportional bed profile. RiverSeaMargin is the bed's
// absolute floor above sea — the erosion flood-guard discipline: no river
// bed may create inland below-sea cells, so the rendered coastline cannot
// move even with rivers carved.
public static string Rivers = "off";
public static string RiverRoutingStyle = "lowground";
public static float RiverWidthScale = 1.0f;
public static float RiverDepthScale = 1.0f;
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
// Island falloff shaping (task 11).
//
// CoastProfile: "wide" adds the submarine shelf — the height curve is identity
@ -140,6 +158,24 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
{
string path = "res://ServerConfig.json";
// Iteration override (task 22): ISLA_SERVER_CONFIG names an alternate
// config FILE to load instead of the repo's ServerConfig.json. Batch and
// A/B runs point this at a scratch config, so the developer's live
// ServerConfig.json is never written by tooling again — the whole
// backup/restore dance (and its task-19 near-miss) goes away. Loud, so
// a forgotten env var cannot silently masquerade as the repo config.
string envPath = OS.GetEnvironment("ISLA_SERVER_CONFIG");
if (!string.IsNullOrEmpty(envPath))
{
if (FileAccess.FileExists(envPath))
{
path = envPath;
GD.Print($"[ConfigManager] ⚠ ISLA_SERVER_CONFIG override: loading '{envPath}' (NOT the repo ServerConfig.json).");
}
else
GD.PrintErr($"[ConfigManager] ISLA_SERVER_CONFIG set but '{envPath}' does not exist — falling back to the repo config.");
}
if (!FileAccess.FileExists(path))
{
GD.PrintErr("[ConfigManager] ServerConfig.json not found! Defaulting to 8K.");
@ -272,6 +308,30 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
ErosionCarveCap = Mathf.Clamp(ErosionCarveCap, 0f, 60f);
if (rawCount != ErosionDropletCount || rawLife != ErosionDropletLifetime || rawCap != ErosionCarveCap)
GD.PrintErr($"[ConfigManager] Erosion governor out of bounds — clamped: count {rawCount}->{ErosionDropletCount}, lifetime {rawLife}->{ErosionDropletLifetime}, cap {rawCap}->{ErosionCarveCap} m.");
// Rivers gate + dials (task 22)
if (data.ContainsKey("Rivers"))
{
string rv = (string)data["Rivers"];
if (rv == "off" || rv == "v1")
Rivers = rv;
else
GD.PrintErr($"[ConfigManager] Unknown Rivers '{rv}'. Keeping '{Rivers}'.");
}
if (data.ContainsKey("RiverRoutingStyle"))
{
string st = (string)data["RiverRoutingStyle"];
if (st == "short" || st == "lowground")
RiverRoutingStyle = st;
else
GD.PrintErr($"[ConfigManager] Unknown RiverRoutingStyle '{st}'. Keeping '{RiverRoutingStyle}'.");
}
if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"];
if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"];
if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"];
RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f);
RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f);
RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f);
// Crater erosion treatment (task 19)
if (data.ContainsKey("CraterErosionMode"))
{