feat: lock lowground routing; smooth river courses with the road pass's RDP+Chaikin (terrain-water task 23)

LOWGROUND is the gate verdict and the locked default ('short' stays available
behind the dial for the record). Every river polyline (upland stem + lowland
reach) is now decimated with RDP(4.0) and rounded with 4 Chaikin passes —
numerically mirroring MapGenerator.SmoothPath, since this pass is Godot-free —
before the bed profile is built, so the carved centreline carries none of the
8-connected Dijkstra 45° kinks (task-22 §4 caveat a). The monotone-descent and
sea-clamp constraints are applied to the smoothed course, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-11 19:37:29 -04:00
parent 1eacd22972
commit 148602b4c5
2 changed files with 54 additions and 2 deletions

View file

@ -113,7 +113,9 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
// terrain-aware), "lowground" follows the lowest ground and wanders like a // terrain-aware), "lowground" follows the lowest ground and wanders like a
// real river — the task-22 gate decides which ships; "lowground" is the // real river — the task-22 gate decides which ships; "lowground" is the
// provisional default pending that verdict. Width/depth scales are taste // provisional default pending that verdict. Width/depth scales are taste
// dials on the flow-proportional bed profile. RiverSeaMargin is the bed's // dials on the flow-proportional bed profile. LOWGROUND is the LOCKED
// default — the task-22/23 gate verdict ("short" stays available for the
// record). RiverSeaMargin is the bed's
// absolute floor above sea — the erosion flood-guard discipline: no river // absolute floor above sea — the erosion flood-guard discipline: no river
// bed may create inland below-sea cells, so the rendered coastline cannot // bed may create inland below-sea cells, so the rendered coastline cannot
// move even with rivers carved. // move even with rivers carved.

View file

@ -151,6 +151,54 @@ public static class RiverCarvePass
return stats; return stats;
} }
// ---- Route smoothing (task 23): the road pass's AAA pipeline, numerically ----
// RDP(4.0) decimation + 4 Chaikin corner-cutting passes, endpoints pinned —
// the same constants and structure as MapGenerator.SmoothPath, mirrored here
// because this pass is Godot-free. Kills the 8-connected Dijkstra 45° kinks;
// the bed then carves along the smoothed centreline.
private static List<(float x, float y)> SmoothCourse(List<(float x, float y)> raw)
{
if (raw.Count < 3) return raw;
var dec = Rdp(raw, 0, raw.Count - 1, 4.0f);
if (dec.Count < 3) return raw;
var sm = dec;
for (int pass = 0; pass < 4; pass++)
{
var nxt = new List<(float x, float y)>(sm.Count * 2) { sm[0] };
for (int i = 0; i + 1 < sm.Count; i++)
{
var a = sm[i]; var b = sm[i + 1];
nxt.Add((a.x * 0.75f + b.x * 0.25f, a.y * 0.75f + b.y * 0.25f));
nxt.Add((a.x * 0.25f + b.x * 0.75f, a.y * 0.25f + b.y * 0.75f));
}
nxt.Add(sm[^1]);
sm = nxt;
}
return sm;
}
private static List<(float x, float y)> Rdp(List<(float x, float y)> pts, int i0, int i1, float tol)
{
if (i1 - i0 <= 1) return new List<(float x, float y)> { pts[i0], pts[i1] };
var a = pts[i0]; var b = pts[i1];
float abx = b.x - a.x, aby = b.y - a.y;
float abLen = MathF.Sqrt(abx * abx + aby * aby);
float maxD = 0f; int maxI = i0;
for (int i = i0 + 1; i < i1; i++)
{
float d = abLen < 1e-6f
? MathF.Sqrt((pts[i].x - a.x) * (pts[i].x - a.x) + (pts[i].y - a.y) * (pts[i].y - a.y))
: MathF.Abs(abx * (a.y - pts[i].y) - (a.x - pts[i].x) * aby) / abLen;
if (d > maxD) { maxD = d; maxI = i; }
}
if (maxD <= tol) return new List<(float x, float y)> { pts[i0], pts[i1] };
var left = Rdp(pts, i0, maxI, tol);
var right = Rdp(pts, maxI, i1, tol);
left.RemoveAt(left.Count - 1);
left.AddRange(right);
return left;
}
/// <summary> /// <summary>
/// Deterministic Dijkstra from the start cell to the nearest ocean cell under /// Deterministic Dijkstra from the start cell to the nearest ocean cell under
/// the selected style's cost model. Returns the path start → ocean (1-px steps), /// the selected style's cost model. Returns the path start → ocean (1-px steps),
@ -233,10 +281,12 @@ public static class RiverCarvePass
float[,] height, int n, Func<int, int, float> seaAt, float[,] height, int n, Func<int, int, float> seaAt,
float coreSq, float craterCx, float craterCy, Params p, Stats stats) float coreSq, float craterCx, float craterCy, Params p, Stats stats)
{ {
// Full head→mouth polyline: upland stem, then the lowland reach if any. // Full head→mouth polyline: upland stem, then the lowland reach if any —
// then SMOOTHED (task 23) so the carved centreline carries no routing kinks.
var pts = new List<(float x, float y)>(upland); var pts = new List<(float x, float y)>(upland);
if (lowlandRoute != null && lowlandRoute.Count > 1) if (lowlandRoute != null && lowlandRoute.Count > 1)
pts.AddRange(lowlandRoute.GetRange(1, lowlandRoute.Count - 1)); pts.AddRange(lowlandRoute.GetRange(1, lowlandRoute.Count - 1));
pts = SmoothCourse(pts);
// Densify to ~1-px samples (plan courses are decimated ×4). // Densify to ~1-px samples (plan courses are decimated ×4).
var dense = new List<(float x, float y)>(); var dense = new List<(float x, float y)>();