diff --git a/Core/Scripts/BLUEPRINT_FORMAT.md b/Core/Scripts/BLUEPRINT_FORMAT.md index 6c0da54..87eb253 100644 --- a/Core/Scripts/BLUEPRINT_FORMAT.md +++ b/Core/Scripts/BLUEPRINT_FORMAT.md @@ -55,8 +55,7 @@ Every section: `[u32 tag][u64 payload-length in bytes][payload]`. | `HGTS` / `0x53544748` | `MapSize²` × `f32` height, **X outer / Y inner** | Mandatory. The second index is the map's north/south axis; the server consumes it as world **Z**. (This axis convention was never written down for v1 — it is now normative.) Length must equal `4·MapSize²`. | | `BIOM` / `0x4D4F4942` | `MapSize²` × `u8` biome ordinal, same pixel order | Mandatory. Ordinals from `Enums.cs::Biome` — **append-only, never reorder** (the ordinal IS the wire value). Writer refuses ordinals > 255; reader rejects ordinals ≥ the known biome count (parse-time validation; the palette's runtime Dirt fallback for in-memory values is unchanged). Length must equal `MapSize²`. | | `TOWN` / `0x4E574F54` | `i32 count`, then per town: `f32 X` · `f32 Y` · `u8 tier` · `u8 isHighwayNode` (0/1) | Tier ordinals from `Enums.cs::TownTier`, append-only, range-checked on read. The highway-node flag is what the generator's road topology was built from (v1 dropped it); carried and exposed on the parsed blueprint, consumed by nothing server-side yet. | -| `TCRV` / `0x56524354` | 50 B base: `u16 curveVersion` · `f32 knot1..knot4` · `f32 spikeMax` · `f32 sea` · `f32 orangeCeil` · `f32 redCeil` · `f32 benchLo` · `f32 benchHi` · `f32 peakCap` · `f32 tailSlope`. **When `curveVersion ≥ 4`, a 36 B modulation extension follows:** `f32 benchAmp` · `f32 plateauAmp` · `f32 shelfSpanMin` · `f32 shelfSpanMax` · `f32 elevFreqIslands` · `f32 strengthFreqIslands` · `i32 benchSeedOffset` · `i32 plateauSeedOffset` · `i32 strengthSeedOffset`. **When `curveVersion ≥ 5`, a 9 B preset extension follows:** `u8 presetId` (1 = compact, 2 = balanced) · `f32 k5` · `f32 k6` — with the four base knot slots this makes the effective curve unambiguous from the record alone (record total 95 B; the length-framed section + version byte keep every layout change safe) | Optional — present iff the height-redistribution curve shaped this blueprint's `HGTS` (config `TerrainCurve: "v3"`); absent = raw legacy profile. **The `u16 curveVersion` selects the field semantics and the unserialized anchor set:** v1 — knots t1..t4, spikeMax = pooled calibration max, benchLo/Hi = the 50 m plateau's lo/hi. v2 — as v1 but spikeMax = the seed's effective raw pre-curve maximum (per-seed spike normalizer). v3 — knots = K1..K4 of six (K5 = 0.930304, K6 = 1.050720 are version constants, not serialized), spikeMax per-seed, benchLo/benchHi = the fixed 100 m / 220 m shelves. **v4 (current)** — as v3, but benchLo/benchHi are the shelf **BASE** anchors and the extension record carries the spatial-modulation parameters: shelf elevations vary ±benchAmp/±plateauAmp and shelf strength blends the output span across `[shelfSpanMin, shelfSpanMax]`, via Simplex fields seeded `resolvedWorldSeed + seedOffset` at `freq/MapSize` (frequencies stated in undulations per island width). Because spikeMax is per-seed (v2+), blueprints are not reproducible from curve constants alone — that is why it is recorded. **Metadata only:** exported heights are already curved; nothing re-applies the map. Calibration provenance: `HeightCurve.cs` header + the task-05/06/07 reports. | -| `TDTL` / `0x4C544454` | 38 B: `u16 detailVersion` · `f32 reliefAmpM` · `f32 reliefFreqIslands` · `f32 incK` · `f32 incP` · `f32 incCapM` · `f32 seaClampRaw` · `f32 craterExclFactor` · `f32 shelfIncWeight` · `i32 reliefSeedOffset` | Optional — present iff the terrain detail passes shaped this blueprint's `HGTS` (config `TerrainDetail: "v1"`, requires the curve): shelf micro-relief (±reliefAmpM, shelf-ness weighted) and D8 drainage incision (`depth = K·accum^p·slope`, capped, riser-masked, sea+1 m clamped, crater-excluded). **Metadata only** — heights are already detailed; the incision channels are the designated future river routes (Phase C). Machinery: `Tools/Scripts/TerrainDetailPass.cs`. | +| `TCRV` / `0x56524354` | 50 B: `u16 curveVersion` · input knots `f32 t1..t4` · `f32 spikeMax` · output bands `f32 sea` · `f32 orangeCeil` · `f32 redCeil` · `f32 plateauLo` · `f32 plateauHi` · `f32 peakCap` · `f32 tailSlope` | Optional — present iff the height-redistribution curve shaped this blueprint's `HGTS` (config `TerrainCurve: "v2"`); absent = raw legacy profile. **`spikeMax` semantics by version:** v1 = the pooled calibration max (identical every seed); **v2 = the SEED'S own effective raw pre-curve maximum** — the per-seed spike normalizer, which is why blueprints are no longer reproducible from curve constants alone and the value is recorded here. **Metadata only:** exported heights are already curved; nothing re-applies the map. The curve lives in `Tools/Scripts/HeightCurve.cs` (calibration provenance in its header + the task-05/06 reports). | | `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) · `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. | @@ -107,17 +106,12 @@ body). which model produced it (the `WBTB` levels are the observable). - **`SkipRoads`** (bool, default false) — exports with present-but-empty road sections (see the road-section table note). -- **`TerrainCurve`** (`"v5"` | `"off"`, default `"v5"` — the task-09 gate's winner, the - BALANCED preset; the tri-state and `"v1"`–`"v4"` are retired and rejected loudly) — whether - the height-redistribution curve (terraced ascent, corner-eased junctions, spatially modulated - shelves at 100±12 m / 220±20 m, per-seed-normalized 420 m summit spires, 60 % lowland) shapes - `HGTS`. -- **`TerrainDetail`** (`"v1"` | `"off"`, default `"v1"`; no-op without the curve) — the task-10 - detail passes as one judged unit: shelf micro-relief (`ShelfReliefAmp` metres, default 3) and - drainage incision. When on, `TDTL` records the parameters. Biome/water classification is curve-invariant by - construction (it classifies the retained uncurved heights); town positions and everything 3D - follow the curved terrain. When on, `TCRV` records the effective parameters including the - per-seed `spikeMax`. +- **`TerrainCurve`** (`"v2"` | `"off"`, default `"v2"`; `"v1"` retired by the task-06 + recalibration and rejected loudly) — whether the height-redistribution curve (storm ladder: + 75 % orange coverage, 50 m plateau, **per-seed-normalized 420 m peaks**) shapes `HGTS`. + Biome/water classification is curve-invariant by construction (it classifies the retained + uncurved heights); town positions and everything 3D follow the curved terrain. When on, + `TCRV` records the effective parameters including the per-seed `spikeMax`. ### Deliberately NOT a section: basins (`BSIN`) diff --git a/Core/Scripts/BlueprintFormat.cs b/Core/Scripts/BlueprintFormat.cs index 5aad3c7..99d4597 100644 --- a/Core/Scripts/BlueprintFormat.cs +++ b/Core/Scripts/BlueprintFormat.cs @@ -33,7 +33,6 @@ namespace IslaApocalypse.Core public const uint TAG_WATER_BODY_TABLE = 0x42544257; // "WBTB" public const uint TAG_WATER_SURFACE = 0x46525357; // "WSRF" public const uint TAG_TERRAIN_CURVE = 0x56524354; // "TCRV" - public const uint TAG_TERRAIN_DETAIL = 0x4C544454; // "TDTL" // WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L // (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine diff --git a/Core/Scripts/BlueprintWriter.cs b/Core/Scripts/BlueprintWriter.cs index 27fe859..56bc07c 100644 --- a/Core/Scripts/BlueprintWriter.cs +++ b/Core/Scripts/BlueprintWriter.cs @@ -33,8 +33,6 @@ namespace IslaApocalypse.Core WriteSection(writer, BlueprintFormat.TAG_PARAMS, w => WriteParams(w, bp)); if (bp.TerrainCurve != null) WriteSection(writer, BlueprintFormat.TAG_TERRAIN_CURVE, w => WriteTerrainCurve(w, bp.TerrainCurve)); - if (bp.TerrainDetail != null) - WriteSection(writer, BlueprintFormat.TAG_TERRAIN_DETAIL, w => WriteTerrainDetail(w, bp.TerrainDetail)); WriteSection(writer, BlueprintFormat.TAG_HEIGHTS, w => WriteHeights(w, bp)); WriteSection(writer, BlueprintFormat.TAG_BIOMES, w => WriteBiomes(w, bp)); @@ -134,33 +132,6 @@ namespace IslaApocalypse.Core writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil); writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap); writer.Write(c.TailSlope); - - // v4+ extension: shelf-modulation parameters. - if (c.Version >= 4) - { - writer.Write(c.BenchAmp); writer.Write(c.PlateauAmp); - writer.Write(c.ShelfSpanMin); writer.Write(c.ShelfSpanMax); - writer.Write(c.ElevFreqIslands); writer.Write(c.StrengthFreqIslands); - writer.Write(c.BenchSeedOffset); writer.Write(c.PlateauSeedOffset); - writer.Write(c.StrengthSeedOffset); - } - - // v5+ extension: knot preset id + K5/K6. - if (c.Version >= 5) - { - writer.Write(c.PresetId); - writer.Write(c.K5); writer.Write(c.K6); - } - } - - private static void WriteTerrainDetail(BinaryWriter writer, TerrainDetailInfo d) - { - writer.Write(d.Version); - writer.Write(d.ReliefAmpM); writer.Write(d.ReliefFreqIslands); - writer.Write(d.IncK); writer.Write(d.IncP); writer.Write(d.IncCapM); - writer.Write(d.SeaClampRaw); writer.Write(d.CraterExclFactor); - writer.Write(d.ShelfIncWeight); - writer.Write(d.ReliefSeedOffset); } private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp) diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs index 14e233e..a28342c 100644 --- a/Core/Scripts/ConfigManager.cs +++ b/Core/Scripts/ConfigManager.cs @@ -26,19 +26,12 @@ namespace IslaApocalypse.Core // Change this if your namespace is different public static string SeaLevelModel = "flat"; public static float SeaLevelValue = 0.15f; - // Height-redistribution curve (tasks 05–10, graduation M-7): "v5" is the - // task-09 gate's winner — the BALANCED terraced ascent with corner easing and - // modulated shelves; "off" is the raw legacy profile. "v1"–"v4" and the - // taste-batch tri-state ("v5-compact" lost, "v5-balanced" became plain "v5") - // are retired. Default: v5. - public static string TerrainCurve = "v5"; - - // Terrain detail passes (task 10): "v1" = shelf micro-relief + drainage - // incision as one judged unit (requires the curve; no-op when it is off); - // "off" disables both. ShelfReliefAmp is the micro-relief amplitude in - // metres. Defaults: v1, 3 m. - public static string TerrainDetail = "v1"; - public static float ShelfReliefAmp = 3.0f; + // Height-redistribution curve (tasks 05/06, graduation M-7): "v2" applies the + // calibrated storm-ladder curve with the per-seed peak spike (HeightCurve.cs); + // "off" is the raw legacy profile. "v1" was dropped with the v2 recalibration + // (task-05 blueprints are regenerable). Biome classification is curve-invariant + // by construction either way. Default: v2. + public static string TerrainCurve = "v2"; public static void LoadConfig() { @@ -113,32 +106,14 @@ namespace IslaApocalypse.Core // Change this if your namespace is different if (data.ContainsKey("TerrainCurve")) { string curve = (string)data["TerrainCurve"]; - if (curve == "off" || curve == "v5") + if (curve == "off" || curve == "v2") TerrainCurve = curve; - else if (curve == "v5-balanced") - GD.PrintErr($"[ConfigManager] TerrainCurve 'v5-balanced' won the task-09 gate and is now plain \"v5\". Keeping '{TerrainCurve}'."); - else if (curve == "v5-compact") - GD.PrintErr($"[ConfigManager] TerrainCurve 'v5-compact' was retired by the task-09 verdict (BALANCED won). Keeping '{TerrainCurve}' — use \"v5\" or \"off\"."); - else if (curve == "v1" || curve == "v2" || curve == "v3" || curve == "v4") - GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v5, tasks 09/10). Keeping '{TerrainCurve}' — use \"v5\" or \"off\"."); + else if (curve == "v1") + GD.PrintErr($"[ConfigManager] TerrainCurve 'v1' was retired by the v2 recalibration (task 06). Keeping '{TerrainCurve}' — use \"v2\" or \"off\"."); else GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'."); } - // Extract the terrain-detail gate + relief amplitude - if (data.ContainsKey("TerrainDetail")) - { - string detail = (string)data["TerrainDetail"]; - if (detail == "off" || detail == "v1") - TerrainDetail = detail; - else - GD.PrintErr($"[ConfigManager] Unknown TerrainDetail '{detail}'. Keeping '{TerrainDetail}'."); - } - if (data.ContainsKey("ShelfReliefAmp")) - { - ShelfReliefAmp = (float)data["ShelfReliefAmp"]; - } - switch (profile) { case "4K": diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs index 35e4695..c40ec40 100644 --- a/Core/Scripts/MapDataParser.cs +++ b/Core/Scripts/MapDataParser.cs @@ -74,31 +74,6 @@ namespace IslaApocalypse.Core // constants alone, which is exactly why it is recorded here. public float T1, T2, T3, T4, SpikeMax; public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands - - // v4 extension — shelf-modulation parameters (record layout versioned by the - // curve Version byte; zero-valued when parsing an older record). - public float BenchAmp, PlateauAmp, ShelfSpanMin, ShelfSpanMax; - public float ElevFreqIslands, StrengthFreqIslands; - public int BenchSeedOffset, PlateauSeedOffset, StrengthSeedOffset; - - // v5 extension — knot preset (1 = compact, 2 = balanced) and the two knots the - // four base slots cannot carry; with these the effective curve is unambiguous. - public byte PresetId; - public float K5, K6; - } - - /// - /// The terrain detail passes that shaped this blueprint's HGTS (v2 TDTL section, - /// terrain-water task 10): shelf micro-relief + drainage incision parameters. - /// Null when detail was off. Metadata only — heights are already detailed. - /// - public class TerrainDetailInfo - { - public ushort Version; - public float ReliefAmpM, ReliefFreqIslands; - public float IncK, IncP, IncCapM; - public float SeaClampRaw, CraterExclFactor, ShelfIncWeight; - public int ReliefSeedOffset; } public class WorldBlueprint @@ -128,9 +103,6 @@ namespace IslaApocalypse.Core // The curve that shaped HeightMap (v2 TCRV section); null = raw legacy profile. public TerrainCurveInfo TerrainCurve; - - // The detail passes that shaped HeightMap (TDTL section); null = no detail. - public TerrainDetailInfo TerrainDetail; } // 2. The Parser Utility @@ -254,7 +226,6 @@ namespace IslaApocalypse.Core else if (tag == BlueprintFormat.TAG_WATER_SURFACE) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: true); else if (tag == BlueprintFormat.TAG_WATER_BODY_TABLE) sectionOk = ParseWaterBodyTable(reader, blueprint); else if (tag == BlueprintFormat.TAG_TERRAIN_CURVE) sectionOk = ParseTerrainCurve(reader, blueprint); - else if (tag == BlueprintFormat.TAG_TERRAIN_DETAIL) sectionOk = ParseTerrainDetail(reader, blueprint); else { // The property the redesign exists to buy: future sections (water, @@ -428,39 +399,10 @@ namespace IslaApocalypse.Core c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle(); c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle(); c.TailSlope = reader.ReadSingle(); - - // v4+ extension: shelf-modulation parameters (older records simply end here — - // the section is length-framed, so the version byte is the dispatcher). - if (c.Version >= 4) - { - c.BenchAmp = reader.ReadSingle(); c.PlateauAmp = reader.ReadSingle(); - c.ShelfSpanMin = reader.ReadSingle(); c.ShelfSpanMax = reader.ReadSingle(); - c.ElevFreqIslands = reader.ReadSingle(); c.StrengthFreqIslands = reader.ReadSingle(); - c.BenchSeedOffset = reader.ReadInt32(); c.PlateauSeedOffset = reader.ReadInt32(); - c.StrengthSeedOffset = reader.ReadInt32(); - } - if (c.Version >= 5) - { - c.PresetId = reader.ReadByte(); - c.K5 = reader.ReadSingle(); c.K6 = reader.ReadSingle(); - } blueprint.TerrainCurve = c; return true; } - private static bool ParseTerrainDetail(BinaryReader reader, WorldBlueprint blueprint) - { - var d = new TerrainDetailInfo(); - d.Version = reader.ReadUInt16(); - d.ReliefAmpM = reader.ReadSingle(); d.ReliefFreqIslands = reader.ReadSingle(); - d.IncK = reader.ReadSingle(); d.IncP = reader.ReadSingle(); d.IncCapM = reader.ReadSingle(); - d.SeaClampRaw = reader.ReadSingle(); d.CraterExclFactor = reader.ReadSingle(); - d.ShelfIncWeight = reader.ReadSingle(); - d.ReliefSeedOffset = reader.ReadInt32(); - blueprint.TerrainDetail = d; - return true; - } - private static bool ParseRoadTier(BinaryReader reader, List into) { int pathCount = reader.ReadInt32(); diff --git a/Tools/Scripts/HeightCurve.cs b/Tools/Scripts/HeightCurve.cs index 2e18ab0..bced8b8 100644 --- a/Tools/Scripts/HeightCurve.cs +++ b/Tools/Scripts/HeightCurve.cs @@ -1,183 +1,135 @@ using Godot; /// -/// One preset's input knots for the v5 curve. Two presets exist for the task-09 -/// taste batch — COMPACT (maximum lowland, cordillera-from-plains) and BALANCED -/// (the task-07/08 lineage, gradual highland approach). The loser retires after -/// the developer's gate; the winner becomes plain "v5". -/// -public sealed class CurveKnots -{ - public readonly byte PresetId; - public readonly string Name; - public readonly float K1, K2, K3, K4, K5, K6; - - public CurveKnots(byte id, string name, float k1, float k2, float k3, float k4, float k5, float k6) - { - PresetId = id; Name = name; - K1 = k1; K2 = k2; K3 = k3; K4 = k4; K5 = k5; K6 = k6; - } -} - -/// -/// The height-redistribution curve, v5 (terrain-water task 09) — v4's spatially -/// modulated shelves plus the THREE CORNER FIXES, with PRESET-PARAMETERIZED knots. -/// Pure, static, monotonic; every per-column input and the knot set are explicit -/// PARAMETERS (D-035). +/// The height-redistribution curve, v2 (terrain-water task 06) — a pure, static, +/// monotonic piecewise map over raw blueprint heights (D-035: numbers in, numbers +/// out; the per-seed spike maximum is an explicit PARAMETER, not hidden state). /// -/// The corner fixes (both presets — the naturalness work): -/// 1. Riser endpoint slope floor 0.2 → 0.1 (blend 0.1u + 0.9·smoothstep): climbs -/// decelerate into shelves and accelerate out of them — no machined edges. -/// 2. Summit-spike base floor 0.1 → 0.05 (blend 0.05u + 0.95·u⁴): the spike -/// leaves the plateau gently — no hard skirt under the peaks. -/// 3. SHELF_SPAN_MIN 2 m → 6 m: pronounced shelves keep a gentle tilt — flat to -/// build on, never snooker-table flat. +/// v2 changes (developer's task-05 hillshade-gate verdict; everything below the +/// plateau step is behaviorally byte-identical to v1): +/// - PER-SEED SPIKE NORMALIZATION: the spike's input domain runs from t4 to the +/// current seed's own raw pre-curve maximum (hMaxSeed), so every island's +/// tallest pixel reaches the ceiling — v1 mapped against the pooled +/// calibration max and mid-range seeds topped out at 110–175 m. +/// - STIFFER SPIKE: ease-in 0.1u + 0.9·u⁴ (was 0.2u + 0.8·u³) — a wall, not a ramp. +/// - PEAK CEILING 420 m above sea (was 220 m). /// -/// Presets (input land-fraction targets; knots calibrated 2026-08-08 from the same -/// pooled batch-04 flat-sea land CDF as tasks 05/07, 340,618,126 samples; achieved -/// fractions exact by construction): -/// COMPACT — 65/12/7/4/6/3/3 (orange/red/foothill-riser/bench/mid-riser/plateau/spike) -/// BALANCED — 60/13/10/5/8/3/1 +/// Lower knots/bands are v1's, calibrated 2026-08-07 from batch 04's ten flat-sea +/// heightmaps (pooled above-sea land CDF, 340,618,126 samples): P75/P90/P93/P96. /// -/// Unchanged from v4: storm-ladder anchors, bench 100±12 m, plateau 220±20 m, -/// strength modulation (span max 25 m), 420 m cap, per-seed spike normalization, -/// modulation fields/seed offsets, the classify-map invariant. +/// Shape (strictly monotonic; every segment's normalized slope bounded below by a +/// positive constant; asserted numerically per generation against the EFFECTIVE +/// per-seed curve once hMaxSeed is known): +/// h ≤ sea (0.15) identity — water and the below-sea world untouched +/// sea → t1 smooth toe, ease-out blend (gentle rolling, never flat) +/// t1 → t2 linear rise into the red band +/// t2 → t3 smooth shoulder up to the plateau shelf +/// t3 → t4 near-flat plateau step (small positive slope) +/// t4 → spikeMax accelerating u⁴ spike to the 420 m peak cap (per-seed domain) +/// h > spikeMax linear tail (strict monotonicity, no clamp; reachable only +/// in the degenerate near-flat guard case) /// public static class HeightCurve { - public const ushort VERSION = 5; + public const ushort VERSION = 2; - // The task-09 taste gate's WINNER: BALANCED (id 2). COMPACT retired with the - // verdict; its knots survive only in the task-09 report/batch for the record. - public static readonly CurveKnots V5 = new CurveKnots(2, "balanced", - 0.515899f, 0.612157f, 0.710472f, 0.784045f, 0.962922f, 1.119118f); // P60/73/83/88/96/99 + // Input knots — v1 calibration, unchanged (see class header). + public const float T1 = 0.628736f; // P75 — orange coverage boundary + public const float T2 = 0.819152f; // P90 + public const float T3 = 0.879340f; // P93 + public const float T4 = 0.962922f; // P96 - // Fixed output anchors — storm ladder + ceiling (frozen). + // Output bands — the storm ladder. Lower anchors unchanged from v1. public const float SEA = 0.15f; - public const float ORANGE_CEIL = 0.206f; - public const float RED_CEIL = 0.27f; - public const float PEAK_CAP = SEA + 420f / 251f; - public const float TAIL_SLOPE = 0.25f; + public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling + public const float RED_CEIL = 0.27f; // biblical ceiling + public const float PLATEAU_LO = SEA + 50f / 251f; // ≈ 0.34924 (50 m above sea) + public const float PLATEAU_HI = PLATEAU_LO + 0.02f; // ≈ 0.36924 (~5 m step relief) + public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 (420 m above sea; v1: 220 m) + public const float TAIL_SLOPE = 0.25f; // above spikeMax (degenerate guard only) + + // Degenerate/near-flat guard: the spike domain is [T4, max(hMaxSeed, T4 + SPIKE_MIN_SPAN)], + // so a pathological seed whose raw max sits at or below t4 still yields a positive, + // monotonic domain (its cap is then simply never reached; heights above spikeMax — none in + // practice — would ride the tail). public const float SPIKE_MIN_SPAN = 0.01f; - // Modulated shelf anchors (unchanged from v4). - public const float BENCH_BASE = SEA + 100f / 251f; - public const float BENCH_AMP = 12f / 251f; - public const float PLATEAU_BASE = SEA + 220f / 251f; - public const float PLATEAU_AMP = 20f / 251f; - - // Corner fix 3: pronounced shelves keep ~6 m of tilt across the shelf band. - public const float SHELF_SPAN_MIN = 6f / 251f; // ≈ 0.0239 (v4: 0.008 ≈ 2 m) - public const float SHELF_SPAN_MAX = 0.10f; - - // Modulation-field derivation (unchanged from v4). - public const int BENCH_SEED_OFFSET = 7101; - public const int PLATEAU_SEED_OFFSET = 7207; - public const int STRENGTH_SEED_OFFSET = 7303; - public const float ELEV_FREQ_ISLANDS = 3.0f; - public const float STRENGTH_FREQ_ISLANDS = 5.0f; - - public static float EffectiveSpikeMax(float hMaxSeed, CurveKnots k) + /// The effective spike-domain top for a seed's raw maximum, guard applied. + public static float EffectiveSpikeMax(float hMaxSeed) { - return Mathf.Max(hMaxSeed, k.K6 + SPIKE_MIN_SPAN); + return Mathf.Max(hMaxSeed, T4 + SPIKE_MIN_SPAN); } - public static float ShelfSpan(float strength01) - { - return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f)); - } - - public static float Apply(float h, float hMaxSeed, - float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k) + /// Raw pre-curve height. + /// The seed's raw pre-curve maximum (post noise/falloff/Trench/spine, + /// pre-carve) — the same field the curve consumes. Makes the map seed-dependent (v2). + public static float Apply(float h, float hMaxSeed) { if (h <= SEA) return h; float u, s; - if (h < k.K1) + if (h < T1) { - u = (h - SEA) / (k.K1 - SEA); - s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe + u = (h - SEA) / (T1 - SEA); + s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out, slope ≥ 0.3 return SEA + s * (ORANGE_CEIL - SEA); } - if (h < k.K2) + if (h < T2) { - u = (h - k.K1) / (k.K2 - k.K1); - return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise + u = (h - T1) / (T2 - T1); + return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear } - if (h < k.K3) + if (h < T3) { - u = (h - k.K2) / (k.K3 - k.K2); - s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // foothill riser — corner fix 1 - return RED_CEIL + s * (benchLo - RED_CEIL); + u = (h - T2) / (T3 - T2); + s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // smoothstep blend, slope ≥ 0.2 + return RED_CEIL + s * (PLATEAU_LO - RED_CEIL); } - if (h < k.K4) + if (h < T4) { - u = (h - k.K3) / (k.K4 - k.K3); - return benchLo + u * benchSpan; // bench (min span 6 m — fix 3) + u = (h - T3) / (T4 - T3); + return PLATEAU_LO + u * (PLATEAU_HI - PLATEAU_LO); // near-flat, small positive slope } - float benchTop = benchLo + benchSpan; - if (h < k.K5) - { - u = (h - k.K4) / (k.K5 - k.K4); - s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // mid riser — corner fix 1 - return benchTop + s * (plateauLo - benchTop); - } - if (h < k.K6) - { - u = (h - k.K5) / (k.K6 - k.K5); - return plateauLo + u * plateauSpan; // plateau - } - float plateauTop = plateauLo + plateauSpan; - float spikeMax = EffectiveSpikeMax(hMaxSeed, k); + float spikeMax = EffectiveSpikeMax(hMaxSeed); if (h < spikeMax) { - u = (h - k.K6) / (spikeMax - k.K6); - s = 0.05f * u + 0.95f * (u * u * u * u); // summit spike — corner fix 2 - return plateauTop + s * (PEAK_CAP - plateauTop); + u = (h - T4) / (spikeMax - T4); + s = 0.1f * u + 0.9f * (u * u * u * u); // ease-in u⁴ wall, slope ≥ 0.1 + return PLATEAU_HI + s * (PEAK_CAP - PLATEAU_HI); } return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE; } /// - /// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve for - /// the selected preset: all 8 modulation-extreme corners × per-seed spikeMax. - /// The corner fixes lower the slope floors (risers 0.1, spike base 0.05) — the - /// sweep proves they stay strictly positive everywhere. Loud throw on failure. + /// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once + /// per generation after hMaxSeed is known, before the curve pass. A violation is + /// a build bug, not a data condition — fail loudly and refuse to generate. /// - public static void AssertMonotonic(float hMaxSeed, CurveKnots k) + public static void AssertMonotonic(float hMaxSeed) { - float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP }; - float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP }; - float[] spans = { SHELF_SPAN_MIN, SHELF_SPAN_MAX }; + float prevH = -7f; + float prev = Apply(prevH, hMaxSeed); - foreach (float bl in benchLos) + // Successive double samples can round to the SAME float32 — only strictly + // increasing float samples are compared (task-05 incident fix, kept). + void Check(double hd) { - foreach (float pl in plateauLos) - { - foreach (float sp in spans) - { - float prevH = -7f; - float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k); - - void Check(double hd) - { - float h = (float)hd; - if (h <= prevH) return; // dedupe float32 samples (task-05 fix) - float v = Apply(h, hMaxSeed, bl, sp, pl, sp, k); - if (v <= prev) - throw new System.InvalidOperationException( - $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate."); - prev = v; - prevH = h; - } - - double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed, k) + 0.5); - for (double hh = -7.0 + 0.01; hh < 0.10; hh += 0.01) Check(hh); - for (double hh = 0.10; hh <= top; hh += 0.0001) Check(hh); - for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh); - } - } + float h = (float)hd; + if (h <= prevH) return; + float v = Apply(h, hMaxSeed); + if (v <= prev) + throw new System.InvalidOperationException( + $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}): {v} <= {prev}. Refusing to generate."); + prev = v; + prevH = h; } - GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION} preset '{k.Name}', 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6})."); + + // Coarse below the identity region, fine through every knot, out past the + // per-seed spike top and the tail. + double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5); + for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h); + for (double h = 0.10; h <= top; h += 0.0001) Check(h); + for (double h = top + 0.05; h <= top + 6.0; h += 0.05) Check(h); + GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6})."); } } diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs index 40528cb..5206a53 100644 --- a/Tools/Scripts/MapGenerator.cs +++ b/Tools/Scripts/MapGenerator.cs @@ -37,24 +37,6 @@ public partial class MapGenerator : TextureRect private float[,] _heightMapClassify; private bool _curveOn; - // Curve-v4 shelf-modulation fields (task 08): two decorrelated elevation fields - // (bench 100±12 m, plateau 220±20 m) plus a strength field blending each shelf - // between pronounced-flat and barely-a-hint. Seeds derive from the RESOLVED - // noise seed + fixed offsets (no config knob); frequencies scale with MapSize. - // Sampled per column in pass 2 only — the classify path never sees them. - private FastNoiseLite _benchNoise; - private FastNoiseLite _plateauNoise; - private FastNoiseLite _strengthNoise; - - // v5: the selected knot preset; null = curve off. - private CurveKnots _curveKnots; - - // Task-10 detail passes (shelf micro-relief + drainage incision): gated by - // TerrainDetail, active only with the curve on (the masks are curve-band - // defined). The relief noise seeds from resolvedSeed + 7409. - private bool _detailOn; - private FastNoiseLite _reliefNoise; - // The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine, // pre-carve) — the v2 curve's per-seed spike normalizer. Computed in // GenerateTopography pass 1; recorded in TCRV (effective, guard applied). @@ -93,8 +75,7 @@ public partial class MapGenerator : TextureRect this.CustomMinimumSize = new Vector2(MapSize, MapSize); _heightMap = new float[MapSize, MapSize]; - _curveKnots = ConfigManager.TerrainCurve == "v5" ? HeightCurve.V5 : null; - _curveOn = _curveKnots != null; + _curveOn = ConfigManager.TerrainCurve == "v2"; // (The monotonicity assertion now runs inside GenerateTopography, against the // effective per-seed curve, once hMaxSeed is known.) _heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap; @@ -105,23 +86,11 @@ public partial class MapGenerator : TextureRect float scaleFactor = MapSize / 1024f; // Equals 4 - // --- MASTER SEED & DETAILED NOISE (Restored!) --- + // --- MASTER SEED & DETAILED NOISE (Restored!) --- _noise = new FastNoiseLite(); - _noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed; + _noise.Seed = ConfigManager.WorldSeed == 0 ? (int)GD.Randi() : ConfigManager.WorldSeed; _noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex; - _noise.Frequency = 0.004f / scaleFactor; - - if (_curveOn) - { - _benchNoise = MakeModulationNoise(HeightCurve.BENCH_SEED_OFFSET, HeightCurve.ELEV_FREQ_ISLANDS); - _plateauNoise = MakeModulationNoise(HeightCurve.PLATEAU_SEED_OFFSET, HeightCurve.ELEV_FREQ_ISLANDS); - _strengthNoise = MakeModulationNoise(HeightCurve.STRENGTH_SEED_OFFSET, HeightCurve.STRENGTH_FREQ_ISLANDS); - } - _detailOn = _curveOn && ConfigManager.TerrainDetail == "v1"; - if (_detailOn) - _reliefNoise = MakeModulationNoise(TerrainDetailPass.RELIEF_SEED_OFFSET, TerrainDetailPass.RELIEF_FREQ_ISLANDS); - else if (ConfigManager.TerrainDetail == "v1" && !_curveOn) - GD.Print("[MapGenerator] TerrainDetail v1 requires the curve — no-op with TerrainCurve off."); + _noise.Frequency = 0.004f / scaleFactor; // THE CRATER FIX: Push it into the ocean (scales via percentage of MapSize!) // Supposedly! We will have to test this manually on other map sizes to confirm the crater is properly scaled and submerged on the north coast! @@ -287,39 +256,14 @@ public partial class MapGenerator : TextureRect TrailRoads = _trailPaths, WaterBodyIds = _waterBodyIds, WaterBodies = _waterBodies ?? new List(), - // TCRV under curve v4: knot slots carry K1..K4 (K5/K6 are version constants); - // the bench slots carry the two BASE anchors; the v4 extension record carries - // the modulation parameters (amplitudes, spans, frequencies, seed offsets) — - // the blueprint stays self-describing. - // TCRV v5: knot slots carry the PRESET's K1..K4; the extension carries the - // modulation params plus preset id and K5/K6 — the effective curve is - // unambiguous from the record alone. TerrainCurve = _curveOn ? new TerrainCurveInfo { Version = HeightCurve.VERSION, - T1 = _curveKnots.K1, T2 = _curveKnots.K2, T3 = _curveKnots.K3, T4 = _curveKnots.K4, - SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed, _curveKnots), // per-seed + T1 = HeightCurve.T1, T2 = HeightCurve.T2, T3 = HeightCurve.T3, T4 = HeightCurve.T4, + SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed (v2) Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL, - PlateauLo = HeightCurve.BENCH_BASE, PlateauHi = HeightCurve.PLATEAU_BASE, - PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE, - BenchAmp = HeightCurve.BENCH_AMP, PlateauAmp = HeightCurve.PLATEAU_AMP, - ShelfSpanMin = HeightCurve.SHELF_SPAN_MIN, ShelfSpanMax = HeightCurve.SHELF_SPAN_MAX, - ElevFreqIslands = HeightCurve.ELEV_FREQ_ISLANDS, StrengthFreqIslands = HeightCurve.STRENGTH_FREQ_ISLANDS, - BenchSeedOffset = HeightCurve.BENCH_SEED_OFFSET, PlateauSeedOffset = HeightCurve.PLATEAU_SEED_OFFSET, - StrengthSeedOffset = HeightCurve.STRENGTH_SEED_OFFSET, - PresetId = _curveKnots.PresetId, K5 = _curveKnots.K5, K6 = _curveKnots.K6 - } : null, - TerrainDetail = _detailOn ? new TerrainDetailInfo - { - Version = TerrainDetailPass.VERSION, - ReliefAmpM = ConfigManager.ShelfReliefAmp, - ReliefFreqIslands = TerrainDetailPass.RELIEF_FREQ_ISLANDS, - IncK = TerrainDetailPass.INC_K, IncP = TerrainDetailPass.INC_P, - IncCapM = TerrainDetailPass.INC_CAP_M, - SeaClampRaw = TerrainDetailPass.SEA_CLAMP, - CraterExclFactor = TerrainDetailPass.CRATER_EXCL_FACTOR, - ShelfIncWeight = TerrainDetailPass.SHELF_INC_WEIGHT, - ReliefSeedOffset = TerrainDetailPass.RELIEF_SEED_OFFSET + PlateauLo = HeightCurve.PLATEAU_LO, PlateauHi = HeightCurve.PLATEAU_HI, + PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE } : null, FormatVersion = 2, Params = new BlueprintParams @@ -476,7 +420,7 @@ public partial class MapGenerator : TextureRect // The v2 curve is SEED-DEPENDENT: its spike maps [t4, hMaxSeed] onto the peak // band, so the monotonicity assertion must run against the EFFECTIVE per-seed // curve — after hMaxSeed is known, before any pixel is curved. - if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed, _curveKnots); + if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed); // --- PASS 2: curve (task 05/06) + crater carve --- // Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so @@ -484,54 +428,16 @@ public partial class MapGenerator : TextureRect // the curve. Identity at and below sea + this ordering preserve the // Trench/ocean-border guarantee and the crater by construction. classifyH // stays uncurved — see _heightMapClassify; hMaxSeed never touches it. - // --- PASS 2a: curve + shelf micro-relief (task 10 pass A) --- - // classify stays RAW; curved gets the v5 curve plus, when TerrainDetail is on, - // the shelf-ness-weighted noise skin (risers and peaks untouched). - float reliefAmpRaw = ConfigManager.ShelfReliefAmp / 251f; - for (int x = 0; x < MapSize; x++) - { - for (int y = 0; y < MapSize; y++) - { - float raw = _heightMap[x, y]; - float curvedH; - if (_curveOn) - { - // per-column shelf modulation (v4) — anchors and strength from the - // low-frequency fields; ordering safety by construction. - float benchLo = HeightCurve.BENCH_BASE + _benchNoise.GetNoise2D(x, y) * HeightCurve.BENCH_AMP; - float plateauLo = HeightCurve.PLATEAU_BASE + _plateauNoise.GetNoise2D(x, y) * HeightCurve.PLATEAU_AMP; - float shelfSpan = HeightCurve.ShelfSpan((_strengthNoise.GetNoise2D(x, y) + 1f) * 0.5f); - curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan, _curveKnots); - - if (_detailOn) - { - float wShelf = TerrainDetailPass.ShelfWeight(raw, _curveKnots); - if (wShelf > 0f) - curvedH += _reliefNoise.GetNoise2D(x, y) * reliefAmpRaw * wShelf; - } - } - else - { - curvedH = raw; - } - - _heightMapClassify[x, y] = raw; // uncurved; carve joins in pass 2c - _heightMap[x, y] = curvedH; - } - } - - // --- PASS 2b: drainage incision (task 10 pass B) --- - if (_curveOn && _detailOn) - RunIncisionPass(); - - // --- PASS 2c: THE CRATER CARVE (The Flooded Bay & Landbridge Fix!) --- - // The carve remains the FINAL authority on its own terrain: applied after - // curve/relief/incision, to both maps, with the original expression. float physicalCraterRadius = _impactRadius * 0.80f; for (int x = 0; x < MapSize; x++) { for (int y = 0; y < MapSize; y++) { + float raw = _heightMap[x, y]; + float classifyH = raw; + float curvedH = _curveOn ? HeightCurve.Apply(raw, _hMaxSeed) : raw; + + // --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) --- float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter); // We only carve the physical hole at 80% of the radius to guarantee a landbridge! @@ -540,82 +446,16 @@ public partial class MapGenerator : TextureRect float craterDepth = 1.0f - (distToCrater / physicalCraterRadius); // Dialed back to -0.15f as per your excellent instinct! float carveTarget = GetSeaLevel(_tempMap[x, y]) - 0.15f; - // Read BOTH before writing EITHER: with the curve off the two maps - // alias the same array, and a sequential read-modify-write carved - // the crater twice (caught by the task-10 continuity oracle). - float preClassify = _heightMapClassify[x, y]; - float preCurved = _heightMap[x, y]; - _heightMapClassify[x, y] = Mathf.Lerp(preClassify, carveTarget, craterDepth * 0.9f); - _heightMap[x, y] = Mathf.Lerp(preCurved, carveTarget, craterDepth * 0.9f); + classifyH = Mathf.Lerp(classifyH, carveTarget, craterDepth * 0.9f); + curvedH = Mathf.Lerp(curvedH, carveTarget, craterDepth * 0.9f); } + + _heightMapClassify[x, y] = classifyH; + _heightMap[x, y] = curvedH; } } } - /// - /// Task-10 pass B: D8 flow accumulation over the curved+relieved land, then - /// depth = K · accum^p · slope, masked to the risers (shelves feathered to 30 %, - /// toe and peaks zero, crater excluded), capped, and clamped to sea + 1 m. - /// Prints its own MEASURED depth distribution — the tuning/report source. - /// - private void RunIncisionPass() - { - ulong t0 = Time.GetTicksMsec(); - int n = MapSize; - int total = n * n; - - float[] flat = new float[total]; - for (int x = 0; x < n; x++) - for (int y = 0; y < n; y++) - flat[x * n + y] = _heightMap[x, y]; - - int[] accum = TerrainDetailPass.FlowAccumulation(flat, n, out float[] drop); - double accumSeconds = (Time.GetTicksMsec() - t0) / 1000.0; - - float capRaw = TerrainDetailPass.INC_CAP_M / 251f; - float exclR = _impactRadius * TerrainDetailPass.CRATER_EXCL_FACTOR; - float featherR = _impactRadius * TerrainDetailPass.CRATER_FEATHER_FACTOR; - long incised = 0, clampHits = 0; - var depthsM = new System.Collections.Generic.List(1 << 20); - - for (int x = 0; x < n; x++) - { - for (int y = 0; y < n; y++) - { - float raw = _heightMapClassify[x, y]; - float w = TerrainDetailPass.IncisionWeight(raw, _curveKnots); - if (w <= 0f) continue; - - float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter); - if (distToCrater < exclR) continue; - if (distToCrater < featherR) - w *= (distToCrater - exclR) / (featherR - exclR); - - int i = x * n + y; - float depth = TerrainDetailPass.INC_K - * Mathf.Pow(accum[i], TerrainDetailPass.INC_P) * drop[i]; - depth = Mathf.Min(depth, capRaw) * w; - if (depth <= 0f) continue; - - float nh = _heightMap[x, y] - depth; - if (nh < TerrainDetailPass.SEA_CLAMP) - { - nh = TerrainDetailPass.SEA_CLAMP; - clampHits++; - } - float realized = _heightMap[x, y] - nh; - if (realized * 251f >= 0.5f) { incised++; depthsM.Add(realized * 251f); } - _heightMap[x, y] = nh; - } - } - - depthsM.Sort(); - float P(double q) => depthsM.Count == 0 ? 0 : depthsM[Mathf.Clamp((int)(q * depthsM.Count), 0, depthsM.Count - 1)]; - double totalSeconds = (Time.GetTicksMsec() - t0) / 1000.0; - GD.Print($"{T()} [Incision] accumulation {accumSeconds:F1}s, total {totalSeconds:F1}s."); - GD.Print($"{T()} [Incision] incised cells (≥0.5 m): {incised}; depth m: p50 {P(0.5):F1}, p90 {P(0.9):F1}, p99 {P(0.99):F1}, max {(depthsM.Count > 0 ? depthsM[depthsM.Count - 1] : 0):F1}; sea-clamp hits {clampHits}."); - } - private void CalculateTrueOcean() { Queue queue = new Queue(); @@ -680,15 +520,6 @@ public partial class MapGenerator : TextureRect // grid and the water-body grid holds BY CONSTRUCTION, not by parallel // implementations agreeing. // ===================================================================== - private FastNoiseLite MakeModulationNoise(int seedOffset, float periodsPerIsland) - { - var n = new FastNoiseLite(); - n.Seed = _noise.Seed + seedOffset; // deterministic from the RESOLVED seed - n.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex; - n.Frequency = periodsPerIsland / MapSize; // frequency stated in island-widths - return n; - } - private bool IsWaterPixel(int x, int y) => _heightMapClassify[x, y] < GetSeaLevel(_tempMap[x, y]); private bool IsOceanPixel(int x, int y) => IsWaterPixel(x, y) && _isTrueOcean[x, y]; private bool IsLakePixel(int x, int y) => IsWaterPixel(x, y) && !_isTrueOcean[x, y]; @@ -969,8 +800,7 @@ public partial class MapGenerator : TextureRect Color shallowSea = new Color(0.25f, 0.45f, 0.65f); Color green = new Color(0.44f, 0.62f, 0.36f); // orange band terrain: lowland green Color tan = new Color(0.76f, 0.70f, 0.46f); // red band: tan - Color brown = new Color(0.55f, 0.41f, 0.28f); // foothill riser + bench: brown - Color darkBrown = new Color(0.42f, 0.32f, 0.24f); // mid riser: darker brown + Color brown = new Color(0.55f, 0.41f, 0.28f); // shoulder + plateau: brown Color white = new Color(0.97f, 0.97f, 0.98f); // peaks Color grey = new Color(0.72f, 0.72f, 0.70f); @@ -990,14 +820,10 @@ public partial class MapGenerator : TextureRect } else if (h < HeightCurve.ORANGE_CEIL) tint = green; else if (h < HeightCurve.RED_CEIL) tint = tan; - // v4 note: shelf anchors are spatially modulated, so these tint bands - // use the base±amplitude envelopes — approximate banding, visualization only. - else if (h < HeightCurve.BENCH_BASE + HeightCurve.BENCH_AMP) tint = brown; // riser + bench envelope - else if (h < HeightCurve.PLATEAU_BASE - HeightCurve.PLATEAU_AMP) tint = darkBrown; // mid riser + else if (h < HeightCurve.PLATEAU_HI) tint = brown; else { - // Plateau envelope through the summit spike to the 420 m cap. - float t2 = Mathf.Clamp((h - (HeightCurve.PLATEAU_BASE - HeightCurve.PLATEAU_AMP)) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU_BASE + HeightCurve.PLATEAU_AMP), 0f, 1f); + float t2 = Mathf.Clamp((h - HeightCurve.PLATEAU_HI) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU_HI), 0f, 1f); tint = grey.Lerp(white, t2); } @@ -1541,40 +1367,40 @@ public partial class MapGenerator : TextureRect } private void SetBaseAStarWeights(AStarGrid2D astar) { - for (int x = 0; x < MapSize; x++) { - for (int y = 0; y < MapSize; y++) { - Biome b = _biomeMap[x, y]; - - // 1. CRITICAL FIX: Make the crater physically impassable first! - if (b == Biome.Crater) { - astar.SetPointSolid(new Vector2I(x, y), true); - continue; // "Skip the rest, go to next pixel" - } + for (int x = 0; x < MapSize; x++) { + for (int y = 0; y < MapSize; y++) { + Biome b = _biomeMap[x, y]; + + // 1. CRITICAL FIX: Make the crater physically impassable first! + if (b == Biome.Crater) { + astar.SetPointSolid(new Vector2I(x, y), true); + continue; // "Skip the rest, go to next pixel" + } - // 2. Existing water/mainland checks - if (b == Biome.Ocean || b == Biome.Lake || !_isMainland[x, y]) { - astar.SetPointSolid(new Vector2I(x, y), true); - continue; // "Skip the rest, go to next pixel" - } - - // 3. We only reach this point if it's mainland AND not a crater! - float h = _heightMap[x, y]; - float localSea = GetSeaLevel(_tempMap[x, y]); - float normalizedElevation = Mathf.Clamp((h - localSea) / (1.0f - localSea), 0.0f, 1.0f); + // 2. Existing water/mainland checks + if (b == Biome.Ocean || b == Biome.Lake || !_isMainland[x, y]) { + astar.SetPointSolid(new Vector2I(x, y), true); + continue; // "Skip the rest, go to next pixel" + } + + // 3. We only reach this point if it's mainland AND not a crater! + float h = _heightMap[x, y]; + float localSea = GetSeaLevel(_tempMap[x, y]); + float normalizedElevation = Mathf.Clamp((h - localSea) / (1.0f - localSea), 0.0f, 1.0f); - // Simple, steep curve to avoid mountains - float weight = 1.0f + Mathf.Pow(normalizedElevation, 3.0f) * 400.0f; + // Simple, steep curve to avoid mountains + float weight = 1.0f + Mathf.Pow(normalizedElevation, 3.0f) * 400.0f; - // Push off the beach (not an insane wall, just enough to prefer grass) - if (b == Biome.Beach) weight += 15.0f; - - // Let it path through the wasteland normally - if (b == Biome.Wasteland) weight += 3.0f; + // Push off the beach (not an insane wall, just enough to prefer grass) + if (b == Biome.Beach) weight += 15.0f; + + // Let it path through the wasteland normally + if (b == Biome.Wasteland) weight += 3.0f; - astar.SetPointWeightScale(new Vector2I(x, y), weight); - } - } - } + astar.SetPointWeightScale(new Vector2I(x, y), weight); + } + } + } private void PenalizePath(AStarGrid2D astar, Vector2[] path, int radius) { @@ -1587,7 +1413,7 @@ public partial class MapGenerator : TextureRect int ny = Mathf.Clamp((int)p.Y + dy, 0, MapSize - 1); var cell = new Vector2I(nx, ny); if (!astar.IsPointSolid(cell)) { - // The true Iron Curtain + // The true Iron Curtain astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + 10000f); } } @@ -1662,67 +1488,67 @@ public partial class MapGenerator : TextureRect { float scaleMod = MapSize / 1024f; // equals 4 at 4096 - // Trails: Thin brown - foreach (var path in _trailPaths) DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true); - - // Rugged: Thicker dark brown - foreach (var path in _ruggedPaths) DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true); + // Trails: Thin brown + foreach (var path in _trailPaths) DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true); + + // Rugged: Thicker dark brown + foreach (var path in _ruggedPaths) DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true); - // Main Roads: Solid Black - foreach (var path in _branchPaths) DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true); + // Main Roads: Solid Black + foreach (var path in _branchPaths) DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true); - // Highway: Thick Red - foreach (var path in _highwayPaths) DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true); + // Highway: Thick Red + foreach (var path in _highwayPaths) DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true); - // Draw Towns - foreach (var town in _towns) { - float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f); - radius *= scaleMod; // Scale the circles up! - - Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange); - if (town.Tier == TownTier.IslandLoot) c = Colors.Red; - if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; } + // Draw Towns + foreach (var town in _towns) { + float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f); + radius *= scaleMod; // Scale the circles up! + + Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange); + if (town.Tier == TownTier.IslandLoot) c = Colors.Red; + if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; } - DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black); - DrawCircle(town.Position, radius, c); - } + DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black); + DrawCircle(town.Position, radius, c); + } } } // Place this at the very bottom of the file! public partial class MapDrawProxy : Control { - public MapGenerator Source; + public MapGenerator Source; - public override void _Draw() - { - if (Source == null) return; - - float scaleMod = Source.MapSize / 1024f; + public override void _Draw() + { + if (Source == null) return; + + float scaleMod = Source.MapSize / 1024f; - foreach (var path in Source._trailPaths) - DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true); + foreach (var path in Source._trailPaths) + DrawPolyline(path, new Color(0.5f, 0.4f, 0.3f, 0.7f), 1.0f * scaleMod, true); - foreach (var path in Source._ruggedPaths) - DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true); + foreach (var path in Source._ruggedPaths) + DrawPolyline(path, new Color(0.35f, 0.25f, 0.15f), 1.5f * scaleMod, true); - foreach (var path in Source._branchPaths) - DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true); + foreach (var path in Source._branchPaths) + DrawPolyline(path, Colors.Black, 1.0f * scaleMod, true); - foreach (var path in Source._highwayPaths) - DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true); + foreach (var path in Source._highwayPaths) + DrawPolyline(path, new Color(0.9f, 0.1f, 0.1f), 2.0f * scaleMod, true); - foreach (var town in Source._towns) - { - float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f); - radius *= scaleMod; + foreach (var town in Source._towns) + { + float radius = (town.Tier == TownTier.Capitol) ? 12f : (town.Tier == TownTier.Hub ? 8f : 4f); + radius *= scaleMod; - Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange); - if (town.Tier == TownTier.IslandLoot) c = Colors.Red; - if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; } + Color c = town.IsHighwayNode ? Colors.Yellow : (town.Tier == TownTier.Outpost ? Colors.Cyan : Colors.Orange); + if (town.Tier == TownTier.IslandLoot) c = Colors.Red; + if (town.Tier == TownTier.MiniPOI) { radius = 2.5f * scaleMod; c = Colors.SaddleBrown; } - DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black); - DrawCircle(town.Position, radius, c); - } - } -} + DrawCircle(town.Position, radius + (1.5f * scaleMod), Colors.Black); + DrawCircle(town.Position, radius, c); + } + } +} \ No newline at end of file diff --git a/Tools/Scripts/README.md b/Tools/Scripts/README.md index 7b5b18a..0fb6401 100644 --- a/Tools/Scripts/README.md +++ b/Tools/Scripts/README.md @@ -11,18 +11,12 @@ Generates the entire 2D blueprint. Roughly in order: 2. **Topography** — FastNoiseLite base height plus a mountain spine, minus a squircle distance falloff, giving a guaranteed island. Noise frequency is divided by `scaleFactor` (`MapSize / 1024f`) so terrain features stay the same real-world size at any map profile. - When `TerrainCurve: "v5"` (the default — the task-09 gate's BALANCED winner), the calibrated - height-redistribution curve - (`HeightCurve.cs` — the terraced ascent with spatially modulated shelves: flat farmable - lowlands, a walkable foothill bench at 100±12 m, the white mountain-town plateau at - 220±20 m — shelf heights and strength drift across the island so no two flanks wear the same - ring — and a per-seed-normalized summit spike to the 420 m cap) reshapes above-sea terrain - after noise/falloff/Trench and before the crater carve; biome classification reads a - retained uncurved map, so biomes are identical either way. With `TerrainDetail: "v1"` (the - default) two detail passes follow the curve: shelf micro-relief (±3 m rolling skin on the - benches/plateaus) and D8 drainage incision (rain-cut ravines in the risers — the future - river routes; `TerrainDetailPass.cs`). Drops the `0_height` hillshade snapshot (hypsometric - bands × NW hillshade) in both modes. + When `TerrainCurve: "v2"` (the default), the calibrated height-redistribution curve + (`HeightCurve.cs` — flat farmable lowlands, 50 m plateau shelf, and a per-seed-normalized + spike to the 420 m peak cap: every island's tallest point reaches the ceiling) reshapes + above-sea terrain after noise/falloff/Trench and before the crater carve; biome + classification reads a retained uncurved map, so biomes are identical either way. Drops the + `0_height` hillshade snapshot (hypsometric bands × NW hillshade) in both modes. 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 separates true ocean from inland lakes; a mainland fill guarantees one contiguous landmass. diff --git a/Tools/Scripts/RoundTripHarness.cs b/Tools/Scripts/RoundTripHarness.cs index 10128cb..7b2393a 100644 --- a/Tools/Scripts/RoundTripHarness.cs +++ b/Tools/Scripts/RoundTripHarness.cs @@ -121,7 +121,6 @@ public partial class RoundTripHarness : Node ok &= CompareRoads("Trail", a.TrailRoads, b.TrailRoads); ok &= CompareWater(a, b); ok &= CompareTerrainCurve(a, b); - ok &= CompareTerrainDetail(a, b); if (ok) GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " + @@ -201,14 +200,8 @@ public partial class RoundTripHarness : Node } var ca = a.TerrainCurve; var cb = b.TerrainCurve; bool same = ca.Version == cb.Version; - float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope, - ca.BenchAmp, ca.PlateauAmp, ca.ShelfSpanMin, ca.ShelfSpanMax, ca.ElevFreqIslands, ca.StrengthFreqIslands, - ca.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset, - ca.PresetId, ca.K5, ca.K6 }; - float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope, - cb.BenchAmp, cb.PlateauAmp, cb.ShelfSpanMin, cb.ShelfSpanMax, cb.ElevFreqIslands, cb.StrengthFreqIslands, - cb.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset, - cb.PresetId, cb.K5, cb.K6 }; + float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope }; + float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope }; for (int i = 0; i < fa.Length; i++) if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false; if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; } @@ -216,31 +209,6 @@ public partial class RoundTripHarness : Node return true; } - private bool CompareTerrainDetail(WorldBlueprint a, WorldBlueprint b) - { - if (a.TerrainDetail == null && b.TerrainDetail == null) - { - GD.Print("[Harness] TDTL: absent in source — nothing to compare (and none reappeared)."); - return true; - } - if (a.TerrainDetail == null || b.TerrainDetail == null) - { - GD.PrintErr("[Harness] TDTL presence mismatch between source and reread."); - return false; - } - var da = a.TerrainDetail; var db = b.TerrainDetail; - float[] fa = { da.Version, da.ReliefAmpM, da.ReliefFreqIslands, da.IncK, da.IncP, da.IncCapM, da.SeaClampRaw, da.CraterExclFactor, da.ShelfIncWeight, da.ReliefSeedOffset }; - float[] fb = { db.Version, db.ReliefAmpM, db.ReliefFreqIslands, db.IncK, db.IncP, db.IncCapM, db.SeaClampRaw, db.CraterExclFactor, db.ShelfIncWeight, db.ReliefSeedOffset }; - for (int i = 0; i < fa.Length; i++) - if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) - { - GD.PrintErr("[Harness] TDTL fields differ."); - return false; - } - GD.Print($"[Harness] TDTL equal (detail v{da.Version})."); - return true; - } - private bool CompareRoads(string tier, List a, List b) { if (a.Count != b.Count) diff --git a/Tools/Scripts/TerrainDetailPass.cs b/Tools/Scripts/TerrainDetailPass.cs deleted file mode 100644 index b7cf84f..0000000 --- a/Tools/Scripts/TerrainDetailPass.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Godot; -using System; - -/// -/// The terrain DETAIL passes (terrain-water task 10) — pure numeric array machinery -/// (D-035; a named future C++ candidate, kept standalone): -/// -/// PASS A — shelf micro-relief: a medium-frequency noise skin (±ShelfReliefAmp, -/// default 3 m) weighted by shelf-ness, so the compressed shelves get their -/// rolling texture back while risers and peaks stay untouched. -/// -/// PASS B — drainage incision: D8 steepest-descent flow routing + accumulation -/// over the curved terrain; depth = K · accum^p · localSlope (capped), masked to -/// the risers (feathered ~30 % onto shelves, zero on the toe and above the -/// plateau top, zero near the crater), clamped so carved terrain never drops -/// below sea + 1 m. The channels double as the future river routes (Phase C). -/// -/// Ordering (enforced by the caller): curve → micro-relief → incision → crater -/// carve. The classify map never sees any of it. -/// -public static class TerrainDetailPass -{ - public const ushort VERSION = 1; - - // Pass A — micro-relief. - public const float RELIEF_AMP_DEFAULT_M = 3f; // config dial: ShelfReliefAmp (metres) - public const float RELIEF_FREQ_ISLANDS = 40f; // ~40 undulations per island width (~200 m features) - public const int RELIEF_SEED_OFFSET = 7409; - - // Pass B — incision. K/p tuned against the depth targets (gullies 8–15 m, - // trunks ~25 m, cap 30 m); the tuning run's achieved distribution is in the - // task-10 report. - public const float INC_K = 1.40f; - public const float INC_P = 0.45f; // concave: many fingers, few deep trunks - public const float INC_CAP_M = 30f; // IncisionMax - public const float SEA_CLAMP = 0.15f + 1f / 251f; // carved height ≥ sea + 1 m - public const float SHELF_INC_WEIGHT = 0.3f; // shelves get washes, not gorges - public const float CRATER_EXCL_FACTOR = 1.2f; // zero incision inside this × CraterRadius - public const float CRATER_FEATHER_FACTOR = 1.4f; // ...feathering to full by this × CraterRadius - - /// - /// Shelf-ness weight from the RAW input height: 1 mid-shelf, feathering to 0 - /// through the risers (feather extends 30 % of the band half-width past each - /// shelf edge). Covers both shelves. - /// - public static float ShelfWeight(float raw, CurveKnots k) - { - return Mathf.Max(BandBump(raw, k.K3, k.K4), BandBump(raw, k.K5, k.K6)); - } - - private static float BandBump(float h, float lo, float hi) - { - float half = (hi - lo) * 0.5f; - float t = Mathf.Abs(h - (lo + half)) / half; // 0 centre, 1 at band edge - // full inside 60 % of the band, linear feather to zero at 130 % - return Mathf.Clamp(1f - (t - 0.6f) / 0.7f, 0f, 1f); - } - - /// - /// Incision mask from the RAW input height: 0 below the red-ceiling input (K2) - /// and above the plateau top (K6); 1 on the riser bands; SHELF_INC_WEIGHT on the - /// shelf bands; smooth feathers (15 % of the local band width) at every boundary. - /// - public static float IncisionWeight(float raw, CurveKnots k) - { - if (raw <= k.K2 || raw >= k.K6) return 0f; - if (raw < k.K3) // foothill riser: feather in from K2, feather toward shelf weight at K3 - return EdgeBlend(raw, k.K2, k.K3, 0f, 1f, SHELF_INC_WEIGHT); - if (raw < k.K4) // bench - return SHELF_INC_WEIGHT; - if (raw < k.K5) // mid riser - return EdgeBlend(raw, k.K4, k.K5, SHELF_INC_WEIGHT, 1f, SHELF_INC_WEIGHT); - // plateau band: shelf weight, feathering to zero at K6 - float w = (k.K6 - raw) / ((k.K6 - k.K5) * 0.15f); - return Mathf.Min(SHELF_INC_WEIGHT, Mathf.Clamp(w, 0f, 1f) * SHELF_INC_WEIGHT); - } - - private static float EdgeBlend(float h, float lo, float hi, float wIn, float wMid, float wOut) - { - float f = (hi - lo) * 0.15f; - if (h < lo + f) return Mathf.Lerp(wIn, wMid, (h - lo) / f); - if (h > hi - f) return Mathf.Lerp(wMid, wOut, (h - (hi - f)) / f); - return wMid; - } - - /// - /// D8 flow accumulation over a height field (row-major idx = x·n + y). - /// Steepest-descent routing (drop / distance, diagonals ÷√2), deterministic - /// tie-break (fixed neighbour order, first winner). Cells with no lower - /// neighbour are pits/outlets (no outflow). accum = upslope contributing cells - /// including self; steepestDrop = drop per pixel toward the chosen neighbour. - /// - public static int[] FlowAccumulation(float[] h, int n, out float[] steepestDrop) - { - int total = n * n; - int[] downstream = new int[total]; - steepestDrop = new float[total]; - int[] dx = { 1, -1, 0, 0, 1, 1, -1, -1 }; - int[] dy = { 0, 0, 1, -1, 1, -1, 1, -1 }; - float[] invDist = { 1f, 1f, 1f, 1f, 0.7071068f, 0.7071068f, 0.7071068f, 0.7071068f }; - - for (int x = 0; x < n; x++) - { - for (int y = 0; y < n; y++) - { - int i = x * n + y; - float hc = h[i]; - float best = 0f; - int bestIdx = -1; - for (int d = 0; d < 8; d++) - { - int nx = x + dx[d], ny = y + dy[d]; - if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue; - int ni = nx * n + ny; - float grade = (hc - h[ni]) * invDist[d]; - if (grade > best) - { - best = grade; - bestIdx = ni; - } - } - downstream[i] = bestIdx; - steepestDrop[i] = best; - } - } - - // Height-descending order: each cell pushes its accumulated count downstream. - float[] keys = (float[])h.Clone(); - int[] order = new int[total]; - for (int i = 0; i < total; i++) order[i] = i; - Array.Sort(keys, order); // ascending - - int[] accum = new int[total]; - for (int i = 0; i < total; i++) accum[i] = 1; - for (int i = total - 1; i >= 0; i--) - { - int c = order[i]; - int d = downstream[c]; - if (d >= 0) accum[d] += accum[c]; - } - return accum; - } -}