diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs index 65f6cb9..f23a402 100644 --- a/Core/Scripts/ConfigManager.cs +++ b/Core/Scripts/ConfigManager.cs @@ -133,9 +133,19 @@ namespace IslaApocalypse.Core // Change this if your namespace is different // 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. + // Task 25: RiverFillFraction fills the channel to a fraction of its LOCAL bed + // depth (a fixed height was a trickle at deep mouths and overtopped shallow + // heads); RiverWaterDepthM stays as the absolute minimum. RiverBankFlare + // widens the bank shoulder (half-widths beyond the channel) and it now uses a + // smootherstep, so water meets land as a shore instead of a wall. + // RiverTribWaterMinFlow defaults to 0 — tributaries carry water their full + // promoted length (the gate's preference), still fading at the tip. public static float RiverStepDropM = 0.6f; - public static float RiverWaterDepthM = 2.2f; - public static int RiverTribWaterMinFlow = 40000; + public static float RiverWaterDepthM = 1.0f; + public static float RiverFillFraction = 0.80f; + public static float RiverBankFlare = 3.2f; + public static float RiverBankMaxCutM = 3.0f; // shoulder-only cut cap (task 25) + public static int RiverTribWaterMinFlow = 0; public static int RiverTribTaperPx = 120; public static int RiverLakeMinTargetPx = 20000; public static float RiverDepthScale = 1.5f; // deepened at the task-24 gate's ask @@ -351,6 +361,12 @@ namespace IslaApocalypse.Core // Change this if your namespace is different if (data.ContainsKey("RiverTribWaterMinFlow")) RiverTribWaterMinFlow = (int)data["RiverTribWaterMinFlow"]; if (data.ContainsKey("RiverTribTaperPx")) RiverTribTaperPx = (int)data["RiverTribTaperPx"]; if (data.ContainsKey("RiverLakeMinTargetPx")) RiverLakeMinTargetPx = (int)data["RiverLakeMinTargetPx"]; + if (data.ContainsKey("RiverFillFraction")) RiverFillFraction = (float)data["RiverFillFraction"]; + if (data.ContainsKey("RiverBankFlare")) RiverBankFlare = (float)data["RiverBankFlare"]; + RiverFillFraction = Mathf.Clamp(RiverFillFraction, 0.1f, 1f); + if (data.ContainsKey("RiverBankMaxCutM")) RiverBankMaxCutM = (float)data["RiverBankMaxCutM"]; + RiverBankFlare = Mathf.Clamp(RiverBankFlare, 1.2f, 8f); + RiverBankMaxCutM = Mathf.Clamp(RiverBankMaxCutM, 0f, 30f); RiverTribWaterMinFlow = Mathf.Max(RiverTribWaterMinFlow, 0); RiverTribTaperPx = Mathf.Clamp(RiverTribTaperPx, 0, 2000); RiverLakeMinTargetPx = Mathf.Max(RiverLakeMinTargetPx, 0); diff --git a/Tools/Scripts/RiverCarvePass.cs b/Tools/Scripts/RiverCarvePass.cs index 54c9e0d..60f5f10 100644 --- a/Tools/Scripts/RiverCarvePass.cs +++ b/Tools/Scripts/RiverCarvePass.cs @@ -69,8 +69,24 @@ public static class RiverCarvePass // 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 TribWaterMinFlowPx = 0; // task 25: 0 = tributaries wet full length public int TribTaperPx = 120; + // Task 25 (the gate's "trickle at the bottom of a ditch" note): the reach's + // water surface is a FRACTION of the local bed depth rather than a fixed + // height — the channel reads FILLED at every scale and cannot overfill onto + // the plain, which a fixed depth does at the shallow heads. WaterDepthM + // survives as the absolute minimum so tiny channels still hold water. + public float FillFraction = 0.80f; + // Bank shoulders flare this many half-widths beyond the channel (was a hard + // 2×) with a gentler-than-smoothstep curve, so water meets land as a shore. + public float BankFlare = 3.2f; + // ...but the SHOULDER may never lower a cell by more than this. A gentle flare + // across a ridge would otherwise cut a big notch: widening the flare alone took + // cells deeper than 20 m from 339 to 1917 (measured). With the cap, gentle + // ground still flares into a shore — which is where the "water in a groove" + // complaint lives — while a ridge crossing keeps steep walls, which is what a + // gorge actually looks like. The channel bed itself is not affected by this. + public float BankMaxCutM = 3.0f; // 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; @@ -101,6 +117,7 @@ public static class RiverCarvePass 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 float[] DepthM; // local bed depth, metres (task 25 fill) 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 @@ -429,7 +446,7 @@ public static class RiverCarvePass for (int i = 0; i < m; i++) { float hw = halfW[i]; - float outer = hw * 2f; + float outer = hw * p.BankFlare; 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; @@ -455,9 +472,20 @@ public static class RiverCarvePass } else { - float f = (r - hw) / hw; // 0..1 across the shoulder - f = f * f * (3f - 2f * f); // smoothstep + // Shoulder: 0 at the rim → 1 at natural ground, over a flare of + // (BankFlare-1) half-widths. Smootherstep (6t⁵−15t⁴+10t³) leaves + // the rim nearly tangent to the water plane, so the bank reads as + // a shore rather than the wall a plain smoothstep left. + float f = (r - hw) / MathF.Max(1e-3f, hw * (p.BankFlare - 1f)); + if (f > 1f) f = 1f; + f = f * f * f * (f * (6f * f - 15f) + 10f); target = rimH + (old - rimH) * f; + // Clamp against the PRE-PASS surface, not the current height: + // overlapping stamps re-visit a cell, so a per-write cap lets each + // pass take another BankMaxCut (measured: capping against `old` + // changed the carved volume by 1 m³ out of 1.29 M — i.e. nothing). + float shoulderFloor = stats.PrePass[x * n + y] - p.BankMaxCutM / M_PER_UNIT; + if (target < shoulderFloor) target = shoulderFloor; } float floor = sea + p.SeaMarginM / M_PER_UNIT; if (target < floor) target = floor; @@ -501,7 +529,7 @@ public static class RiverCarvePass stats.Carved.Add(new CarvedRiver { Name = name, Kind = kind, DrainagePx = drainagePx, - Dense = dense, Bed = bed, HalfW = halfW, + Dense = dense, Bed = bed, HalfW = halfW, DepthM = depth, Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx }); } @@ -535,7 +563,7 @@ public static class RiverCarvePass 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) + float craterCoreRadius, float stepDropM, float waterDepthM, float fillFraction) { int n = mapSize; float SeaAt(int x, int y) => seaMap != null ? seaMap[x, y] : seaFlat; @@ -551,7 +579,12 @@ public static class RiverCarvePass // 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); + // The fade zone is always TaperPx long starting at i. With a threshold it + // spans (WetFrom-Taper → WetFrom); with tributaries fully watered + // (WetFrom = 0, task 25) it spans the first TaperPx from the HEAD, so a + // full-length tributary still fades in at its tip instead of starting as a + // wall of water. + int taperStart = i, taperEnd = Math.Min(m - 1, i + r.TaperPx); float lastLevel = float.MaxValue; while (i < m) { @@ -559,7 +592,14 @@ public static class RiverCarvePass // 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; + // Fill the channel: the surface sits at FillFraction of the LOCAL bed + // depth, never below the absolute minimum. A fixed height made deep + // mouths a trickle and overtopped shallow heads; a fraction is right at + // both ends and cannot spill onto the plain. + float localDepthM = r.DepthM != null ? r.DepthM[i] : waterDepthM; + float fillM = MathF.Max(waterDepthM, localDepthM * fillFraction); + if (fillM > localDepthM) fillM = localDepthM; // never above the rim + float level = startBed + fillM / M_PER_UNIT; if (level >= lastLevel) // enforce strict descent level = lastLevel - 0.01f / M_PER_UNIT; int j = i;