diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs index cb1a953..65f6cb9 100644 --- a/Core/Scripts/ConfigManager.cs +++ b/Core/Scripts/ConfigManager.cs @@ -127,9 +127,18 @@ namespace IslaApocalypse.Core // Change this if your namespace is different public static string Rivers = "off"; public static string RiverRoutingStyle = "lowground"; public static float RiverWidthScale = 1.75f; // widened at the task-23 gate's ask - public static float RiverStepDropM = 2.0f; - public static float RiverWaterDepthM = 1.2f; - public static float RiverDepthScale = 1.0f; + // Task 24 (the gate's polish): finer steps read as a descending river rather + // than a pond staircase; deeper water sits contained in its banks. + // RiverTribWaterMinFlow is the drainage a TRIBUTARY reach needs to carry + // water (0 = water them end to end); above it the water TAPERS to dry over + // RiverTribTaperPx so a stream head fades instead of ending in a wall. + // RiverLakeMinTargetPx is the smallest water body a lake-ender may target. + public static float RiverStepDropM = 0.6f; + public static float RiverWaterDepthM = 2.2f; + public static int RiverTribWaterMinFlow = 40000; + public static int RiverTribTaperPx = 120; + public static int RiverLakeMinTargetPx = 20000; + public static float RiverDepthScale = 1.5f; // deepened at the task-24 gate's ask public static float RiverSeaMargin = 0.2f; // m above sea, bed floor // Island falloff shaping (task 11). @@ -339,6 +348,12 @@ namespace IslaApocalypse.Core // Change this if your namespace is different if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"]; if (data.ContainsKey("RiverStepDropM")) RiverStepDropM = (float)data["RiverStepDropM"]; if (data.ContainsKey("RiverWaterDepthM")) RiverWaterDepthM = (float)data["RiverWaterDepthM"]; + if (data.ContainsKey("RiverTribWaterMinFlow")) RiverTribWaterMinFlow = (int)data["RiverTribWaterMinFlow"]; + if (data.ContainsKey("RiverTribTaperPx")) RiverTribTaperPx = (int)data["RiverTribTaperPx"]; + if (data.ContainsKey("RiverLakeMinTargetPx")) RiverLakeMinTargetPx = (int)data["RiverLakeMinTargetPx"]; + RiverTribWaterMinFlow = Mathf.Max(RiverTribWaterMinFlow, 0); + RiverTribTaperPx = Mathf.Clamp(RiverTribTaperPx, 0, 2000); + RiverLakeMinTargetPx = Mathf.Max(RiverLakeMinTargetPx, 0); RiverStepDropM = Mathf.Clamp(RiverStepDropM, 0.25f, 10f); RiverWaterDepthM = Mathf.Clamp(RiverWaterDepthM, 0.2f, 5f); RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f); diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs index 4390107..af9f073 100644 --- a/Tools/Scripts/MapGenerator.cs +++ b/Tools/Scripts/MapGenerator.cs @@ -845,6 +845,17 @@ public partial class MapGenerator : TextureRect isClassifyWater[x * MapSize + y] = IsWaterPixel(x, y); } + // Task 24: SIGNIFICANT water — cells of bodies at least RiverLakeMinTargetPx + // in size, from the water-bodies table the stage above already built. A + // lake-ender routes to this so it enters the lagoon, not a puddle. + var bigBodies = new System.Collections.Generic.HashSet(); + foreach (var b in _waterBodies) + if (b.PixelCount >= ConfigManager.RiverLakeMinTargetPx) bigBodies.Add(b.Id); + bool[] isSignificantWater = new bool[MapSize * MapSize]; + for (int x = 0; x < MapSize; x++) + for (int y = 0; y < MapSize; y++) + isSignificantWater[x * MapSize + y] = bigBodies.Contains(_waterBodyIds[x, y]); + float southX = -1f, southY = -1f; foreach (var t in _towns) if (t.Position.Y > southY) { southX = t.Position.X; southY = t.Position.Y; } @@ -855,10 +866,13 @@ public partial class MapGenerator : TextureRect ? RiverCarvePass.STYLE_SHORT : RiverCarvePass.STYLE_LOWGROUND, WidthScale = ConfigManager.RiverWidthScale, DepthScale = ConfigManager.RiverDepthScale, - SeaMarginM = ConfigManager.RiverSeaMargin + SeaMarginM = ConfigManager.RiverSeaMargin, + TribWaterMinFlowPx = ConfigManager.RiverTribWaterMinFlow, + TribTaperPx = ConfigManager.RiverTribTaperPx, + LakeMinTargetPx = ConfigManager.RiverLakeMinTargetPx }; var st = RiverCarvePass.Apply(_heightMap, MapSize, isOcean, isClassifyWater, - southX, southY, seaMap, seaFlat, + isSignificantWater, southX, southY, seaMap, seaFlat, _impactCenter.X, _impactCenter.Y, _impactRadius * ConfigManager.CraterErosionCore, () => (Time.GetTicksMsec()) / 1000.0, p); @@ -899,8 +913,15 @@ public partial class MapGenerator : TextureRect }); riverWetPx += reach.PixelCount; } - GD.Print($"{T()} [Rivers] water: {reaches.Count} stepped reaches across {st.Carved.Count} rivers, " + - $"{riverWetPx} wet px, step drop {ConfigManager.RiverStepDropM:F1} m, depth {ConfigManager.RiverWaterDepthM:F1} m."); + int mainCarved = 0, tribCarved = 0; + foreach (var cr in st.Carved) { if (cr.Kind == "tributary") tribCarved++; else mainCarved++; } + int tribReaches = 0; long tribWet = 0; + foreach (var reach in reaches) + if (reach.River == "trib") { tribReaches++; tribWet += reach.PixelCount; } + GD.Print($"{T()} [Rivers] water: {reaches.Count} stepped reaches ({tribReaches} on tributaries) " + + $"across {mainCarved} mains + {tribCarved} tributaries, {riverWetPx} wet px " + + $"({tribWet} tributary), step drop {ConfigManager.RiverStepDropM:F2} m, depth {ConfigManager.RiverWaterDepthM:F1} m, " + + $"trib flow threshold {ConfigManager.RiverTribWaterMinFlow} px taper {ConfigManager.RiverTribTaperPx} px."); GD.Print($"{T()} [Rivers] v1 '{ConfigManager.RiverRoutingStyle}': plan {st.AnalysisSeconds:F1}s, " + $"routing {st.RoutingSeconds:F1}s, carve {st.CarveSeconds:F1}s " + diff --git a/Tools/Scripts/RiverCarvePass.cs b/Tools/Scripts/RiverCarvePass.cs index cf4dd35..54c9e0d 100644 --- a/Tools/Scripts/RiverCarvePass.cs +++ b/Tools/Scripts/RiverCarvePass.cs @@ -66,6 +66,14 @@ public static class RiverCarvePass public float WidthScale = 1.0f; public float DepthScale = 1.0f; public float SeaMarginM = 0.2f; // bed floor above sea, everywhere + // Task 24: a tributary reach is WET where its along-course flow exceeds this; + // upstream of that it TAPERS to dry over TribTaperPx rather than ending in a + // wall of water. 0 = water tributaries end to end. + public int TribWaterMinFlowPx = 40_000; + public int TribTaperPx = 120; + // Lake-enders route to the nearest water body of at least this size — the + // nearest wet PIXEL was a puddle (task-23 gate finding). + public int LakeMinTargetPx = 20_000; public DrainageAnalysis.Params PlanParams = new(); } @@ -94,6 +102,12 @@ public static class RiverCarvePass public float[] Bed; // raw units, monotone non-increasing public float[] HalfW; // px public bool ReachedWaterTerminal; // lake-enders: extension reached classify water + // Task 24: along-course flow (px of drainage) per sample, and the first index + // that carries water. Between WetFrom-TaperPx and WetFrom the water tapers + // (narrowing and shallowing to the bed) so a stream head fades out. + public float[] Flow; + public int WetFrom; + public int TaperPx; } public class Stats @@ -107,8 +121,13 @@ public static class RiverCarvePass public double AnalysisSeconds, RoutingSeconds, CarveSeconds; } + /// Row-major mask of classify water belonging to + /// bodies of at least LakeMinTargetPx cells (task 24). Lake-enders route to THIS, + /// not to any wet pixel: the task-23 build routed one into a puddle a few hundred + /// px short of the obvious lagoon, because "nearest classify water" is satisfied + /// by a 3-cell pond. public static Stats Apply(float[,] height, int mapSize, bool[] isOcean, - bool[] isClassifyWater, float southX, float southY, + bool[] isClassifyWater, bool[] isSignificantWater, float southX, float southY, float[,] seaMap, float seaFlat, float craterCx, float craterCy, float craterCoreRadius, Func secondsNow, Params p) @@ -171,8 +190,14 @@ public static class RiverCarvePass bool reachedLake = false; if (g.Kind == "lake-ender") { - var ext = RouteToOcean(height, n, isClassifyWater, + // Target SIGNIFICANT water (task 24). Fall back to any classify water + // only if no significant body is reachable, so a seed whose lake-ender + // genuinely has only small ponds still connects rather than dead-ending. + var ext = RouteToOcean(height, n, isSignificantWater, (int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt); + if (ext.Count == 0) + ext = RouteToOcean(height, n, isClassifyWater, + (int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt); if (ext.Count > 0) { route = ext; reachedLake = true; } } @@ -304,7 +329,10 @@ public static class RiverCarvePass { var course = new List<(float x, float y)>(trib.Course); course.Reverse(); // head → confluence - CarveRiver(null, "tributary", trib.DrainageAreaPx, course, null, + // Task 24: tributaries are registered as carved rivers (name "trib") so the + // water stage sees them — they carved but stayed dry in task 23. They are NOT + // added to stats.Rivers, so the per-river console table stays the 6 mains. + CarveRiver("trib", "tributary", trib.DrainageAreaPx, course, null, height, n, seaAt, coreSq, craterCx, craterCy, p, stats); } @@ -452,11 +480,29 @@ public static class RiverCarvePass if (name != null) { - stats.Rivers.Add(rs); + if (kind != "tributary") stats.Rivers.Add(rs); + // Along-course flow: drainage grows from a headwater trickle to the full + // figure at the mouth. Quadratic in t so the substantial lower half + // dominates — a first-order stand-in for real accumulation, which the + // plan only records per-river. + var flow = new float[m]; + for (int i = 0; i < m; i++) + { + float t = m > 1 ? (float)i / (m - 1) : 1f; + flow[i] = drainagePx * (0.05f + 0.95f * t * t); + } + int wetFrom = 0; + if (kind == "tributary" && p.TribWaterMinFlowPx > 0) + { + wetFrom = m; // dry unless the threshold is met + for (int i = 0; i < m; i++) + if (flow[i] >= p.TribWaterMinFlowPx) { wetFrom = i; break; } + } stats.Carved.Add(new CarvedRiver { Name = name, Kind = kind, DrainagePx = drainagePx, - Dense = dense, Bed = bed, HalfW = halfW + Dense = dense, Bed = bed, HalfW = halfW, + Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx }); } return rs; @@ -501,7 +547,11 @@ public static class RiverCarvePass { int m = r.Dense.Count; if (m < 2) continue; - int i = 0; + // Task 24: start at the wet-from index (tributary threshold; 0 for mains), + // but back up by the taper length so the transition is a FADE, not a wall. + int i = Math.Max(0, r.WetFrom - r.TaperPx); + if (i >= m) continue; // entirely below threshold: dry + int taperStart = i, taperEnd = Math.Min(m - 1, r.WetFrom); float lastLevel = float.MaxValue; while (i < m) { @@ -518,7 +568,14 @@ public static class RiverCarvePass var reach = new Reach { Id = nextId, River = r.Name, Level = level }; for (int k2 = i; k2 < j; k2++) { - float hw = r.HalfW[k2]; + // Taper: 0 at the dry end of the fade zone → 1 at full water. The + // water narrows AND its surface drops to the bed, so a stream head + // thins out and disappears instead of ending in a flat wall. + float taper = 1f; + if (taperEnd > taperStart && k2 < taperEnd) + taper = (float)(k2 - taperStart) / (taperEnd - taperStart); + if (taper <= 0.02f) continue; + float hw = r.HalfW[k2] * (0.25f + 0.75f * taper); 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++) @@ -535,7 +592,12 @@ public static class RiverCarvePass 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 + // The tapered surface sits between the bed and the reach + // level, so the wet strip shallows as it narrows. + float localLevel = taper >= 1f + ? level + : r.Bed[k2] + (level - r.Bed[k2]) * taper; + if (h >= localLevel) continue; // bank above the water line wbid[x, y] = nextId; reach.PixelCount++; reach.Cx += x; reach.Cy += y;