using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05, retuned chat2/06). Runs over
/// the finished pass-1 arrays, IN PLACE, before HMaxSeed is taken and before anything
/// classifies.
///
/// ═══ WHERE THIS SITS, AND WHY IT IS A SECOND SWEEP ═══
///
/// The reference did all of this INSIDE the pass-1 pixel loop (MapGenerator.cs:621-664):
/// combine → shelf → islets → _hMaxSeed → write. v2 runs the same arithmetic as a second
/// sweep over the arrays the first loop produced. Per pixel the inputs are identical — the raw
/// height, the sea level, the pre-Trench falloff, (x, y) — and the operations are applied in the
/// same order on the same floats, so the FAITHFUL mode reproduces the reference cell for cell.
/// The second sweep is also what lets the slop guards see whole islands: a component rule needs
/// the finished field.
///
/// ⚠⚠ THE ORDERING THAT CLOSES chat2/00 DRIFT §2: the caller recomputes HMaxSeed AFTER this
/// pass, as the reference did, so the curve's per-seed peak normalization sees the same maximum
/// the reference saw. Expected to be unchanged (a ~34 m crest is far below any peak) — reported,
/// not assumed.
///
/// ═══ THE SUB-PASSES ═══
///
/// 1. SHELF every below-sea cell; depth-preserving; held strictly below sea by BitDecrement.
/// 2. ORGANIC ⭐ THE ONE ISLAND MECHANISM — the reference's noise layer, faithful or reshaped
/// (smaller / lower / flatter / crisper, density + south weight), every below-sea
/// cell inside the zone mask. Nothing places an island from a centre; nothing
/// guarantees a count. (chat2/05's seeded floor was reverted out in chat2/06 — it
/// looked stamped. It lives in git history.)
/// 3. GUARDS (Organic only) specks, clusters and blobs reverted BY COMPONENT, then the
/// reference's submerged humps outside any surviving island's skirt.
///
/// ═══ THE TWO PROTECTIONS ═══
///
/// The moat (min depth) and the "actually offshore" test (pre-Trench falloff) gate the organic
/// layer per pixel — that is the reference. Nothing in this pass can raise a cell outside the
/// zone, so no island can bridge to shore or appear in a lake / the crater bay.
///
/// ═══ ⚠ THE ONE HONEST COUPLING ═══
///
/// Unlike the curve, this pass TURNS WATER INTO LAND. New land is new classification downstream
/// (biome/water pixels that did not exist before). That is precisely why it runs here, in the base
/// shape, before anything classifies: change an island dial, regenerate, and classification
/// re-runs consistently. Known property, stated in code, not a surprise.
///
/// Every cell lifted from below sea to at/above sea is TAGGED (Result.Tag) with its
/// hemisphere (Result.Hemi). That tag is data the shape pass sets and carries; nothing in
/// this phase reads it. → for the hemisphere convention.
///
public static class OffshorePass
{
public sealed class Result
{
public bool[,] Tag; // null when the islet layer is off (shelf only)
public byte[,] Hemi;
public long ShelfCells, LiftedOrganic;
public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN;
public List Notes = new();
public int CountNorth, CountSouth, Bridged;
// ---- the guards' ledger (Organic only) ----
public int PreGuardNorth, PreGuardSouth; // islands before any guard
public int SpecksNorth, SpecksSouth; // reverted as specks
public int ClustersNorth, ClustersSouth; // reverted as too-close-to-a-larger-island
public int BlobsNorth, BlobsSouth; // reverted as oversize
public long LiftedReverted; // surfaced cells the guards put back
public long RaisedReverted; // submerged bump cells the guard put back
internal List<(int x, int y, float h0)> LiftedOrigin = new(); // every SURFACED lift
internal List<(int x, int y, float h0)> RaisedOrigin = new(); // every organic raise, surfaced or not (guards on)
public OffshoreLedger ToLedger() => Tag == null ? null : new OffshoreLedger
{
ThresholdNorth = ThresholdNorth, ThresholdSouth = ThresholdSouth,
PreGuardNorth = PreGuardNorth, PreGuardSouth = PreGuardSouth,
SpecksNorth = SpecksNorth, SpecksSouth = SpecksSouth,
ClustersNorth = ClustersNorth, ClustersSouth = ClustersSouth,
BlobsNorth = BlobsNorth, BlobsSouth = BlobsSouth,
LiftedOrganic = LiftedOrganic, LiftedReverted = LiftedReverted, RaisedReverted = RaisedReverted,
CountNorth = CountNorth, CountSouth = CountSouth,
};
}
///
/// Apply the shelf and/or islets to IN PLACE. Returns null when
/// both are off (nothing touched, nothing allocated).
///
public static Result Apply(float[,] height, float[,] preTrench, int mapSize, int seed,
float sea, TerrainGenConfig cfg)
{
OffshoreSettings s = cfg.Offshore;
bool shelfOn = cfg.CoastShelf;
bool isletsOn = s != null && s.Mode != OffshoreMode.Off;
if (!shelfOn && !isletsOn) return null;
var r = new Result();
GenerationScale scale = cfg.Scale;
// ⚠ MAP-anchored, exactly as the reference: `center = MapSize / 2.0f`, `distX = |x − cx| / (MapSize / 2.0f)`.
float centerX = mapSize / 2.0f, centerY = mapSize / 2.0f;
float halfSpan = mapSize / 2.0f;
// ═══ 1. THE COAST SHELF ═══
if (shelfOn)
{
// ⭐ THE CLAMP THAT MAKES "CANNOT MOVE THE WATERLINE" EXACT. Reference: the remap is
// strictly positive on positive depth, so in exact arithmetic the waterline cannot
// move; in float32 it can — a pixel a few microns under water rounds back up to
// exactly sea and `h < sea` then calls it land. That cost 5 px of 67 M on the
// reference's first batch. Hold the result strictly below sea and the invariant is
// exact. Do not port this without the clamp.
float strictlyBelowSea = MathF.BitDecrement(sea);
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
float h = height[x, y];
if (h >= sea) continue; // below-sea ONLY
float depthM = WorldScale.MetresFromRaw(sea - h); // (seaHere − finalH) * 251f
height[x, y] = MathF.Min(
sea - WorldScale.RawFromMetres(IslandFalloff.CoastShelf(depthM, cfg.ShelfStrength, cfg.ShelfScaleM)),
strictlyBelowSea);
r.ShelfCells++;
}
}
r.Notes.Add($"[Offshore] coast shelf: {r.ShelfCells:N0} below-sea cells remapped " +
$"(strength {cfg.ShelfStrength:F3}, scale {cfg.ShelfScaleM:F0} m) — held strictly below sea.");
}
if (!isletsOn) return r;
r.Tag = new bool[mapSize, mapSize];
r.Hemi = new byte[mapSize, mapSize];
bool faithful = s.Mode == OffshoreMode.Faithful;
bool guardsOn = !faithful && (s.MinIslandAreaFrac > 0f || s.MinSeparationFrac > 0f || s.MaxIslandAreaFrac > 0f);
float crest = sea + WorldScale.RawFromMetres(s.CrestM); // seaHere + OFFSHORE_ISLAND_H_M / 251f
// ═══ 2. THE ORGANIC LAYER — the one island mechanism ═══
{
FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale);
// ⭐ Calibrate against the field's ACTUAL distribution, not the theoretical [−1,1]:
// sample on a stride grid and take the quantile. Deterministic from the seed, and it
// makes the density dial mean what it says whatever FastNoiseLite's range turns out
// to be. Verbatim: stride 8, (side)² samples, (noise + 1) * 0.5.
float[] samples = CalibrationSamples(noise, mapSize);
float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density);
float thrS = faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth);
r.ThresholdNorth = thrN; r.ThresholdSouth = thrS;
float mid = mapSize * 0.5f;
float band = MathF.Max(1f, s.HemisphereBlendHalfWidth * mapSize);
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
float h = height[x, y];
if (h >= sea) continue; // below-sea ONLY — never touches land
float ambientDepthM = WorldScale.MetresFromRaw(sea - h);
float zone = IslandFalloff.OffshoreZoneWeight(
ambientDepthM, preTrench[x, y],
MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan,
s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather,
s.TrenchInner, s.TrenchOuter);
if (zone <= 0f) continue;
float v = (noise.GetNoise2D(x, y) + 1.0f) * 0.5f;
// The threshold: one number in faithful mode; north/south blended smoothly
// across the midline in the reshape, so a straddling island is not sliced.
float thr = faithful ? thrN : BlendedThreshold(y, mid, band, thrN, thrS);
float blob = (faithful
? IslandFalloff.OffshoreBlob(v, thr)
: IslandFalloff.RigidBlob(v, thr, s.CoreFraction, s.EdgeSharpness)) * zone;
if (blob <= 0f) continue;
// ⭐ LERP TOWARD THE CREST, never add — surfaces at any ambient depth.
float before = h;
h = h + (crest - h) * blob; // Mathf.Lerp, written out
height[x, y] = h;
if (guardsOn) r.RaisedOrigin.Add((x, y, before)); // for the submerged-bump guard
if (before < sea && h >= sea)
{
r.LiftedOrganic++;
r.Tag[x, y] = true;
r.Hemi[x, y] = OffshoreAnalysis.HemisphereOfRow(y, mapSize);
r.LiftedOrigin.Add((x, y, before));
}
}
}
r.Notes.Add($"[Offshore] organic ({(faithful ? "faithful" : "reshaped")}): threshold N {thrN:F4}" +
$"{(faithful ? "" : $" S {thrS:F4}")} from {samples.Length:N0} samples " +
$"(range {Min(samples):F3}..{Max(samples):F3}); lifted {r.LiftedOrganic:N0} cells above sea.");
}
// ═══ 3. THE SLOP GUARDS — reshape only; over everything the pass raised ═══
//
// Four kinds of slop, three component rules and one location rule:
// (a) SPECKS a local maximum that barely clears the threshold surfaces a cap of a few
// cells — not an island. Reverted by MEMBERSHIP (a speck inside a real
// island's skirt is still a speck).
// (b) BLOBS a superlevel region that merged several maxima into one sprawling
// landmass. Reverted by membership. The cap sits well above the natural
// size so it is a net, not a sculptor; how often it bites is reported.
// (c) CLUSTERS two islands whose shores are closer than the minimum separation read as
// one; the SMALLER goes (greedy by size, so the largest of a cluster stays).
// (d) SUBMERGED BUMPS — every blob with any weight lerps the seabed toward the crest
// whether or not it surfaces, so the ocean fills with shallow humps (the
// reference's character; the water pass would draw a reef field nobody
// asked for). Attributed by HUMP: a hump (connected raised region) that
// holds a kept island is that island's own skirt and stays; one that holds
// none goes back to seabed; a dropped island's own cap goes too.
// None of these places, shapes or counts anything. (The faithful control keeps all four;
// that is the reference.)
if (guardsOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0))
{
double area = (double)mapSize * mapSize;
long minCells = s.MinIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * area)) : 0;
long maxCells = s.MaxIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MaxIslandAreaFrac * area)) : long.MaxValue;
int minSep = s.MinSeparationFrac > 0f ? Math.Max(1, (int)Math.Round(s.MinSeparationFrac * mapSize)) : 0;
var pre = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize, out int[] compId);
(r.PreGuardNorth, r.PreGuardSouth) = OffshoreAnalysis.CountByHemisphere(pre);
var dropped = new HashSet();
var byId = new Dictionary();
foreach (var c in pre) byId[c.Id] = c;
// (a) specks and (b) blobs — by size.
foreach (var c in pre)
{
if (c.Cells < minCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.SpecksNorth, ref r.SpecksSouth); }
else if (c.Cells > maxCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.BlobsNorth, ref r.BlobsSouth); }
}
// (c) clusters — greedy by size among the survivors of (a)/(b).
if (minSep > 0)
{
var survivors = new List();
foreach (var c in pre) if (!dropped.Contains(c.Id)) survivors.Add(c);
survivors.Sort((a, b) => b.Cells.CompareTo(a.Cells)); // largest first
var boundary = OffshoreAnalysis.BoundaryCells(r.Tag, compId, mapSize, r.LiftedOrigin);
var kept = new List();
foreach (var c in survivors)
{
bool tooClose = false;
foreach (var k in kept)
{
if (OffshoreAnalysis.BoxGap(c, k) >= minSep) continue; // cannot be closer than the bbox gap
if (OffshoreAnalysis.MinChebyshev(boundary[c.Id], boundary[k.Id], minSep) < minSep) { tooClose = true; break; }
}
if (tooClose) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.ClustersNorth, ref r.ClustersSouth); }
else kept.Add(c);
}
}
// (d) THE SUBMERGED BUMPS — by HUMP, not by box. A hump is one 8-connected region of
// raised cells (the blob's footprint, surfaced or not). A hump that holds a kept island
// is that island's own skirt and stays whole; a hump that holds none is a reef nobody
// asked for and goes back to seabed whole. A DROPPED island's own cap — its bbox plus a
// margin — is reverted even inside a kept hump, or its rim would stay as a hollow ring
// beside its neighbour (the first probe plate showed exactly those ghost outlines).
var raised = new bool[mapSize, mapSize];
foreach (var (x, y, _) in r.RaisedOrigin) raised[x, y] = true;
var humpId = new int[mapSize * mapSize];
var humpKept = new List { false }; // index 0 unused
{
var stack = new Stack();
foreach (var (sx, sy, _) in r.RaisedOrigin)
{
if (humpId[sx * mapSize + sy] != 0) continue;
int id = humpKept.Count; humpKept.Add(false);
humpId[sx * mapSize + sy] = id; stack.Push(sx * mapSize + sy);
bool kept = false;
while (stack.Count > 0)
{
int cur = stack.Pop(); int cx = cur / mapSize, cy = cur % mapSize;
if (r.Tag[cx, cy] && !dropped.Contains(compId[cur])) kept = true;
for (int dx = -1; dx <= 1; dx++)
{
int nx = cx + dx; if (nx < 0 || nx >= mapSize) continue;
for (int dy = -1; dy <= 1; dy++)
{
int ny = cy + dy; if (ny < 0 || ny >= mapSize || (dx == 0 && dy == 0)) continue;
if (!raised[nx, ny]) continue;
int ni = nx * mapSize + ny;
if (humpId[ni] != 0) continue;
humpId[ni] = id; stack.Push(ni);
}
}
}
humpKept[id] = kept;
}
}
var dropBoxes = new List<(int x0, int y0, int x1, int y1)>();
foreach (var c in pre)
{
if (!dropped.Contains(c.Id)) continue;
int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1;
int margin = Math.Max(4, Math.Max(w, hgt) / 2);
dropBoxes.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin));
}
bool InDropBox(int x, int y)
{
foreach (var (x0, y0, x1, y1) in dropBoxes)
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true;
return false;
}
long revertedLifted = 0, revertedRaised = 0;
foreach (var (x, y, h0) in r.LiftedOrigin)
{
if (!dropped.Contains(compId[x * mapSize + y])) continue;
height[x, y] = h0; r.Tag[x, y] = false; r.Hemi[x, y] = OffshoreAnalysis.HemiNone;
revertedLifted++;
}
foreach (var (x, y, h0) in r.RaisedOrigin)
{
if (r.Tag[x, y]) continue; // a kept island's own surfaced cell
bool keepIt = humpKept[humpId[x * mapSize + y]] && !InDropBox(x, y);
if (keepIt) continue;
if (height[x, y] > h0) { height[x, y] = h0; revertedRaised++; }
}
r.LiftedReverted = revertedLifted;
r.RaisedReverted = revertedRaised;
r.Notes.Add($"[Offshore] guards: of {pre.Count} islands (N {r.PreGuardNorth} / S {r.PreGuardSouth}) reverted " +
$"{r.SpecksNorth + r.SpecksSouth} specks (< {minCells:N0} cells), " +
$"{r.ClustersNorth + r.ClustersSouth} clustered (< {minSep} px from a larger island), " +
$"{r.BlobsNorth + r.BlobsSouth} blobs (> {(maxCells == long.MaxValue ? "∞" : maxCells.ToString("N0"))} cells) " +
$"— {revertedLifted:N0} surfaced cells, plus {revertedRaised:N0} submerged bump cells outside any kept island's hump.");
}
// ═══ PROVE THE MOAT ON THE FINISHED FIELD — in the pass, before anyone looks ═══
var comps = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize);
(r.CountNorth, r.CountSouth) = OffshoreAnalysis.CountByHemisphere(comps);
r.Bridged = OffshoreAnalysis.BridgedCount(comps);
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
r.Notes.Add($"[Offshore] islands: {r.CountNorth} north, {r.CountSouth} south ({comps.Count} components, " +
$"{r.Bridged} bridged to mainland); lifted {r.LiftedOrganic - r.LiftedReverted:N0} cells net" +
$"{(r.RaisedReverted > 0 ? $", {r.RaisedReverted:N0} submerged debris cells reverted" : "")}; " +
$"size cells min {szMin} median {szMed} mean {szMean:F0} max {szMax}.");
if (r.Bridged > 0)
throw new InvalidOperationException(
$"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing.");
return r;
}
/// The reference's calibration sample: stride 8, (side)² samples, (noise + 1) · 0.5. Shared with the diagnosis.
public static float[] CalibrationSamples(FastNoiseLite noise, int mapSize)
{
const int stride = 8;
int side = mapSize / stride;
var samples = new float[side * side];
for (int i = 0; i < side; i++)
for (int j = 0; j < side; j++)
samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f;
return samples;
}
/// The north/south threshold, smoothstep-blended across the midline. Shared with the diagnosis.
public static float BlendedThreshold(int y, float mid, float band, float thrN, float thrS)
{
float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f);
t = t * t * (3f - 2f * t);
return thrN + (thrS - thrN) * t;
}
private static void Bump(Result r, byte hemi, ref int north, ref int south)
{
if (hemi == OffshoreAnalysis.HemiNorth) north++; else south++;
}
private static float Min(float[] a) { float m = float.MaxValue; foreach (float v in a) if (v < m) m = v; return m; }
private static float Max(float[] a) { float m = float.MinValue; foreach (float v in a) if (v > m) m = v; return m; }
}
///
/// The islet layer's ledger, carried on Pass1Result for the report: thresholds, the
/// pre-guard island count, what each guard reverted, the final count. Numbers only — the tag
/// arrays are carried separately.
///
public sealed class OffshoreLedger
{
public float ThresholdNorth, ThresholdSouth;
public int PreGuardNorth, PreGuardSouth;
public int SpecksNorth, SpecksSouth, ClustersNorth, ClustersSouth, BlobsNorth, BlobsSouth;
public long LiftedOrganic, LiftedReverted, RaisedReverted;
public int CountNorth, CountSouth;
}
}