diff --git a/Core/Scripts/BLUEPRINT_FORMAT.md b/Core/Scripts/BLUEPRINT_FORMAT.md
index 6e3d925..f905988 100644
--- a/Core/Scripts/BLUEPRINT_FORMAT.md
+++ b/Core/Scripts/BLUEPRINT_FORMAT.md
@@ -59,7 +59,7 @@ Every section: `[u32 tag][u64 payload-length in bytes][payload]`.
| `TDTL` / `0x4C544454` | 30 B: `u16 detailVersion` · `f32 reliefAmpM` · `f32 reliefFreqIslands` · `i32 reliefSeedOffset` · `f32 edgeAmpM` · `f32 edgeFreqIslands` · `i32 edgeSeedOffset` · `f32 edgeMaxShiftM` | Optional — present iff the terrain detail passes shaped this blueprint's `HGTS` (config `TerrainDetail: "v1"`, requires the curve). **`detailVersion` selects the body layout and a reader that does not recognise it SKIPS the section** (leaving detail metadata null) rather than misreading a differently shaped payload — the one section whose body is versioned rather than extended, because v1's layout was retired rather than grown. **v1 (retired, never shipped)** — shelf micro-relief + D8 drainage incision, 38 B; the incision produced grid-aligned artifacts and was reverted whole, so the only v1 payloads that exist are in that batch's own tree. **v2 (current)** — shelf micro-relief (±`reliefAmpM` output metres, shelf-ness weighted) + shelf-edge variation: a per-column shift of the curve's shelf/riser knot block K3/K4/K5, drawn from a Simplex field seeded `resolvedWorldSeed + edgeSeedOffset` at `edgeFreqIslands/MapSize`, amplitude ±`edgeAmpM` **metres of INPUT height** — a displacement of the shelf boundary contour, not an elevation change. `edgeAmpM` is recorded **as applied**, after the clamp to `edgeMaxShiftM` (the preset's band-squeeze bound), so the record always describes the terrain rather than the request. **Metadata only** — heights are already detailed. Machinery: `Tools/Scripts/TerrainDetailPass.cs`. |
| `EROS` / `0x534F5245` | 67 B: `u16 erosionVersion` · `i32 dropletCount` · `i32 lifetime` · `i32 brushRadius` · `i32 seedOffset` · `f32 carveCapM` · `f32 depositCapM` · `f32 seaMarginM` · `f32 inertia` · `f32 capacityFactor` · `f32 minSlopeM` · `f32 erodeRate` · `f32 depositRate` · `f32 evaporation` · `f32 gravity` · `f32 craterCoreFactor` · `f32 craterFeatherFactor` · `u8 craterMode` | Optional — present iff the droplet hydraulic-erosion pass shaped this blueprint's `HGTS` (config `Erosion: "v1"`, terrain-water tasks 17–19). **`erosionVersion` selects the body layout; an unrecognised version is SKIPPED whole** (erosion metadata left null), same rule as `TDTL`. **v1 (task 17)** — 58 B, no `depositCapM`; deposition was bilinear over 4 cells and unbounded, which built isolated cones (measured 15.5 m). **v2 (task 18)** — 62 B; deposition brush-spread and per-cell bounded. **v3 (task 19, current)** — replaces v2's single `craterExclFactor` with `craterCoreFactor` (the protected strike core), `craterFeatherFactor` and `craterMode` (`0` = full, `1` = feather). Only v2+ payloads exist outside tasks 17–18's own batch trees. The FOUR governors are `dropletCount`/`lifetime`/`carveCapM`/`depositCapM`; both caps are enforced against one per-cell NET displacement ledger (positive = carved below the height the pass found, negative = built up above it) and asserted on exit. `seaMarginM` is the flood-guard clamp — no cell is carved below sea + margin, and below-sea cells are untouched in BOTH directions, so the rendered coastline cannot move **and the crater's flooded bay can be neither carved open nor silted shut regardless of `craterMode`**. Crater radii are FACTORS of `PRMS.CraterRadius`: nothing inside `craterCoreFactor ×` it is modified; `feather` ramps erosion 0→full from there out to `craterFeatherFactor ×` it, `full` applies full strength immediately. Droplets are deterministic from `resolvedWorldSeed + seedOffset` (PCG32). Remaining fields are the droplet-model strength dials; slopes/amounts in metres (1 raw = 251 m). All values recorded **as applied** (post config clamping). **Metadata only** — heights are already eroded, and the classify-side sections (`BIOM`/`WBID`/…) never saw the pass by design. Machinery: `Tools/Scripts/HydraulicErosion.cs`. |
| `WBID` / `0x44494257` | `MapSize²` × `u16` water-body id, same pixel order as `HGTS` | Optional (absent = no water data, e.g. a legacy re-encode). `0` = no water, `1` = **the** ocean body, `2..N` = lakes. Ids assigned in deterministic scan order (X outer / Y inner, first-encountered pixel), lakes labeled with the **same 4-connectivity as `CalculateTrueOcean`**. Membership is exactly the generator's water classification — the biome grid's Ocean/Lake pixels and this grid's nonzero pixels are the same set **by construction** (shared predicates). Length must equal `2·MapSize²`. |
-| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake, 2 river — a stepped river REACH, one flat level per reach; task 23) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. |
+| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. |
| `WSRF` / `0x46525357` | `MapSize²` × `u16` quantized water-surface elevation, same pixel order | Optional, paired with `WBID`. `0` is the reserved **no-water sentinel**; a real level `L` (raw height units) encodes as `1 + round(L × 32768)` so it can never encode to 0; decode `(q − 1)/32768` (`BlueprintFormat.EncodeWaterLevel`/`DecodeWaterLevel`). Covers `[0 … ~1.99997]` raw at `1/32768` raw ≈ **7.7 mm** of world height (1 raw = 251 m) — far finer than the 1 m voxel. Nonzero exactly where `WBID` is nonzero; the value is the pixel's body level. |
| `RDHW` / `0x57484452` | `i32 pathCount`, then per path: `i32 pointCount` + `pointCount` × (`f32 X` · `f32 Y`) | Highway tier. **Tags, not file position, identify the tier** — the v1 order-fragility is gone. Road sections may legitimately be **present but empty** (zero paths): the `SkipRoads` config toggle exports a road-less iteration blueprint. |
| `RDBR` / `0x52424452` | same layout | Branch tier. |
diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs
index f23a402..2ae8816 100644
--- a/Core/Scripts/ConfigManager.cs
+++ b/Core/Scripts/ConfigManager.cs
@@ -113,42 +113,14 @@ 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. LOWGROUND is the LOCKED
- // default — the task-22/23 gate verdict ("short" stays available for the
- // record). RiverSeaMargin is the bed's
+ // dials on the flow-proportional bed profile. 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.
- // RiverStepDropM/RiverWaterDepthM (task 23): the stepped-water dials — each
- // river is a chain of flat water-body reaches; a new reach starts every
- // StepDrop metres of bed descent and sits WaterDepth metres above its bed.
- // Smaller drop = more, finer steps = smoother water (the smoothing dial;
- // tilted continuous-slope water is the deferred model B).
public static string Rivers = "off";
public static string RiverRoutingStyle = "lowground";
- public static float RiverWidthScale = 1.75f; // widened at the task-23 gate's ask
- // 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.
- // 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 = 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
+ public static float RiverWidthScale = 1.0f;
+ public static float RiverDepthScale = 1.0f;
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
// Island falloff shaping (task 11).
@@ -356,22 +328,6 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"];
if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"];
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"];
- 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);
- RiverStepDropM = Mathf.Clamp(RiverStepDropM, 0.25f, 10f);
- RiverWaterDepthM = Mathf.Clamp(RiverWaterDepthM, 0.2f, 5f);
RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f);
RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f);
RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f);
diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs
index 8f05503..d86ed9b 100644
--- a/Core/Scripts/MapDataParser.cs
+++ b/Core/Scripts/MapDataParser.cs
@@ -47,7 +47,6 @@ namespace IslaApocalypse.Core
{
public const byte TYPE_OCEAN = 0;
public const byte TYPE_LAKE = 1;
- public const byte TYPE_RIVER = 2; // task 23: a stepped river REACH (one flat level)
public const byte SALINITY_FRESH = 0;
public const byte SALINITY_SALT = 1;
@@ -163,24 +162,6 @@ namespace IslaApocalypse.Core
{
public static WorldBlueprint LoadMapData(string seedStr)
{
- // ISLA_BLUEPRINT_PATH (task 25, FILE SAFETY): load an explicit blueprint
- // FILE instead of the seed-named one in the runtime root. Capture and A/B
- // tooling points this straight at a batch folder, so it never has to copy
- // a blueprint over — or delete — the file the developer has staged at the
- // root to view a world in 3D. Loud, so a forgotten env var cannot be
- // mistaken for the root blueprint.
- string envPath = OS.GetEnvironment("ISLA_BLUEPRINT_PATH");
- if (!string.IsNullOrEmpty(envPath))
- {
- if (File.Exists(envPath))
- {
- GD.Print($"[MapDataParser] ⚠ ISLA_BLUEPRINT_PATH override: loading '{envPath}' " +
- "(NOT the runtime-root blueprint).");
- return LoadMapDataFromPath(envPath);
- }
- GD.PrintErr($"[MapDataParser] ISLA_BLUEPRINT_PATH set but '{envPath}' does not exist — " +
- "falling back to the runtime-root blueprint.");
- }
string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat");
return LoadMapDataFromPath(filePath);
}
@@ -451,7 +432,7 @@ namespace IslaApocalypse.Core
body.SurfaceLevel = reader.ReadSingle();
body.PixelCount = reader.ReadInt32();
body.Centroid = new Vector2(reader.ReadSingle(), reader.ReadSingle());
- if (body.Type > WaterBodyInfo.TYPE_RIVER)
+ if (body.Type > WaterBodyInfo.TYPE_LAKE)
{
GD.PrintErr($"[MapDataParser] ERROR: water body {i} has unknown type {body.Type}.");
return false;
diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs
index eaa8b7c..e8ff480 100644
--- a/Tools/Scripts/MapGenerator.cs
+++ b/Tools/Scripts/MapGenerator.cs
@@ -237,16 +237,7 @@ public partial class MapGenerator : TextureRect
// only: biomes and WBID are already computed from classify — the oracle is
// untouched by construction. NO WATER — part 2b.
if (_riversOn)
- {
CarveRivers();
- // The 0_height/0_water snapshots were captured before rivers existed —
- // re-draw and re-capture so the exported PNGs show the carved beds and
- // the new river water (task-22 nit 2).
- DrawHeightStageTexture();
- await CaptureStage("0_height");
- DrawWaterStageTexture();
- await CaptureStage("0_water");
- }
if (ConfigManager.SkipRoads)
{
@@ -335,12 +326,12 @@ public partial class MapGenerator : TextureRect
// 6. Pull the image from the invisible monitor and save it!
Image capture = offscreenVP.GetTexture().GetImage();
string seedStr = _noise.Seed.ToString();
- string fileName = System.IO.Path.Combine(OutputDir(), $"Map_Seed_{seedStr}_{label}.png");
+ string fileName = $"user://Map_Seed_{seedStr}_{label}.png";
Error saveResult = capture.SavePng(fileName);
if (saveResult == Error.Ok)
- GD.Print($"{T()} Map saved: {fileName}");
+ GD.Print($"{T()} Map saved: {ProjectSettings.GlobalizePath(fileName)}");
else
GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}");
@@ -348,40 +339,18 @@ public partial class MapGenerator : TextureRect
offscreenVP.QueueFree();
}
- ///
- /// Where generated artifacts go. Normally the runtime root (`user://`), but
- /// ISLA_EXPORT_DIR redirects EVERYTHING this run writes — blueprints and stage
- /// snapshots — into a task folder instead (task 25, FILE SAFETY). Batch and A/B
- /// tooling sets it so a generation can never overwrite the blueprint the
- /// developer has staged at the root to view a world in 3D, and so no cleanup
- /// move/delete is needed afterwards. Loud on use.
- ///
- private string OutputDir()
- {
- string dir = OS.GetEnvironment("ISLA_EXPORT_DIR");
- if (!string.IsNullOrEmpty(dir))
- {
- if (System.IO.Directory.Exists(dir)) return dir;
- GD.PrintErr($"[MapGenerator] ISLA_EXPORT_DIR '{dir}' does not exist — writing to the runtime root instead.");
- }
- return ProjectSettings.GlobalizePath("user://");
- }
-
private void ExportMapData()
{
string seedStr = _noise.Seed.ToString();
- string outDir = OutputDir();
- if (outDir != ProjectSettings.GlobalizePath("user://"))
- GD.Print($"{T()} ⚠ ISLA_EXPORT_DIR override: writing artifacts to '{outDir}' (NOT the runtime root).");
// PRIMARY: the v2 tagged-section container (Core/Scripts/BLUEPRINT_FORMAT.md),
// under the name the server looks for.
- string v2Path = System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}.dat");
+ string v2Path = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat");
BlueprintWriter.WriteV2(v2Path, BuildBlueprint());
// SAFETY NET: the legacy v1 format beside it, until the developer has lived
// with v2 across several regenerations. Removal is a future task.
- ExportMapDataV1(System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}_v1.dat"));
+ ExportMapDataV1(ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}_v1.dat"));
}
///
@@ -867,17 +836,6 @@ 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; }
@@ -888,16 +846,10 @@ public partial class MapGenerator : TextureRect
? RiverCarvePass.STYLE_SHORT : RiverCarvePass.STYLE_LOWGROUND,
WidthScale = ConfigManager.RiverWidthScale,
DepthScale = ConfigManager.RiverDepthScale,
- SeaMarginM = ConfigManager.RiverSeaMargin,
- TribWaterMinFlowPx = ConfigManager.RiverTribWaterMinFlow,
- TribTaperPx = ConfigManager.RiverTribTaperPx,
- FillFraction = ConfigManager.RiverFillFraction,
- BankFlare = ConfigManager.RiverBankFlare,
- BankMaxCutM = ConfigManager.RiverBankMaxCutM,
- LakeMinTargetPx = ConfigManager.RiverLakeMinTargetPx
+ SeaMarginM = ConfigManager.RiverSeaMargin
};
var st = RiverCarvePass.Apply(_heightMap, MapSize, isOcean, isClassifyWater,
- isSignificantWater, southX, southY, seaMap, seaFlat,
+ southX, southY, seaMap, seaFlat,
_impactCenter.X, _impactCenter.Y,
_impactRadius * ConfigManager.CraterErosionCore,
() => (Time.GetTicksMsec()) / 1000.0, p);
@@ -911,45 +863,6 @@ public partial class MapGenerator : TextureRect
for (int y = 0; y < MapSize; y++)
if (_heightMap[x, y] > topAfter) topAfter = _heightMap[x, y];
- // --- Stepped river water (task 23, part 2b): reaches as flat water bodies.
- // Levels-not-cells, the existing model: WBID cells + WBTB entries per reach;
- // the writer derives WSRF from body levels. Touches no heights, so the
- // flood-guard count above stays valid; touches no classify data, so the
- // BIOME oracle holds (0_water changes — that IS the river water).
- ushort nextBodyId = 1;
- foreach (var b in _waterBodies)
- if (b.Id >= nextBodyId) nextBodyId = (ushort)(b.Id + 1);
- var reaches = RiverCarvePass.AddSteppedWater(_heightMap, MapSize, _waterBodyIds,
- nextBodyId, st.Carved, seaMap, seaFlat,
- _impactCenter.X, _impactCenter.Y,
- _impactRadius * ConfigManager.CraterErosionCore,
- ConfigManager.RiverStepDropM, ConfigManager.RiverWaterDepthM,
- ConfigManager.RiverFillFraction);
- long riverWetPx = 0;
- foreach (var reach in reaches)
- {
- _waterBodies.Add(new WaterBodyInfo
- {
- Id = reach.Id,
- Type = WaterBodyInfo.TYPE_RIVER,
- Salinity = WaterBodyInfo.SALINITY_FRESH,
- SurfaceLevel = reach.Level,
- PixelCount = reach.PixelCount,
- Centroid = new Vector2((float)reach.Cx, (float)reach.Cy)
- });
- riverWetPx += reach.PixelCount;
- }
- 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, " +
- $"fill {ConfigManager.RiverFillFraction:P0} of bed depth (min {ConfigManager.RiverWaterDepthM:F1} m), bank flare {ConfigManager.RiverBankFlare:F1}x (shoulder cut cap {ConfigManager.RiverBankMaxCutM:F1} m).");
-
GD.Print($"{T()} [Rivers] v1 '{ConfigManager.RiverRoutingStyle}': plan {st.AnalysisSeconds:F1}s, " +
$"routing {st.RoutingSeconds:F1}s, carve {st.CarveSeconds:F1}s " +
$"({(Time.GetTicksMsec() - tRiv0) / 1000.0:F1}s total). " +
diff --git a/Tools/Scripts/README.md b/Tools/Scripts/README.md
index 2ae0acd..53398ab 100644
--- a/Tools/Scripts/README.md
+++ b/Tools/Scripts/README.md
@@ -109,48 +109,7 @@ Generates the entire 2D blueprint. Roughly in order:
sea + `RiverSeaMargin` everywhere (the erosion flood-guard discipline: below-sea cells
read-only, zero new below-sea cells, coastline provably fixed; asserted per generation).
Slots AFTER towns (town placement reads the render map and must not shift) and BEFORE roads.
- Crater core excluded.
-
- **River water (task 23, C0b finale)** — the payoff. Each carved main river becomes a chain of
- **stepped flat water-body REACHES**: a new reach starts every `RiverStepDropM` (2 m) of bed
- descent and sits `RiverWaterDepthM` (1.2 m) above its bed, each reach strictly lower than the
- one upstream, stepping down to the sea (small vertical drops — riffles — smoothable purely by
- the step dial; tilted continuous-slope water is the deferred model B). Reaches are ordinary
- water bodies: WBID cells + a WBTB entry (`type 2 = river`, fresh) each; WSRF derives from body
- levels exactly as for lakes, and river water renders through the C1/task-13 path with the
- task-15 presence rule at its banks. Lake-enders route the last reach into their lake by the
- lowground Dijkstra (a pooling terminal IS a local minimum — blind descent dead-ends there);
- rivers meet lakes and the sea without repainting them (existing bodies are never overwritten).
- Only the lowland reaches are ROUTE-SMOOTHED (RDP+Chaikin, the road pipeline): the upland stems
- already thread carved valley floors, and smoothing them off-line cut valley walls (measured).
- The BIOME oracle holds (classify untouched; `1_biomes` md5-identical); `0_water` and the
- re-captured `0_height` change — that IS the river water and its beds (both snapshots are
- re-captured after the river pass so the exported PNGs show them). Max-cut is now reported
- CUMULATIVELY vs the pre-pass surface.
-
- **Polish (task 24)** — tributaries carry water too, gated by along-course flow
- (`RiverTribWaterMinFlow`, 0 = water them end to end) and **tapered** at the wet→dry
- transition over `RiverTribTaperPx`: the strip narrows and its surface drops to the bed, so a
- stream head fades instead of ending in a wall of water. Lake-enders route to the nearest
- **significant** water body (`RiverLakeMinTargetPx`, 20k cells) — "nearest wet cell" was
- satisfied by a 322-px puddle beside a 197k-px lagoon. Steps are finer (`RiverStepDropM` 0.6 m)
- and beds deeper (`RiverDepthScale` 1.5, `RiverWaterDepthM` 2.2 m) so the chain reads as a graded
- descent rather than a pond staircase: 1034 reaches, adjacent-reach level gaps median 0.12 m.
-
- **Polish r2 (task 25)** — tributaries carry water their **full** promoted length
- (`RiverTribWaterMinFlow` 0) while still fading at the tip (the taper zone is always
- `RiverTribTaperPx` long, starting wherever water begins). The water surface is
- `RiverFillFraction` (0.80) of the **local** bed depth rather than a fixed height, floored by
- `RiverWaterDepthM` — a fixed height was a trickle in a 13 m mouth and over the rim at a 1.5 m
- head. Bank shoulders flare `RiverBankFlare` (3.2) half-widths with a smootherstep so water meets
- land as a shore, and the **shoulder** may not lower a cell more than `RiverBankMaxCutM` (3 m)
- **measured against the pre-pass surface** — gentle ground flares, a ridge crossing keeps steep
- walls, and max cut fell to 16.8 m with no cell past 20 m.
-
- **FILE SAFETY** — `ISLA_BLUEPRINT_PATH` (load a specific blueprint file) and `ISLA_EXPORT_DIR`
- (write all of a run's blueprints and snapshots into a task folder) exist so tooling never writes
- to, or deletes from, the runtime root — the developer stages `MapData_Seed_*.dat` there to view
- worlds in 3D. Batch drivers set both and contain no delete of any kind.
+ Crater core excluded. **NO WATER yet** — part 2b waters the gated routing style.
3. **Sea level and water** — sea level per the configured model (`SeaLevelModel`: `"flat"` scalar
— the default, `SeaLevelValue` 0.15 — or the legacy `"field"` latitude Lerp); flood fill
diff --git a/Tools/Scripts/RiverCarvePass.cs b/Tools/Scripts/RiverCarvePass.cs
index 60f5f10..a5d803b 100644
--- a/Tools/Scripts/RiverCarvePass.cs
+++ b/Tools/Scripts/RiverCarvePass.cs
@@ -66,30 +66,6 @@ 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 = 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;
public DrainageAnalysis.Params PlanParams = new();
}
@@ -108,43 +84,17 @@ public static class RiverCarvePass
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 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
- // (narrowing and shallowing to the bed) so a stream head fades out.
- public float[] Flow;
- public int WetFrom;
- public int TaperPx;
- }
-
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 float MaxCutM;
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, bool[] isSignificantWater, float southX, float southY,
+ bool[] isClassifyWater, float southX, float southY,
float[,] seaMap, float seaFlat,
float craterCx, float craterCy, float craterCoreRadius,
Func secondsNow, Params p)
@@ -172,14 +122,6 @@ public static class RiverCarvePass
// --- 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)
{
@@ -198,31 +140,10 @@ public static class RiverCarvePass
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")
- {
- // 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; }
- }
-
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;
+ rs.ReachedOcean = g.Kind != "routed" || (route != null && route.Count > 0);
foreach (var trib in g.Tributaries)
CarveTributary(trib, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats);
}
@@ -230,61 +151,13 @@ 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),
/// 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)
+ bool[] isOcean, int sx, int sy, byte style, Func seaAt)
{
int total = n * n;
var gcost = new float[total];
@@ -306,7 +179,7 @@ public static class RiverCarvePass
int c = pq.Dequeue();
if (closed[c]) continue;
closed[c] = true;
- if (targets[c]) { goal = c; break; }
+ if (isOcean[c]) { goal = c; break; }
int cx = c / n, cy = c % n;
float hc = height[cx, cy];
for (int k = 0; k < 8; k++)
@@ -346,10 +219,7 @@ public static class RiverCarvePass
{
var course = new List<(float x, float y)>(trib.Course);
course.Reverse(); // head → confluence
- // 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,
+ CarveRiver(null, "tributary", trib.DrainageAreaPx, course, null,
height, n, seaAt, coreSq, craterCx, craterCy, p, stats);
}
@@ -364,16 +234,9 @@ public static class RiverCarvePass
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));
- }
+ pts.AddRange(lowlandRoute.GetRange(1, lowlandRoute.Count - 1));
// Densify to ~1-px samples (plan courses are decimated ×4).
var dense = new List<(float x, float y)>();
@@ -446,7 +309,7 @@ public static class RiverCarvePass
for (int i = 0; i < m; i++)
{
float hw = halfW[i];
- float outer = hw * p.BankFlare;
+ 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;
@@ -472,188 +335,27 @@ public static class RiverCarvePass
}
else
{
- // 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);
+ float f = (r - hw) / hw; // 0..1 across the shoulder
+ f = f * f * (3f - 2f * f); // smoothstep
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;
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;
+ if (cutM > stats.MaxCutM) stats.MaxCutM = cutM;
+ if (cutM > rs.MaxCutM) rs.MaxCutM = cutM;
rs.VolumeM3 += cutM;
}
}
}
}
- if (name != null)
- {
- 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, DepthM = depth,
- Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx
- });
- }
+ if (name != null) stats.Rivers.Add(rs);
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, float fillFraction)
- {
- 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;
- // 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
- // 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)
- {
- // 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];
- // 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;
- 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++)
- {
- // 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++)
- {
- 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
- // 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;
- }
- }
- }
- if (reach.PixelCount > 0)
- {
- reach.Cx /= reach.PixelCount; reach.Cy /= reach.PixelCount;
- reaches.Add(reach);
- nextId++;
- lastLevel = level;
- }
- i = j;
- }
- }
- return reaches;
- }
}
diff --git a/Tools/Scripts/RiverCarvePass.cs.uid b/Tools/Scripts/RiverCarvePass.cs.uid
deleted file mode 100644
index 728bfbb..0000000
--- a/Tools/Scripts/RiverCarvePass.cs.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dufsppmyn4g8v