using System; using System.Collections.Generic; /// /// River bed carving — C0b part 2a (terrain-water task 22). The first river pass /// that MODIFIES terrain: executes the frozen task-21b plan by carving channel /// beds for the promoted rivers. NO WATER — part 2b puts water into these beds /// once the routing-style gate picks SHORT or LOWGROUND. /// /// Standalone numeric (D-035 family). Runs the task-21 DrainageAnalysis in- /// pipeline (deterministic: same seed → same eroded surface → same plan), then: /// /// 1. LOWLAND ROUTING (the A/B): each routed giant gets a route from its /// pooling terminal to the nearest OCEAN cell by deterministic Dijkstra. /// SHORT — cost ≈ distance, uphill penalised: heads direct, avoids walls. /// LOWGROUND — cost ≈ elevation above sea: follows the lowest available /// ground and wanders like a real lowland river. /// 2. BED CARVING: every promoted course (trunks, routed giants + their lowland /// reaches, lake-enders, tributaries) is stamped as a parabolic channel with /// a smoothstep shoulder — a bed for water to sit in, not a canyon. Width and /// depth grow downstream with drainage. The bed elevation is made MONOTONE /// NON-INCREASING toward the outlet (water must flow), and is clamped to /// sea + margin everywhere — the flood-guard discipline erosion established: /// below-sea cells are read-only, no carve may create inland below-sea cells, /// so the rendered coastline cannot move. The bed meets the ocean AT the /// coast, where the terrain itself descends through sea level. /// 3. Crater: nothing inside the protected core is modified (erosion's rule). /// /// Output-only: the caller applies this to the RENDER map after classify-side /// data (biomes, WBID) is already computed — the oracle stays byte-identical. /// public static class RiverCarvePass { public const float M_PER_UNIT = 251f; public const byte STYLE_SHORT = 0; public const byte STYLE_LOWGROUND = 1; private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 }; private static readonly int[] DY = { -1, 0, 1, -1, 1, -1, 0, 1 }; private static readonly float[] DIST = { 1.41421356f, 1f, 1.41421356f, 1f, 1f, 1.41421356f, 1f, 1.41421356f }; // Routing cost constants. SHORT pays lightly for climbing (8 per metre of rise, // so a 10 m wall costs like an 80 px detour — walls are avoided, direction is // kept). LOWGROUND pays for BEING high (1 per metre of elevation per px) plus // heavily for climbing, so the cheapest corridor is the lowest ground even when // that wanders. private const float SHORT_UPHILL_PER_M = 8f; private const float LOWGROUND_ELEV_PER_M = 1f; private const float LOWGROUND_BASE = 0.05f; private const float LOWGROUND_UPHILL_PER_M = 50f; // Bed geometry: sizes grow downstream from head to mouth, scaled by // sqrt(drainage / 1e6) so a 2.3M px giant carves roughly 2.3× deeper/wider at // the mouth than a 0.4M px trunk. Kept channel-scale, per the erosion // detailing philosophy. private const float DEPTH_HEAD_M = 1.0f; private const float DEPTH_MOUTH_M = 6.0f; // × sizeFactor × DepthScale private const float HALFWIDTH_HEAD_PX = 2.0f; private const float HALFWIDTH_MOUTH_PX = 12.0f; // × sizeFactor × WidthScale private const float MIN_BED_SLOPE = 0.002f; // m per px of enforced descent public class Params { public byte RoutingStyle = STYLE_LOWGROUND; public float WidthScale = 1.0f; public float DepthScale = 1.0f; public float SeaMarginM = 0.2f; // bed floor above sea, everywhere public DrainageAnalysis.Params PlanParams = new(); } public class RiverStat { public string Name; public string Kind; public bool SouthernCandidate; public long DrainagePx; public int CourseLenPx; public int RouteLenPx; // lowland reach only (routed giants) public float RouteStraightPx; public float WanderRatio; // routeLen / straight-line public bool ReachedOcean; public float MaxCutM; public double VolumeM3; } /// The carved geometry the water stage consumes (main rivers only). public class CarvedRiver { public string Name, Kind; public bool Southern; public long DrainagePx; public List<(float x, float y)> Dense; // head → mouth, ~1-px samples public float[] Bed; // raw units, monotone non-increasing public float[] HalfW; // px public bool ReachedWaterTerminal; // lake-enders: extension reached classify water } public class Stats { public List Rivers = new(); public List Carved = new(); // for the stepped-water stage (task 23) internal float[] PrePass; // cumulative-cut baseline public long CarvedCells; public double CarvedVolumeM3; public float MaxCutM; // CUMULATIVE vs pre-pass heights (task-22 nit 1 fixed) public double AnalysisSeconds, RoutingSeconds, CarveSeconds; } public static Stats Apply(float[,] height, int mapSize, bool[] isOcean, bool[] isClassifyWater, float southX, float southY, float[,] seaMap, float seaFlat, float craterCx, float craterCy, float craterCoreRadius, Func secondsNow, Params p) { int n = mapSize; var stats = new Stats(); float SeaAt(int x, int y) => seaMap != null ? seaMap[x, y] : seaFlat; float coreSq = craterCoreRadius * craterCoreRadius; // --- the frozen plan, recomputed deterministically in-pipeline --- double t0 = secondsNow(); var plan = DrainageAnalysis.Run(height, mapSize, isOcean, isClassifyWater, southX, southY, p.PlanParams); stats.AnalysisSeconds = secondsNow() - t0; // --- lowland routing for the routed giants (the A/B) --- t0 = secondsNow(); var giantRoutes = new List>(); foreach (var g in plan.Giants) { if (g.Kind != "routed") { giantRoutes.Add(null); continue; } giantRoutes.Add(RouteToOcean(height, n, isOcean, (int)g.Terminal.x, (int)g.Terminal.y, p.RoutingStyle, SeaAt)); } stats.RoutingSeconds = secondsNow() - t0; // --- carve --- t0 = secondsNow(); // Pre-pass snapshot: max-cut is measured CUMULATIVELY against the heights // this pass found, not per-write — overlapping stamps re-cut a cell and the // per-write number understated the true deepest cut ~4× (task-22 nit 1). float[] pre = new float[n * n]; for (int x = 0; x < n; x++) for (int y = 0; y < n; y++) pre[x * n + y] = height[x, y]; stats.PrePass = pre; int riverIdx = 0; foreach (var t in plan.Trunks) { riverIdx++; var course = new List<(float x, float y)>(t.Course); course.Reverse(); // head → mouth var rs = CarveRiver($"trunk{riverIdx}", "ocean-trunk", t.DrainageAreaPx, course, null, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats); rs.ReachedOcean = true; // outlet is on the coast by construction foreach (var trib in t.Tributaries) CarveTributary(trib, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats); } int gi = 0; foreach (var g in plan.Giants) { var route = giantRoutes[gi]; gi++; var course = new List<(float x, float y)>(g.Course); course.Reverse(); // head → terminal // Lake-enders (task 23): the stem pools on dry ground short of its lake // BECAUSE its pooling point is a local minimum — a blind descent walk // dead-ends there immediately (measured: 0 steps). Route to the nearest // classify-water cell with the same lowground Dijkstra the routed giants // use, so the bed (and then the water) actually joins the lake. bool reachedLake = false; if (g.Kind == "lake-ender") { var ext = RouteToOcean(height, n, isClassifyWater, (int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt); if (ext.Count > 0) { route = ext; reachedLake = true; } } var rs = CarveRiver($"giant{gi}", g.Kind, g.DrainageAreaPx, course, route, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats); rs.SouthernCandidate = g.SouthernCandidate; rs.ReachedOcean = g.Kind == "routed" ? (route != null && route.Count > 0) : reachedLake; if (stats.Carved.Count > 0) stats.Carved[^1].ReachedWaterTerminal = reachedLake; foreach (var trib in g.Tributaries) CarveTributary(trib, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats); } stats.CarveSeconds = secondsNow() - t0; 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), /// or an empty list if no path exists (reported upstream, never asserted away). /// private static List<(float x, float y)> RouteToOcean(float[,] height, int n, bool[] targets, int sx, int sy, byte style, Func seaAt) { int total = n * n; var gcost = new float[total]; var parent = new int[total]; var closed = new bool[total]; Array.Fill(gcost, float.MaxValue); Array.Fill(parent, -1); float ElevM(int x, int y) => MathF.Max(0f, (height[x, y] - seaAt(x, y)) * M_PER_UNIT); var pq = new PriorityQueue(); int start = sx * n + sy; gcost[start] = 0f; pq.Enqueue(start, (0f, start)); int goal = -1; while (pq.Count > 0) { int c = pq.Dequeue(); if (closed[c]) continue; closed[c] = true; if (targets[c]) { goal = c; break; } int cx = c / n, cy = c % n; float hc = height[cx, cy]; for (int k = 0; k < 8; k++) { int nx = cx + DX[k], ny = cy + DY[k]; if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue; int ni = nx * n + ny; if (closed[ni]) continue; float dhM = MathF.Max(0f, (height[nx, ny] - hc) * M_PER_UNIT); float step = style == STYLE_SHORT ? DIST[k] + dhM * SHORT_UPHILL_PER_M : DIST[k] * (LOWGROUND_BASE + ElevM(nx, ny) * LOWGROUND_ELEV_PER_M) + dhM * LOWGROUND_UPHILL_PER_M; float nc = gcost[c] + step; if (nc < gcost[ni]) { gcost[ni] = nc; parent[ni] = c; pq.Enqueue(ni, (nc, ni)); } } } var path = new List<(float x, float y)>(); if (goal >= 0) { for (int c = goal; c >= 0; c = parent[c]) path.Add((c / n, c % n)); path.Reverse(); } return path; } private static void CarveTributary(DrainageAnalysis.Stream trib, float[,] height, int n, Func seaAt, float coreSq, float craterCx, float craterCy, Params p, Stats stats) { var course = new List<(float x, float y)>(trib.Course); course.Reverse(); // head → confluence CarveRiver(null, "tributary", trib.DrainageAreaPx, course, null, height, n, seaAt, coreSq, craterCx, craterCy, p, stats); } /// /// Carves one river: densify the course, build a monotone-descending clamped /// bed profile, stamp the channel. Returns the per-river stat (also appended /// to stats.Rivers unless name is null — tributaries fold into the totals). /// private static RiverStat CarveRiver(string name, string kind, long drainagePx, List<(float x, float y)> upland, List<(float x, float y)> lowlandRoute, 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. // ONLY the lowland reach is smoothed: the Dijkstra 45° kinks live there, on // near-flat ground where a rounded corner costs nothing. The upland stems // already thread the carved valley FLOORS — smoothing them off-line cut // valley walls (measured: max cut 14.6 → 27.3 m before this was split). var pts = new List<(float x, float y)>(upland); if (lowlandRoute != null && lowlandRoute.Count > 1) { var smoothedRoute = SmoothCourse(lowlandRoute); pts.AddRange(smoothedRoute.GetRange(1, smoothedRoute.Count - 1)); } // Densify to ~1-px samples (plan courses are decimated ×4). var dense = new List<(float x, float y)>(); for (int i = 0; i + 1 < pts.Count; i++) { var a = pts[i]; var b = pts[i + 1]; float segLen = MathF.Sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)); int steps = Math.Max(1, (int)MathF.Ceiling(segLen)); for (int s2 = 0; s2 < steps; s2++) dense.Add((a.x + (b.x - a.x) * s2 / steps, a.y + (b.y - a.y) * s2 / steps)); } if (pts.Count > 0) dense.Add(pts[^1]); if (dense.Count < 2) return new RiverStat(); float sizeFactor = MathF.Sqrt(drainagePx / 1_000_000f); var rs = new RiverStat { Name = name, Kind = kind, DrainagePx = drainagePx, CourseLenPx = dense.Count, RouteLenPx = lowlandRoute?.Count ?? 0 }; if (lowlandRoute != null && lowlandRoute.Count > 1) { var a = lowlandRoute[0]; var b = lowlandRoute[^1]; rs.RouteStraightPx = MathF.Sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)); // Wander = POLYLINE length over straight-line — cell count undercounts // diagonal steps and can read below 1, which is geometrically impossible. float polyLen = 0f; for (int i = 1; i < lowlandRoute.Count; i++) { float sdx = lowlandRoute[i].x - lowlandRoute[i - 1].x; float sdy = lowlandRoute[i].y - lowlandRoute[i - 1].y; polyLen += MathF.Sqrt(sdx * sdx + sdy * sdy); } rs.RouteLenPx = (int)polyLen; rs.WanderRatio = rs.RouteStraightPx > 1f ? polyLen / rs.RouteStraightPx : 1f; } // Bed profile: raw = terrain − depth(t); then monotone non-increasing // downstream; then clamped to sea + margin. The clamp can flatten the tail // near the mouth — allowed: non-increasing is what water needs, and the // flood guard is absolute. int m = dense.Count; var bed = new float[m]; var depth = new float[m]; var halfW = new float[m]; for (int i = 0; i < m; i++) { float t = m > 1 ? (float)i / (m - 1) : 1f; depth[i] = (DEPTH_HEAD_M + (DEPTH_MOUTH_M * sizeFactor - DEPTH_HEAD_M) * t) * p.DepthScale; if (depth[i] < 0.5f) depth[i] = 0.5f; halfW[i] = (HALFWIDTH_HEAD_PX + (HALFWIDTH_MOUTH_PX * sizeFactor - HALFWIDTH_HEAD_PX) * t) * p.WidthScale; if (halfW[i] < 1.5f) halfW[i] = 1.5f; int cx = (int)dense[i].x, cy = (int)dense[i].y; bed[i] = height[cx, cy] - depth[i] / M_PER_UNIT; } for (int i = 1; i < m; i++) { float maxAllowed = bed[i - 1] - MIN_BED_SLOPE / M_PER_UNIT; if (bed[i] > maxAllowed) bed[i] = maxAllowed; } for (int i = 0; i < m; i++) { int cx = (int)dense[i].x, cy = (int)dense[i].y; float floor = seaAt(cx, cy) + p.SeaMarginM / M_PER_UNIT; if (bed[i] < floor) bed[i] = floor; } // Stamp: parabolic channel to the rim, smoothstep shoulder back to terrain. for (int i = 0; i < m; i++) { float hw = halfW[i]; float outer = hw * 2f; int cx0 = (int)MathF.Floor(dense[i].x - outer), cx1 = (int)MathF.Ceiling(dense[i].x + outer); int cy0 = (int)MathF.Floor(dense[i].y - outer), cy1 = (int)MathF.Ceiling(dense[i].y + outer); float rimH = bed[i] + depth[i] / M_PER_UNIT; for (int x = cx0; x <= cx1; x++) { if (x < 0 || x >= n) continue; for (int y = cy0; y <= cy1; y++) { if (y < 0 || y >= n) continue; float rx = x - dense[i].x, ry = y - dense[i].y; float r = MathF.Sqrt(rx * rx + ry * ry); if (r > outer) continue; float ddx = x - craterCx, ddy = y - craterCy; if (ddx * ddx + ddy * ddy < coreSq) continue; // crater core protected float sea = seaAt(x, y); float old = height[x, y]; if (old < sea) continue; // below-sea cells read-only float target; if (r <= hw) { float f = r / hw; target = bed[i] + (depth[i] / M_PER_UNIT) * f * f; } else { float f = (r - hw) / hw; // 0..1 across the shoulder f = f * f * (3f - 2f * f); // smoothstep target = rimH + (old - rimH) * f; } float floor = sea + p.SeaMarginM / M_PER_UNIT; if (target < floor) target = floor; if (target < old) { float cutM = (old - target) * M_PER_UNIT; // Cumulative depth vs the PRE-PASS surface (nit 1): the // honest "how deep did we cut here in total" number. float cumM = (stats.PrePass[x * n + y] - target) * M_PER_UNIT; height[x, y] = target; stats.CarvedCells++; stats.CarvedVolumeM3 += cutM; if (cumM > stats.MaxCutM) stats.MaxCutM = cumM; if (cumM > rs.MaxCutM) rs.MaxCutM = cumM; rs.VolumeM3 += cutM; } } } } if (name != null) { stats.Rivers.Add(rs); stats.Carved.Add(new CarvedRiver { Name = name, Kind = kind, DrainagePx = drainagePx, Dense = dense, Bed = bed, HalfW = halfW }); } return rs; } /// /// The stepped-water builder (task 23, part 2b): segments each carved main /// river into REACHES — flat water bodies stepping down the bed toward the /// outlet — and stamps their ids into the WBID grid. Reuses the existing /// levels-not-cells water model exactly: one body per reach, one flat level /// each; the writer derives WSRF from body levels as it always has. The step /// drops are the smoothing dial (smaller drop = more, finer steps); tilted /// water is the deferred model B and is NOT built here. /// /// Emission is plain data (no engine types): the caller turns reaches into /// WBTB entries. Wet cells: inside the channel half-width, currently dry in /// WBID, at/above sea (below-sea cells belong to the ocean/crater-seam rule), /// bed below the reach level. Existing water bodies are never overwritten — /// a river MEETS a lake or the sea, it does not repaint them. /// public class Reach { public ushort Id; public string River; public float Level; // raw units public int PixelCount; public double Cx, Cy; // centroid accumulators → mean } public static List AddSteppedWater(float[,] height, int mapSize, ushort[,] wbid, ushort firstId, List rivers, float[,] seaMap, float seaFlat, float craterCx, float craterCy, float craterCoreRadius, float stepDropM, float waterDepthM) { int n = mapSize; float SeaAt(int x, int y) => seaMap != null ? seaMap[x, y] : seaFlat; float coreSq = craterCoreRadius * craterCoreRadius; var reaches = new List(); ushort nextId = firstId; foreach (var r in rivers) { int m = r.Dense.Count; if (m < 2) continue; int i = 0; float lastLevel = float.MaxValue; while (i < m) { // Reach spans from i while the bed stays within stepDropM of the // reach's starting bed; its flat level sits waterDepthM above that // start (deepening toward the next step — the pool behind a riffle). float startBed = r.Bed[i]; float level = startBed + waterDepthM / M_PER_UNIT; if (level >= lastLevel) // enforce strict descent level = lastLevel - 0.01f / M_PER_UNIT; int j = i; while (j < m && r.Bed[j] > startBed - stepDropM / M_PER_UNIT) j++; var reach = new Reach { Id = nextId, River = r.Name, Level = level }; for (int k2 = i; k2 < j; k2++) { float hw = r.HalfW[k2]; int x0 = (int)MathF.Floor(r.Dense[k2].x - hw), x1 = (int)MathF.Ceiling(r.Dense[k2].x + hw); int y0 = (int)MathF.Floor(r.Dense[k2].y - hw), y1 = (int)MathF.Ceiling(r.Dense[k2].y + hw); for (int x = x0; x <= x1; x++) { if (x < 0 || x >= n) continue; for (int y = y0; y <= y1; y++) { if (y < 0 || y >= n) continue; if (wbid[x, y] != 0) continue; // never repaint existing water float rx = x - r.Dense[k2].x, ry = y - r.Dense[k2].y; if (rx * rx + ry * ry > hw * hw) continue; float ddx = x - craterCx, ddy = y - craterCy; if (ddx * ddx + ddy * ddy < coreSq) continue; float h = height[x, y]; float sea = SeaAt(x, y); if (h < sea) continue; // ocean/seam territory if (h >= level) continue; // bank above the water line wbid[x, y] = nextId; reach.PixelCount++; reach.Cx += x; reach.Cy += y; } } } if (reach.PixelCount > 0) { reach.Cx /= reach.PixelCount; reach.Cy /= reach.PixelCount; reaches.Add(reach); nextId++; lastLevel = level; } i = j; } } return reaches; } }