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). 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.
/// What the second sweep buys is the seeded floor: choosing island centres needs the whole
/// depth/falloff field to exist first, which a single pixel loop cannot provide.
///
/// ⚠⚠ 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 THREE SUB-PASSES ═══
///
/// 1. SHELF every below-sea cell; depth-preserving; held strictly below sea by BitDecrement.
/// 2. ORGANIC the reference's noise layer — faithful, or reshaped (smaller / lower / flatter /
/// crisper, south-weighted) — every below-sea cell inside the zone mask.
/// 3. FLOOR (Hybrid only) a seed-derived RNG places ≥N north / ≥S south stamps, each
/// min-separated from the others AND clear of ALL existing land by a gap, so each
/// stamp is its own connected component BY CONSTRUCTION. Positions vary per seed;
/// there are no reserved zones. Refuses loudly if the floor cannot be placed.
///
/// ═══ THE TWO PROTECTIONS ARE APPLIED TO EVERYTHING, INCLUDING THE FLOOR ═══
///
/// The moat (min depth) and the "actually offshore" test (pre-Trench falloff) gate the organic
/// layer per pixel — that is the reference. They ALSO gate every seeded stamp: a centre must sit
/// fully inside the zone (weight exactly 1), and the stamp's every pixel is still multiplied by
/// the zone weight. A guaranteed island cannot be guaranteed onto the mainland or into a lake.
///
/// ═══ ⚠ 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, LiftedSeeded;
public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN;
public List<(int x, int y, int r)> Centres = new();
public List Notes = new();
public int CountNorth, CountSouth, Bridged;
public long LiftedReverted; // specks the guard put back
public long RaisedReverted; // submerged debris bumps 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 (guard on)
}
///
/// 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 guardOn = !faithful && s.MinIslandAreaFrac > 0f;
float crest = sea + WorldScale.RawFromMetres(s.CrestM); // seaHere + OFFSHORE_ISLAND_H_M / 251f
// ═══ 2. THE ORGANIC LAYER ═══
var liftedOrigin = r.LiftedOrigin; // every lift, for the debris guard
if (s.Organic)
{
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.
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;
float thrN = IslandFalloff.CalibrateThreshold(samples, s.DensityNorth);
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;
if (faithful) thr = thrN;
else
{
float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f);
t = t * t * (3f - 2f * t);
thr = thrN + (thrS - thrN) * t;
}
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 (guardOn) r.RaisedOrigin.Add((x, y, before)); // for the debris guard (reshape only)
if (before < sea && h >= sea)
{
r.LiftedOrganic++;
r.Tag[x, y] = true;
r.Hemi[x, y] = OffshoreAnalysis.HemisphereOfRow(y, mapSize);
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 SEEDED FLOOR (Hybrid only) ═══
if (s.Mode == OffshoreMode.Hybrid && (s.FloorNorth > 0 || s.FloorSouth > 0))
{
var rng = new Pcg32(seed + s.PlacementSeedOffset);
int radius = Math.Max(2, (int)MathF.Round(s.StampRadiusFrac * mapSize));
// Rim jitter field: fine enough to put a few lobes around a stamp's rim (wavelength
// ≈ radius / 1.5), stated per map width so the look holds at every size. Seeded off
// the islet offset so it is decorrelated from the organic field but just as deterministic.
FastNoiseLite jitterNoise = s.StampEdgeJitter > 0f
? TerrainNoise.CreateModulation(seed, s.SeedOffset + 1, 1.5f / MathF.Max(s.StampRadiusFrac, 1e-4f), scale)
: null;
int reachRadius = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter));
int gap = Math.Max(1, (int)MathF.Round(s.LandGapFrac * mapSize));
float sepPx = s.SeparationFrac * mapSize;
float zoneFloorDepth = s.MinDepthM + s.DepthFeatherM; // zone weight exactly 1
float zoneFloorFalloff = s.MinFalloff + s.FalloffFeather;
var placed = new List<(int x, int y)>();
foreach (byte hemi in new[] { OffshoreAnalysis.HemiNorth, OffshoreAnalysis.HemiSouth })
{
int need = hemi == OffshoreAnalysis.HemiNorth ? s.FloorNorth : s.FloorSouth;
if (need <= 0) continue;
string name = OffshoreAnalysis.HemisphereName(hemi);
int y0 = hemi == OffshoreAnalysis.HemiNorth ? 0 : mapSize / 2;
int y1 = hemi == OffshoreAnalysis.HemiNorth ? mapSize / 2 : mapSize;
int got = 0, attempts = 0;
while (got < need)
{
if (++attempts > s.MaxPlacementAttempts)
throw new InvalidOperationException(
$"[OffshorePass] could not place the seeded floor: {name} needed {need}, placed {got} " +
$"after {attempts - 1} attempts. The valid ocean zone is too small or too crowded " +
$"for sep {s.SeparationFrac:F3} / gap {s.LandGapFrac:F3} / trench ≤ {s.TrenchInner:F2}. " +
"Refusing rather than under-delivering a guaranteed floor.");
int cx = rng.NextInt(mapSize);
int cy = y0 + rng.NextInt(y1 - y0);
// Valid ocean: below sea, FULLY inside the zone (moat + falloff + trench),
// so the centre's own weight is exactly 1 and the stamp surfaces.
float hc = height[cx, cy];
if (hc >= sea) continue;
if (WorldScale.MetresFromRaw(sea - hc) < zoneFloorDepth) continue;
if (preTrench[cx, cy] < zoneFloorFalloff) continue;
float dTr = MathF.Max(MathF.Abs(cx - centerX) / halfSpan, MathF.Abs(cy - centerY) / halfSpan);
if (dTr > s.TrenchInner) continue;
// Separation from the other seeded centres.
bool tooClose = false;
foreach (var (px, py) in placed)
{
float ddx = px - cx, ddy = py - cy;
if (ddx * ddx + ddy * ddy < sepPx * sepPx) { tooClose = true; break; }
}
if (tooClose) continue;
// ⭐ THE LAND GAP — the guarantee's mechanism. No land of ANY kind (mainland or
// organic) within radius + gap of the centre, so this stamp can touch nothing
// and is its own connected component by construction.
if (LandWithin(height, mapSize, cx, cy, reachRadius + gap, sea)) continue;
long lifted = Stamp(height, preTrench, r, cx, cy, radius, s, sea, crest, mapSize, centerX, centerY, halfSpan, jitterNoise);
if (lifted == 0)
throw new InvalidOperationException(
$"[OffshorePass] a seeded stamp at ({cx},{cy}) lifted no cells — the zone test and the stamp disagree. Refusing.");
placed.Add((cx, cy));
r.Centres.Add((cx, cy, radius));
r.LiftedSeeded += lifted;
got++;
r.Notes.Add($"[Offshore] floor {name} #{got} at ({cx},{cy}) r{radius}: lifted {lifted:N0} cells (attempt {attempts})");
}
}
}
// ═══ 3b. THE DEBRIS GUARD — reshape only; runs LAST, over everything the pass raised ═══
//
// Two kinds of debris, one rule. (a) SPECKS: a local maximum that barely clears the
// threshold surfaces a cap of a few cells — not an island. (b) 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 — which the first probe plate showed as a field of
// pale speckles across the southern sea, and which the water pass would later draw as a
// reef field nobody asked for. (The faithful control keeps both; that is the reference.)
//
// The rule: find the surviving islands (components at or above the minimum area), take
// each one's bounding box with a margin as its KEEP region, and put every organic raise
// OUTSIDE all keep regions back to the height it had — surfaced or not, tagged or not. A
// real island keeps its own submerged skirt; everything else goes back to seabed.
//
// ⚠ Runs after the floor so a stamp's rim satellite (w·zone crossing the waterline
// non-monotonically) is caught too; the stamp bodies are ~800+ cells and survive by
// construction. The floor is still asserted below.
if (guardOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0))
{
long minCells = Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * (double)mapSize * mapSize));
var pre = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize, out int[] compId);
// Rule 1 — SPECKS go by MEMBERSHIP: any surfaced cell whose component is below the
// minimum is reverted and untagged, wherever it sits (a speck inside a real island's
// skirt is still a speck — the first keep-box cut let those survive as 1-cell islands).
// Rule 2 — SUBMERGED BUMPS go by LOCATION: a raise outside every surviving island's
// skirt (bbox + margin) goes back to seabed; inside, it is that island's own slope.
var small = new HashSet();
var keep = new List<(int x0, int y0, int x1, int y1)>();
foreach (var c in pre)
{
if (c.Cells < minCells) { small.Add(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);
keep.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin));
}
bool Kept(int x, int y)
{
foreach (var (x0, y0, x1, y1) in keep)
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true;
return false;
}
long revertedRaised = 0, revertedLifted = 0;
foreach (var (x, y, h0) in r.LiftedOrigin)
{
if (!small.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
if (Kept(x, y)) continue; // inside a kept island's skirt
if (height[x, y] > h0) { height[x, y] = h0; revertedRaised++; }
}
r.LiftedReverted = revertedLifted;
r.RaisedReverted = revertedRaised;
r.Notes.Add($"[Offshore] debris guard: {small.Count} of {pre.Count} islands below {minCells:N0} cells reverted " +
$"({revertedLifted:N0} surfaced cells), plus {revertedRaised:N0} submerged bump cells outside any kept island's skirt.");
}
// ═══ PROVE THE FLOOR 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.LiftedSeeded - 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 (s.Mode == OffshoreMode.Hybrid && (r.CountNorth < s.FloorNorth || r.CountSouth < s.FloorSouth))
throw new InvalidOperationException(
$"[OffshorePass] FLOOR VIOLATION after placement: {r.CountNorth} N / {r.CountSouth} S against " +
$"{s.FloorNorth} N / {s.FloorSouth} S. The land gap should have made this impossible. Refusing.");
if (r.Bridged > 0)
throw new InvalidOperationException(
$"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing.");
return r;
}
/// One seeded stamp: a flat-topped disc lerped toward the crest, zone-masked per pixel.
private static long Stamp(float[,] height, float[,] preTrench, Result r, int cx, int cy, int radius,
OffshoreSettings s, float sea, float crest, int mapSize, float centerX, float centerY, float halfSpan,
FastNoiseLite jitterNoise)
{
long lifted = 0;
int reach = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter));
for (int dx = -reach; dx <= reach; dx++)
{
int px = cx + dx;
if (px < 0 || px >= mapSize) continue;
for (int dy = -reach; dy <= reach; dy++)
{
int py = cy + dy;
if (py < 0 || py >= mapSize) continue;
// The rim wanders: the effective radius at this pixel is the nominal one scaled
// by ±jitter from a fine noise field. Flat top and crisp shore are kept; the
// compass-drawn outline is not.
float rEff = radius;
if (jitterNoise != null)
rEff = radius * (1f + s.StampEdgeJitter * jitterNoise.GetNoise2D(px, py));
float dist = MathF.Sqrt(dx * dx + dy * dy);
float w = IslandFalloff.StampWeight(dist, rEff, s.StampCoreFrac);
if (w <= 0f) continue;
float h = height[px, py];
if (h >= sea) continue; // never touches land
// ⭐ The moat and the falloff test apply to the FLOOR too. Absolute.
float zone = IslandFalloff.OffshoreZoneWeight(
WorldScale.MetresFromRaw(sea - h), preTrench[px, py],
MathF.Abs(px - centerX) / halfSpan, MathF.Abs(py - centerY) / halfSpan,
s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather,
s.TrenchInner, s.TrenchOuter);
float t = w * zone;
if (t <= 0f) continue;
float before = h;
h = h + (crest - h) * t;
height[px, py] = h;
if (before < sea && h >= sea)
{
lifted++;
r.Tag[px, py] = true;
r.Hemi[px, py] = OffshoreAnalysis.HemisphereOfRow(py, mapSize);
r.LiftedOrigin.Add((px, py, before));
}
}
}
return lifted;
}
/// Any land cell (at/above sea) within a Chebyshev box of ? Conservative by design.
private static bool LandWithin(float[,] height, int mapSize, int cx, int cy, int reach, float sea)
{
int x0 = Math.Max(0, cx - reach), x1 = Math.Min(mapSize - 1, cx + reach);
int y0 = Math.Max(0, cy - reach), y1 = Math.Min(mapSize - 1, cy + reach);
for (int x = x0; x <= x1; x++)
for (int y = y0; y <= y1; y++)
if (height[x, y] >= sea) return true;
return false;
}
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; }
///
/// PCG32 (O'Neill) — the same tiny deterministic generator the reference's erosion pass
/// used. Seeded from the world seed + an offset, so island POSITIONS vary per world and
/// reproduce exactly for one. Trivially portable to C++.
///
private struct Pcg32
{
private ulong _state;
public Pcg32(int seed) { _state = 0; NextU(); _state += (ulong)(uint)seed; NextU(); }
public uint NextU()
{
ulong old = _state;
_state = old * 6364136223846793005UL + 1442695040888963407UL;
uint xorshifted = (uint)(((old >> 18) ^ old) >> 27);
int rot = (int)(old >> 59);
return (xorshifted >> rot) | (xorshifted << (-rot & 31));
}
public int NextInt(int n) => (int)(NextU() % (uint)n);
}
}
}