From 148602b4c52ef266cc59dbb99a2d02bc42a6be30 Mon Sep 17 00:00:00 2001 From: beezm Date: Tue, 11 Aug 2026 19:37:29 -0400 Subject: [PATCH] feat: lock lowground routing; smooth river courses with the road pass's RDP+Chaikin (terrain-water task 23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Core/Scripts/ConfigManager.cs | 4 ++- Tools/Scripts/RiverCarvePass.cs | 52 ++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs index 2ae8816..f7443db 100644 --- a/Core/Scripts/ConfigManager.cs +++ b/Core/Scripts/ConfigManager.cs @@ -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 // 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 + // 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 // bed may create inland below-sea cells, so the rendered coastline cannot // move even with rivers carved. diff --git a/Tools/Scripts/RiverCarvePass.cs b/Tools/Scripts/RiverCarvePass.cs index a5d803b..26f5530 100644 --- a/Tools/Scripts/RiverCarvePass.cs +++ b/Tools/Scripts/RiverCarvePass.cs @@ -151,6 +151,54 @@ public static class RiverCarvePass 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; + } + /// /// 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), @@ -233,10 +281,12 @@ public static class RiverCarvePass float[,] height, int n, Func seaAt, 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); if (lowlandRoute != null && lowlandRoute.Count > 1) pts.AddRange(lowlandRoute.GetRange(1, lowlandRoute.Count - 1)); + pts = SmoothCourse(pts); // Densify to ~1-px samples (plan courses are decimated ×4). var dense = new List<(float x, float y)>();