islaApocalypse-v2/Tools/Scripts/TagOverlayRenderer.cs
beezm 3b96e06c1a chat2/05: offshore islands — rejoin the stub faithfully, then reshape to a guaranteed hybrid
Stage 1 fills the IslandFalloff.cs stub from the reference, verbatim: the coast shelf
(below-sea only, depth-preserving, held strictly below sea by MathF.BitDecrement — the
clamp that makes "cannot move the waterline" exact rather than statistical) and the islet
layer (OffshoreBlob, CalibrateThreshold against the field's ACTUAL distribution, the
OffshoreZoneWeight moat + pre-Trench-falloff test). Both run as pass 1b — OffshorePass, a
second sweep over the finished pass-1 arrays with the same per-pixel arithmetic in the
same order — and HMaxSeed is retaken AFTER them, as the reference did. That closes
chat2/00 Drift §2. The value did not move on any of 15 runs; the order is now right by
construction and oracle (l) prints it every time.

Stage 2 is the reshape, OffshoreSettings.Hybrid(): a seeded floor of ≥2 N / ≥4 S islands
placed by a PCG32 off the world seed — min-separated, clear of ALL existing land by a gap
so each stamp is its own connected component by construction, fully inside the zone so
the moat and falloff protections gate the floor exactly as they gate the organic layer —
plus the noise layer on top, smaller (freq 16), lower (24 m pre-curve, which the
preserved toe squashes to ~5 m and keeps there across curve tweaks), flatter (core 0.25),
crisper (edge sharpness 2.5, stamp rim jittered so it is rigid without being a compass
disc), south-weighted (0.007 N / 0.012 S, blended across the midline), corners allowed
(trench mask 0.90→0.97). Every lifted cell is tagged with its hemisphere — NORTH is rows
[0, N/2), y runs south, read off the spine's southern fade and the southern sinker, not
invented — and carried through Pass1Result → Pass2Result for a consumer that does not
exist yet.

Two things the probes taught, both now in the code: the first organic densities produced
183 blobs of which 174 were noise debris (a six-config sweep fixed that), and the
reference's lerp-to-crest leaves shallow humps across the seabed wherever a blob fails to
surface (a guard reverts them outside any surviving island's skirt; specks by component
membership, bumps by location). The faithful control keeps both, because it is the
reference — its islets measure min 1 cell, median 28.

Oracle, all hard checks passing: floor met on every hybrid seed (N 2–4, S 9–16); zero
land bridges; every offshore-OFF land cell bit-identical with offshore ON; tag ↔ coastline
consistent in both fields; curve-off still bit-identical to Phase 1's dump and
continuous_restored to task 03's. Both gates default OFF, deliberately: flipping them is
the act that retires the Phase-1 regression dumps, and that belongs in a task that
re-baselines the oracles.

The faithful control on seed 8675309 produced one north island. That is the gap the
floor exists to close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
2026-08-21 05:14:53 -04:00

96 lines
4 KiB
C#

using System.Collections.Generic;
using Godot;
namespace IslaApocalypse.Tools
{
/// <summary>
/// The offshore TAG / HEMISPHERE debug overlay (chat2/05): mainland one tint, offshore-island
/// land tinted by hemisphere, seeded centres ringed, the midline drawn — so the island count,
/// the N/S split and the tag's correctness are all visible at one glance.
///
/// ⚠ A DIAGNOSTIC, NOT A MAP. It draws the tag layer, which is DATA the shape pass set; it is
/// the one artifact in the batch that shows what a downstream consumer of the tag would see.
/// No hypsometry, no hillshade — three flat tints and some rings, on purpose.
///
/// Presentation only: it is handed arrays and returns a PNG. It cannot change them.
/// </summary>
public static class TagOverlayRenderer
{
private static readonly Color Sea = new(0.055f, 0.110f, 0.235f);
private static readonly Color Mainland = new(0.310f, 0.360f, 0.300f);
private static readonly Color IslandN = new(0.250f, 0.850f, 0.950f); // cool — north
private static readonly Color IslandS = new(0.980f, 0.600f, 0.200f); // warm — south
private static readonly Color Untagged = new(0.950f, 0.150f, 0.800f); // ⚠ land that is neither — must never appear
private static readonly Color Midline = new(0.700f, 0.720f, 0.760f);
private static readonly Color Ring = new(1.000f, 1.000f, 1.000f);
private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
/// <param name="isMainlandLand">
/// Per cell, land that is NOT offshore (from the offshore-OFF field, so a tag bug cannot hide
/// by mis-tagging mainland). Null ⇒ derived as "land and not tagged", which is weaker.
/// </param>
public static void SavePng(float[,] height, bool[,] tag, byte[,] hemi, int mapSize, float sea,
IReadOnlyList<(int x, int y, int r)> centres, int countN, int countS, string absolutePath)
{
var img = Image.CreateEmpty(mapSize, mapSize, false, Image.Format.Rgb8);
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
Color c;
bool land = height[x, y] >= sea;
bool tagged = tag != null && tag[x, y];
if (!land) c = Sea;
else if (!tagged) c = Mainland;
else if (hemi == null) c = Untagged;
else c = hemi[x, y] switch
{
OffshoreAnalysis.HemiNorth => IslandN,
OffshoreAnalysis.HemiSouth => IslandS,
_ => Untagged,
};
img.SetPixel(x, y, c);
}
}
// The hemisphere midline — the tag's convention, drawn where it bites.
int mid = mapSize / 2;
for (int x = 0; x < mapSize; x += 3) img.SetPixel(x, mid, Midline);
// Seeded centres: a ring at the stamp radius, so the floor islands can be told from the
// organic ones by eye.
if (centres != null)
foreach (var (cx, cy, r) in centres)
DrawRing(img, cx, cy, r, mapSize);
// A legend that cannot be separated from the picture.
int s = mapSize >= 4096 ? 4 : 3;
int lh = TinyFont.Height(s) + 6;
TinyFont.Draw(img, "OFFSHORE TAG OVERLAY", 12, 12, s, Ink);
TinyFont.Draw(img, "GREY: MAINLAND CYAN: ISLAND N ORANGE: ISLAND S", 12, 12 + lh, s, Ink);
TinyFont.Draw(img, $"ISLANDS: {countN} NORTH {countS} SOUTH - RINGS: SEEDED FLOOR", 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, "N ABOVE THE LINE - S BELOW - Y RUNS SOUTH", 12, 12 + lh * 3, s, Ink);
Error err = img.SavePng(absolutePath);
if (err != Error.Ok) GD.PrintErr($"[TagOverlayRenderer] SavePng failed ({err}) for {absolutePath}");
}
private static void DrawRing(Image img, int cx, int cy, int r, int n)
{
int rr = r + 3; // just outside the rim
int steps = System.Math.Max(64, rr * 4);
for (int i = 0; i < steps; i++)
{
double a = i * 2.0 * System.Math.PI / steps;
for (int t = 0; t < 2; t++) // 2 px thick
{
int px = cx + (int)System.Math.Round((rr + t) * System.Math.Cos(a));
int py = cy + (int)System.Math.Round((rr + t) * System.Math.Sin(a));
if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, Ring);
}
}
}
}
}