feat: tributary water (tapered), significant-lake targeting, finer steps + deeper beds (terrain-water task 24)
All three gate polish items — they interleave in the same files, so they land as one buildable commit rather than three that do not compile independently. 1. TRIBUTARY WATER. Tributaries carved in task 22/23 but stayed dry: they were never registered with the water stage. They now are (as CarvedRiver entries named 'trib', kept OUT of the per-river console table so it stays the 6 mains). RiverTribWaterMinFlow (40k px of along-course flow) sets where water starts; RiverTribTaperPx (120 px) makes the wet->dry transition a FADE, not a wall — across the taper the wet strip narrows (0.25x -> 1x half-width) AND its surface drops toward the bed, so a stream head thins out and vanishes. Along-course flow is modelled quadratically from headwater trickle to full drainage at the mouth (the plan records drainage per river, not per sample). RiverTribWaterMinFlow=0 waters them end to end — the documented fallback. 2. LAKE-ENDER TARGETING. The join routed to the nearest classify-water CELL, which a 322-px puddle satisfies; it stopped ~80 px short of the 197k-px lagoon beside it. It now routes to a SIGNIFICANT water mask (bodies of at least RiverLakeMinTargetPx = 20k cells, built from the water-body table), falling back to any classify water only if no significant body is reachable, so a seed with genuinely small ponds still connects. Proven by adjacency: task 23 had 63 river cells touching the puddle and 0 touching the lagoon; task 24 has 49 touching the lagoon and 0 touching the puddle. 3. FINER STEPS + DEEPER BEDS. RiverStepDropM 2.0 -> 0.6 and RiverWaterDepthM 1.2 -> 2.2, RiverDepthScale 1.0 -> 1.5. Reaches 298 -> 1034; the level gap between adjacent reaches drops from median 0.571 m / p95 2.08 m to median 0.124 m / p95 0.74 m — the pond-staircase reads as a graded descent. Reaches stay trivial bodies (median 44 px). Guards all held: flood guard 0 newly-below-sea and 0 below-sea cells modified, BIOME oracle md5-identical to the task-22 baseline (output-only unchanged), island top 457.65 m exact, crater core excluded, lowest carved cell exactly sea+margin (37.85 m). Cumulative max cut 25.98 m with 339 cells >20 m — identical to task 23, so deepening the WATER did not deepen the worst cuts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
645955d49c
commit
0e39538604
3 changed files with 113 additions and 15 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<ushort>();
|
||||
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 " +
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <param name="isSignificantWater">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.</param>
|
||||
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<double> 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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue