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