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