using System; namespace IslaApocalypse.Tools { /// /// Shape helpers for the island mask, ported from the reference's /// Tools/Scripts/IslandFalloff.cs — now the WHOLE file, in three parts: /// /// 1. the spine crest () — Phase 1, chat1 /// 2. the submarine COAST SHELF () — chat2/05 stage 1 /// 3. the OFFSHORE ISLET layer (, /// , ) — chat2/05 stage 1 /// + the RESHAPE helper () — stage 2 /// /// The faithful functions keep the reference's names, constants and arithmetic verbatim (D-050); /// the parameterized overloads beside them exist so the reshape can move a dial without touching /// the faithful path — the faithful overload CALLS the parameterized one with the reference's /// constants, so the two cannot drift apart. /// /// The reference's own separability argument (verbatim): "Every one of these is monotone in the /// sign of (sea − height): none of them can turn water into land or land into water ON ITS OWN." /// That holds for the shelf, which is why it is invisible until water renders. ⚠ It does NOT hold /// for the islets, which exist precisely to turn water into land — see the note on /// . /// /// This type is pure math and engine-free. It sits in Tools/ rather than Core/ so the pass-1 /// port stays auditable as one unit against one reference file. /// public static class IslandFalloff { /// /// The rounding width of the spine crest, in normalized axis-distance units. /// READ from the reference: IslandFalloff.CREST_EPSILON = 0.03f (~:34). /// public const float CREST_EPSILON = 0.03f; /// /// A smooth absolute value: zero AT zero, with zero slope there, converging to |d| away from it. /// READ from the reference (~:42-46), verbatim: /// a = |d|; return a*a / sqrt(a*a + eps*eps); /// /// ═══ WHY IT EXISTS — do not "simplify" it back to Abs ═══ /// /// The spine's ridge axis is the line x = centre, and a plain 1 - |x - cx| peaks there /// with a SLOPE DISCONTINUITY. On real terrain that put the four columns at the centre axis /// in the top four slope-step locations out of 5,999 — a visible crease running the whole /// height of the island, measured at ~170x the off-axis controls. It was blamed on the /// falloff blend for a long time; transect measurement ruled that out and found this. /// /// SmoothAbs(0) = 0, so the crest keeps its FULL HEIGHT — it rounds without dropping. /// Measured on the reference: the 1-px kink fell 99.3%, the centre column dropped from /// rank 1 of 5,999 to rank 1,364, and nothing anywhere was lowered. /// → `Design - Terrain - Mountain Spine.md`. /// public static float SmoothAbs(float d, float epsilon) { float a = Math.Abs(d); return a * a / MathF.Sqrt(a * a + epsilon * epsilon); } // ═══════════════════════════════════════════════════════════════════════ // 2. THE COAST SHELF — chat2/05 stage 1, faithful (reference ~:48-62) // ═══════════════════════════════════════════════════════════════════════ // // depth' = depth · (1 − STRENGTH · exp(−depth / SCALE_M)) // // The height curve is identity at and below sea, so it never reached the seabed. Measured // on the reference: land rises from the shoreline at 0.038 m/px while the seabed drops at // 0.258 m/px — a shelf on the land side and a ramp on the sea side. This compresses shallow // depth so the shallows extend much further out, leaving deep water and the Trench alone. // At the shoreline the seabed starts at (1 − STRENGTH) = 22.5 % of its former gradient. // // C^∞ everywhere and STRICTLY POSITIVE for positive depth — in exact arithmetic it cannot // move the waterline by one pixel. ⚠ In float32 it can (see the call site's BitDecrement // clamp), which is why "cannot" is enforced at the call site and not assumed here. // // ⚠ INVISIBLE UNTIL WATER RENDERS. Nothing in Phase 2's hypsometric plates shows it; it is // ported faithfully now, wired in now, and judged when the water pass lands. /// Reference: 0 = off, →1 = a flat lagoon. public const float SHELF_STRENGTH = 0.775f; /// Reference: metres of depth over which the shelf relaxes back to the raw seabed. public const float SHELF_SCALE_M = 100f; /// Remaps a positive depth in metres. Returns the new depth in metres. THE FAITHFUL FORM. public static float CoastShelf(float depthMetres) => CoastShelf(depthMetres, SHELF_STRENGTH, SHELF_SCALE_M); /// The parameterized form. With the reference constants it IS the reference — same floats, same order. public static float CoastShelf(float depthMetres, float strength, float scaleM) { if (depthMetres <= 0f) return depthMetres; return depthMetres * (1f - strength * MathF.Exp(-depthMetres / scaleM)); } // ═══════════════════════════════════════════════════════════════════════ // 3. THE OFFSHORE ISLETS — chat2/05 stage 1, faithful (reference ~:64-142) // ═══════════════════════════════════════════════════════════════════════ // // Islets are placed by LERPING the seabed TOWARD a target height, not by adding to it, so // they surface at any ambient depth instead of only where the seafloor happens to be shallow. // // ⚠⚠ THIS IS THE ONE LAYER IN PASS 1 THAT TURNS WATER INTO LAND. Every other shaping element // is monotone in (sea − height). Islets add above-sea land, which means they CHANGE // CLASSIFICATION — new land is new biome/water pixels downstream. That is exactly why they // belong in the base shape before classification runs: tweaking an island dial later and // regenerating re-runs classification consistently. It is a known property, not a surprise. // → chat2/05 report, "modularity". /// Reference: ~585 px blobs at 8K — few and sizeable, not a scatter of 50 px debris. public const float OFFSHORE_FREQ_ISLANDS = 14f; /// Reference: the islet noise field's seed offset. A SEED offset, not a coordinate offset. public const int OFFSHORE_SEED_OFFSET = 7607; /// Reference: target crest, metres above sea, PRE-CURVE. The curve's toe squashes it lower. public const float OFFSHORE_ISLAND_H_M = 34f; /// Reference: fraction of a blob's excess over threshold that saturates to full weight. public const float OFFSHORE_CORE = 0.45f; /// /// ⭐ THE MOAT. Reference: the raise is EXACTLY zero wherever the ambient water is shallower /// than this, so the ring of water between the mainland shore and any islet cannot be /// bridged — a continuous path from shore to islet must cross this depth contour, and every /// pixel on it is untouched water. /// public const float OFFSHORE_MIN_DEPTH_M = 14f; /// Reference: the moat's feather width, metres. public const float OFFSHORE_DEPTH_FEATHER_M = 10f; /// Reference: the Trench mask — zone fades from INNER to zero at OUTER (the Trench ramp starts at 0.90). public const float OFFSHORE_TRENCH_INNER = 0.78f; public const float OFFSHORE_TRENCH_OUTER = 0.86f; /// /// ⭐ THE "ACTUALLY OFFSHORE" TEST. Reference: depth alone is not enough — a deep LAKE or the /// carved crater bay is also below sea level, and islets have no business in either. The /// pre-Trench falloff is the honest discriminator: the mainland coast sits near f = 0.66, and /// inland water is far below that whatever the axis ratios are, because elongation moves /// WHERE a given f occurs, not the f at which land ends. /// → this is precisely why Pass1Result.PreTrenchFalloff exists. /// public const float OFFSHORE_MIN_FALLOFF = 0.72f; /// Reference: the falloff test's feather width. public const float OFFSHORE_FALLOFF_FEATHER = 0.06f; /// /// Blob weight in [0,1] for one ocean column. THE FAITHFUL FORM. /// /// ⚠ comes from , NOT from the /// density directly. Reference: "Simplex output is concentrated well inside [−1,1] (in /// practice it rarely passes ±0.87), so treating density as a fraction of the theoretical /// range produces a threshold almost nothing clears. That bug shipped in the first task-11 /// build and raised 171 pixels on the whole map, none of them above sea." /// public static float OffshoreBlob(float noise01, float threshold) => OffshoreBlob(noise01, threshold, OFFSHORE_CORE); /// /// The parameterized form. is the fraction of the excess /// over threshold that saturates: SMALLER ⇒ more of the blob at full weight ⇒ FLATTER top /// and a sharper base. (The reshape's "flatter" lever lowers this, not raises it.) /// public static float OffshoreBlob(float noise01, float threshold, float coreFraction) { if (noise01 <= threshold) return 0f; float core = MathF.Max((1f - threshold) * coreFraction, 1e-4f); float k = Math.Clamp((noise01 - threshold) / core, 0f, 1f); return k * k * (3f - 2f * k); } /// /// The noise value that of exceed. /// Sorts a COPY, so the caller's array is left alone. Verbatim. /// public static float CalibrateThreshold(float[] samples, float density) { if (samples.Length == 0 || density <= 0f) return 1f; float[] s = (float[])samples.Clone(); Array.Sort(s); int idx = (int)((1f - Math.Clamp(density, 0f, 1f)) * (s.Length - 1)); return s[Math.Clamp(idx, 0, s.Length - 1)]; } /// /// How much of the blob is allowed here: zero in shallow water near the mainland (the moat), /// zero anywhere not genuinely outside the island body, zero in and near the Trench ramp, /// full in the open ocean between. THE FAITHFUL FORM. /// public static float OffshoreZoneWeight(float ambientDepthMetres, float preTrenchFalloff, float distX01, float distY01) => OffshoreZoneWeight(ambientDepthMetres, preTrenchFalloff, distX01, distY01, OFFSHORE_MIN_DEPTH_M, OFFSHORE_DEPTH_FEATHER_M, OFFSHORE_MIN_FALLOFF, OFFSHORE_FALLOFF_FEATHER, OFFSHORE_TRENCH_INNER, OFFSHORE_TRENCH_OUTER); /// /// The parameterized form. ⚠ / are /// MAP-anchored (|x − cx| / halfSpan, no axis ratio) — the same normalization the Trench /// itself uses, because the mask's job is to stay off the Trench, not off the island ellipse. /// public static float OffshoreZoneWeight(float ambientDepthMetres, float preTrenchFalloff, float distX01, float distY01, float minDepthM, float depthFeatherM, float minFalloff, float falloffFeather, float trenchInner, float trenchOuter) { if (ambientDepthMetres < minDepthM) return 0f; if (preTrenchFalloff < minFalloff) return 0f; float w = Math.Clamp((ambientDepthMetres - minDepthM) / depthFeatherM, 0f, 1f); w *= Math.Clamp((preTrenchFalloff - minFalloff) / falloffFeather, 0f, 1f); float d = MathF.Max(distX01, distY01); if (d >= trenchOuter) return 0f; if (d > trenchInner) w *= 1f - (d - trenchInner) / (trenchOuter - trenchInner); return w; } // ═══════════════════════════════════════════════════════════════════════ // 3b. THE RESHAPE HELPER — chat2/05 stage 2. Not in the reference. (The seeded-floor // stamp that sat beside it was reverted out in chat2/06 — git history has it.) // ═══════════════════════════════════════════════════════════════════════ /// /// The reshaped organic blob: the faithful smoothstep, then its weight pushed toward /// saturation by 1 − (1 − w)^sharpness. At /// sharpness 1 this IS . Higher values keep the same footprint but /// make the top flatter and the crest-to-sea transition narrower: a distinct flat-topped /// landmass instead of a gentle noise bump. C¹ at both ends, so it cannot alias. /// public static float RigidBlob(float noise01, float threshold, float coreFraction, float edgeSharpness) { float w = OffshoreBlob(noise01, threshold, coreFraction); if (w <= 0f || edgeSharpness <= 1f) return w; return 1f - MathF.Pow(1f - w, edgeSharpness); } } }