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
96 lines
4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|