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);
}
}
}