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