using System; namespace IslaApocalypse.Core { /// /// The terrain DETAIL passes — the two things that make a redistributed shelf read as ground /// rather than as a terrace. Ported from REFERENCE:Tools/Scripts/TerrainDetailPass.cs /// (v1) at tag pre-rewrite-reference (ab78883). /// /// ═══ TWO PASSES, AND THEY DO DIFFERENT KINDS OF THING ═══ /// /// PASS A — SHELF MICRO-RELIEF. A medium-frequency noise skin (±3 m by default) added to the /// OUTPUT height, weighted by shelf-ness. The curve compresses the shelves flat; this gives /// them their rolling texture back. Risers and peaks are untouched by construction. /// /// PASS B — SHELF-EDGE VARIATION. A per-column shift of the shelf/riser KNOT BLOCK (K3/K4/K5) /// by a low-frequency field. /// /// > ### ⚠⚠ PASS B IS NOT HEIGHT PERTURBATION, AND THE DIFFERENCE IS THE WHOLE POINT. /// > /// > Pass A moves a column's OUTPUT HEIGHT. Pass B moves WHERE THE BANDS ARE for that column. /// > /// > Every shelf↔riser boundary is the contour where the raw height crosses one of K3/K4/K5. Slide /// > those knots per column and the contour stops tracing a clean iso-height line: the shelf edge /// > scallops into coves, notches and peninsulas. Do it by perturbing height instead and you get /// > a fuzzy terrace edge, not an organic one — and you lose monotonicity as a STRUCTURAL /// > property, because the curve is monotonic for any ordered knot set but nothing is monotonic /// > after arbitrary additive noise. /// > /// > The amplitude is therefore stated in METRES OF INPUT HEIGHT (raw × the yardstick): how far a /// > boundary contour is displaced in raw-height terms, NOT an output elevation change. What the /// > eye sees is the LATERAL wander — that displacement divided by the local raw gradient. /// > Measured on the reference (seed 1375359975): |∇raw| at the K3/K4/K5 contours is p50 0.00088 /// > raw/px, so 12 m of input height buys a median peak displacement of ~54 px and a mean of ~8 px /// > along the boundary. 5 m — the first attempt — moved it a mean 3.7 px and was invisible at /// > map scale. /// /// ═══ ⚠ OUTPUT-HEIGHT ONLY. THE CLASSIFY FIELD NEVER SEES EITHER PASS. ═══ /// /// Both are consumers of the raw height, never producers of it. That is what keeps the classify /// map — and every biome and water body that will later be derived from it — invariant under /// every detail change. /// /// ═══ WHAT THIS FILE DELIBERATELY DOES NOT CONTAIN ═══ /// /// The reference's task-10 draft also carried D8 flow routing, accumulation and drainage /// INCISION. It shipped, produced the canonical grid artifact — thousands of straight, /// disconnected, pooling scratches along the D8 neighbour directions — and was reverted whole. /// → `Design - Terrain - D8 Incision Revert.md`. D8 returns later as ANALYSIS only. /// /// Engine-free (System.MathF), so it sits in Core/ beside : it /// reads and is meaningless apart from the curve it warps. /// public static class TerrainDetailPass { /// Detail body version — the identity of this pass's layout. public const ushort Version = 1; // ---- pass A: micro-relief ------------------------------------------- /// Micro-relief amplitude, metres of OUTPUT height. Reference default: 3 m. public const float ReliefAmpDefaultM = 3f; /// /// Micro-relief frequency, periods per MAP WIDTH. Reference: 40 — about 40 undulations /// across the island (~200 m features at 8K). ⚠ Scale-safe by construction: stated per map /// width, so the feature SIZE in metres holds at every map profile. /// public const float ReliefFreqPerMapWidth = 40f; /// Micro-relief field seed offset. Reference: 7409. ⚠ A SEED offset, not a coordinate offset. public const int ReliefSeedOffset = 7409; // ---- pass B: shelf-edge variation ----------------------------------- /// Edge-warp amplitude, metres of INPUT height. Reference default: 12 m. See the type header. public const float EdgeAmpDefaultM = 12f; /// /// Edge-warp frequency, periods per MAP WIDTH. Reference: 20 — ~410 px wavelength at 8K, /// coves and notches at the scale of the developer's sketch, not a fringe of teeth. /// public const float EdgeFreqPerMapWidth = 20f; /// Edge-warp field seed offset. Reference: 7507. ⚠ A SEED offset, not a coordinate offset. public const int EdgeSeedOffset = 7507; /// /// The warp bound, as a fraction of the smaller adjacent band. Reference: 2/3. /// /// ⚠ THE REAL CONSTRAINT IS BAND SQUEEZE; KNOT ORDERING FOLLOWS FROM IT. The shift compresses /// whichever of the foothill riser / plateau it moves into. Bounding it at 2/3 of the smaller /// band means that band never compresses below a THIRD of its nominal width — its slope never /// more than triples, even where peak noise lands exactly on a boundary. Ordering /// (K2 < K3±d, K5±d < K6) is then automatic, with a third of each band to spare. /// public const float EdgeSafetyFraction = 2f / 3f; // ---- the crater's precedence ---------------------------------------- // // ⚠ INERT UNTIL THE CRATER CARVE LANDS. No crater exists in this phase, so // CraterDetailWeight is called with a non-positive radius and returns 1 everywhere — detail // applies unmasked. The logic is ported now rather than later because it is part of THIS // pass's contract, and bolting it on after the carve arrives is how the reference's 532-px // bug happened in the first place. It is exercised when the carve lands. /// /// Detail exclusion radius, as a factor of CraterRadius. Reference: 0.80 — EXACTLY the /// carve's own extent (CRATER_CARVE_FACTOR), so the two agree by construction rather /// than by two constants that happen to match. /// public const float CraterDetailExclFactor = 0.80f; /// Detail feather-to-full radius, factor of CraterRadius. Reference: 1.05. public const float CraterDetailFeatherFactor = 1.05f; /// /// Detail weight from distance to the impact centre: 0 inside the carve, 1 well outside it, /// linear between. /// /// ═══ WHY IT EXISTS — measured, not precautionary ═══ /// /// Without it, detail moves a column's PRE-carve height, the carve's Lerp passes a fraction /// of that through, and columns sitting a metre or two above sea inside the bowl get pushed /// UNDER it: 532 px on reference seed 1158286446 in the first batch — terrain below the sea /// scalar that the (classify-driven, and correctly unchanged) water grid calls dry. The carve /// is the final authority on its own terrain; detail yields to it. /// /// /// The configured crater radius. ⚠ Non-positive means NO CRATER EXISTS — returns 1 /// (detail unmasked). That is this phase's state, and it is a defined case, not a fallthrough. /// public static float CraterDetailWeight(float distToCrater, float craterRadius) { if (craterRadius <= 0f) return 1f; // no crater in this phase — see the note above float excl = craterRadius * CraterDetailExclFactor; if (distToCrater <= excl) return 0f; float feather = craterRadius * CraterDetailFeatherFactor; if (distToCrater >= feather) return 1f; return (distToCrater - excl) / (feather - excl); } /// /// The largest per-column knot shift this knot set allows — see . /// K1/K2/K6 never move, so the toe, the orange/red bands and the summit spike are /// bit-identical whatever the warp does; that is what makes the red-ceiling floor and the /// peak cap exact rather than statistical. /// public static float MaxEdgeShift(CurveKnots k) => EdgeSafetyFraction * MathF.Min(k.K3 - k.K2, k.K6 - k.K5); /// /// Shelf-ness weight from the RAW input height: 1 mid-shelf, feathering to 0 through the /// risers. Covers both shelves (bench and plateau). /// /// ⚠ must be the SAME per-column warp the curve was evaluated /// with, so the micro-relief skin follows the shelf wherever pass B moved its boundary. /// Passing 0 here while the curve got a shift puts the skin on the wrong ground. /// /// ⚠ Note the asymmetry in the second call: K6 is NOT shifted, because K6 never moves. /// public static float ShelfWeight(float raw, CurveKnots k, float edgeShift) => MathF.Max(BandBump(raw, k.K3 + edgeShift, k.K4 + edgeShift), BandBump(raw, k.K5 + edgeShift, k.K6)); /// /// A trapezoid over one band: full inside the central 60 %, linear feather to zero at 130 % /// of the band half-width — so the skin dies out inside the risers rather than at the exact /// band edge, which would put a visible seam on the boundary the warp is busy hiding. /// private static float BandBump(float h, float lo, float hi) { float half = (hi - lo) * 0.5f; if (half <= 0f) return 0f; // degenerate band — no skin, no divide by zero float t = MathF.Abs(h - (lo + half)) / half; // 0 at centre, 1 at the band edge return Math.Clamp(1f - (t - 0.6f) / 0.7f, 0f, 1f); } } }