chat2/08: southern stretch, EXPLORATION — the fragmentation knob space, diagnosed then laddered
Diagnostic first (SouthernStretchTool, ISLA_DIAG_ONLY; scratch/southern_diagnosis.md). Along the central south profile the mask blend climbs 0.018 per 0.01 N, edge noise adds 0.03-0.06, and the southern sinker is zero until 0.75 N then adds 0.024 per 0.01 N before the 2.5 power; the coast sits at 0.76-0.82 N, ~0.06 N north of where the blend alone would end. The stretch compresses the southward distance the mask sees inside a fixed feathered latitude band, y' = yB + (y - yB)/(1 + s*ramp), texture (base noise, edge noise, latitude) untouched, so the extended mass keeps the elevation of the rows it came from. With the sinker reading the REAL y it cancels the stretch outright: median coast saturates at 0.83-0.84 from s = 1 to 16 and the south stays 1-10 tiny nubs on both seeds. The lever is therefore two things - extend reach AND hold the sinker back - and this pass couples them 1:1 (StretchSinker = true; the real-y mode is kept as the config's other setting). Ladder chosen from the sweep, non-linear: 0.5, 1, 2, 3, 5 (peninsula / onset / few big pieces / big pieces consolidating / over-stretched sheet); 8 and 16 are indistinguishable from 5. Topography: cells above the band take the untouched code path (fy == y), so the classify field north of the band is bit-identical by construction. SouthernStretch.cs holds the band constants (start 0.70 N, feather 0.05 N - fixed for the batch) and the ramp. TerrainGenConfig: SouthStretch (the swept axis, 0 = off), SouthBandStartFrac, SouthBandFeatherFrac, StretchSinker. Oracle, all passing: a1/a3 stretch OFF bit-identical; a3b stretch 5 north of the band bit-identical to the task-03 dump (2.93 M cells), a4b the same at 8192 against the terrain-curve-v1 gallery dump (46.97 M cells north of the band, 20.1 M differ below - the relaxation); per field north bit-locked classify and render vs stretch 0 (p, p2), northern island set invariant (q), centre is land, tag/coastline, classify == raw; ids and heights deterministic. Batch: BatchRoot(8, "southern_stretch_explore") - 5 levels x 2 seeds (1063685222, 999999937 - the two 07 seeds with the most mainland south of the band), lean render (regions overlay + relief + .f32), the hemisphere-split count/size table. Region labeling on, offshore / shelf / speck revert off: the pure fragmentation signal. The read at 4096: s 0.5 peninsula (coast +0.04 N); s 1 onset (999999937 6 -> 12 southern islands, 11 substantial); s 2 few big pieces (35k / 86k-cell fragments); s 3-5 a solid sheet to the trench whose southern edge is the trench wall - a ruler-straight coast - and whose island count falls as pieces re-merge. Gravel never dominates. Graduation held, per the task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
This commit is contained in:
parent
c32a3b177c
commit
537bd3e048
9 changed files with 798 additions and 4 deletions
|
|
@ -39,6 +39,8 @@ constants, carried over verbatim — not re-derived from a design summary** (→
|
||||||
| `Scripts/RegionPass.cs` | ⭐ **Pass 1c** (chat2/07) — `Core.RegionLabeling` over the classify field, the **origin-blind speck revert** (lower-only + component-only asserted, mainland never), the island tag **by construction** |
|
| `Scripts/RegionPass.cs` | ⭐ **Pass 1c** (chat2/07) — `Core.RegionLabeling` over the classify field, the **origin-blind speck revert** (lower-only + component-only asserted, mainland never), the island tag **by construction** |
|
||||||
| `Scripts/RegionOverlayRenderer.cs` | The labeled-regions overlay: mainland one tint, each island its own colour, reverted specks dark red |
|
| `Scripts/RegionOverlayRenderer.cs` | The labeled-regions overlay: mainland one tint, each island its own colour, reverted specks dark red |
|
||||||
| `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument |
|
| `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument |
|
||||||
|
| `Scripts/SouthernStretch.cs` | ⭐ **The southern stretch** (chat2/08, exploration) — the one deliberate sea-identity relaxation, inside a fixed feathered latitude band: `y' = yB + (y − yB)/(1 + stretch·ramp)` for the mask geometry (and the sinker), texture untouched; north of the band bit-locked by construction |
|
||||||
|
| `Scripts/SouthernStretchTool.cs` + `Scenes/SouthernStretchTool.tscn` | The chat2/08 batch — the diagnostic (`ISLA_DIAG_ONLY`) and the 5-level × 2-seed fragmentation ladder with the hemisphere-split instrument |
|
||||||
| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) |
|
| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) |
|
||||||
| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
|
| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
|
||||||
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
|
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
|
||||||
|
|
|
||||||
6
Tools/Scenes/SouthernStretchTool.tscn
Normal file
6
Tools/Scenes/SouthernStretchTool.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://cstretch08isla"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://Tools/Scripts/SouthernStretchTool.cs" id="1_sst"]
|
||||||
|
|
||||||
|
[node name="SouthernStretchTool" type="Node"]
|
||||||
|
script = ExtResource("1_sst")
|
||||||
|
|
@ -579,6 +579,54 @@ namespace IslaApocalypse.Tools
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ chat2/08 — the southern-stretch band checks ═══
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// (p) ⭐ NORTH BIT-LOCKED, BOTH DIRECTIONS — every cell with y < <paramref name="bandRow"/> is
|
||||||
|
/// bit-identical between the two fields (no land lowered, no sea raised); reports how many cells
|
||||||
|
/// differ at/below the band (allowed — that is the relaxation). The surgical guarantee, per cell.
|
||||||
|
/// </summary>
|
||||||
|
public static Check NorthLocked(string id, string name, float[,] a, float[,] b, int mapSize, int bandRow)
|
||||||
|
{
|
||||||
|
var c = new Check { Id = id, Name = name };
|
||||||
|
long northDiff = 0, southDiff = 0, north = 0; string first = null;
|
||||||
|
for (int x = 0; x < mapSize; x++)
|
||||||
|
for (int y = 0; y < mapSize; y++)
|
||||||
|
{
|
||||||
|
bool same = BitConverter.SingleToInt32Bits(a[x, y]) == BitConverter.SingleToInt32Bits(b[x, y]);
|
||||||
|
if (y < bandRow) { north++; if (!same) { northDiff++; first ??= $"[{x},{y}] {a[x, y]:G9} vs {b[x, y]:G9}"; } }
|
||||||
|
else if (!same) southDiff++;
|
||||||
|
}
|
||||||
|
c.Passed = northDiff == 0;
|
||||||
|
c.Detail = c.Passed
|
||||||
|
? $"all {north:N0} cells north of row {bandRow} bit-identical; {southDiff:N0} cells differ in the band or below (the relaxation)"
|
||||||
|
: $"{northDiff:N0} cells north of row {bandRow} DIFFER — {first}";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>(q) ⭐ NORTHERN ISLANDS INVARIANT — the multiset of north-hemisphere island components (size, centroid) is identical between two labelings.</summary>
|
||||||
|
public static Check NorthIslandsInvariant(string name, RegionLabels a, RegionLabels b)
|
||||||
|
{
|
||||||
|
var c = new Check { Id = "q", Name = name };
|
||||||
|
if (a == null || b == null) { c.Passed = false; c.Detail = "no region labeling"; return c; }
|
||||||
|
var sa = NorthSet(a); var sb = NorthSet(b);
|
||||||
|
bool same = sa.Count == sb.Count;
|
||||||
|
if (same) for (int i = 0; i < sa.Count; i++) if (sa[i] != sb[i]) { same = false; break; }
|
||||||
|
c.Passed = same;
|
||||||
|
c.Detail = same ? $"{sa.Count} northern islands, identical (size + centroid)" : $"DIFFER — {sa.Count} vs {sb.Count} northern islands, or a size/centroid moved";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> NorthSet(RegionLabels l)
|
||||||
|
{
|
||||||
|
var list = new List<string>();
|
||||||
|
foreach (var r in l.Regions)
|
||||||
|
if (!r.IsMainland && r.Hemisphere == RegionLabeling.HemiNorth)
|
||||||
|
list.Add($"{r.SizeCells}:{r.CentroidX:F3}:{r.CentroidY:F3}");
|
||||||
|
list.Sort(StringComparer.Ordinal);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
|
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
|
||||||
public static string ToMarkdownTable(IEnumerable<Check> checks)
|
public static string ToMarkdownTable(IEnumerable<Check> checks)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
57
Tools/Scripts/SouthernStretch.cs
Normal file
57
Tools/Scripts/SouthernStretch.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE SOUTHERN STRETCH (chat2/08, EXPLORATION) — the one deliberate relaxation of sea identity,
|
||||||
|
/// confined to a FIXED feathered latitude band.
|
||||||
|
///
|
||||||
|
/// ═══ THE MECHANISM (read from pass 1, chat2/08 diagnostic) ═══
|
||||||
|
///
|
||||||
|
/// Pass 1's mask is falloff = ½·ellipse + ½·squircle (+ edge noise · squircle), then the SOUTHERN
|
||||||
|
/// SINKER adds 0.6 · (y − 0.75N)/(0.25N) for y > 0.75N, BEFORE the 2.5 power; the coast sits
|
||||||
|
/// where rawBase − falloff^2.5 crosses sea, i.e. near falloff ≈ 0.66. Every term that grows with y
|
||||||
|
/// is a function of the SOUTHWARD DISTANCE. The stretch compresses that distance inside the band:
|
||||||
|
///
|
||||||
|
/// y' = yB + (y − yB) / (1 + stretch · ramp(y)) ramp = smoothstep over the feather
|
||||||
|
///
|
||||||
|
/// so a cell at y takes the mask geometry of the row y' north of it — the mass reaches further
|
||||||
|
/// south AND keeps the elevation profile of the rows it came from (the base noise, edge noise and
|
||||||
|
/// latitude field keep the real y — texture stays, only the mask's geometry stretches). Where the
|
||||||
|
/// stretched thin edge drops below sea it fragments organically. Nothing is stamped.
|
||||||
|
///
|
||||||
|
/// ⚠ North of the band (y ≤ yB) the caller takes the untouched code path: bit-identical by
|
||||||
|
/// construction, asserted by the oracle. The band line and feather are constants for a batch;
|
||||||
|
/// <c>TerrainGenConfig.SouthStretch</c> is the only swept axis.
|
||||||
|
///
|
||||||
|
/// The SINKER: <c>TerrainGenConfig.StretchSinker</c> decides whether it rides y' (pushed out with
|
||||||
|
/// the geometry — held back inside the band) or the real y (keeps pulling the extended mass down
|
||||||
|
/// where it always did). The chat2/08 diagnostic measured both — see the report.
|
||||||
|
/// </summary>
|
||||||
|
public static class SouthernStretch
|
||||||
|
{
|
||||||
|
/// <summary>The band's fixed latitude line, fraction of the map. Chosen by the chat2/08 diagnostic (see the report).</summary>
|
||||||
|
public const float DefaultBandStartFrac = 0.70f;
|
||||||
|
|
||||||
|
/// <summary>The feather width, fraction of the map.</summary>
|
||||||
|
public const float DefaultBandFeatherFrac = 0.05f;
|
||||||
|
|
||||||
|
/// <summary>Whether the sinker rides the stretched distance by default. Set by the chat2/08 diagnostic.</summary>
|
||||||
|
public const bool DefaultStretchSinker = true;
|
||||||
|
|
||||||
|
/// <summary>The smoothstep ramp across the feather: 0 at the band line, 1 a feather-width below it.</summary>
|
||||||
|
public static float Ramp(float y, float bandStart, float bandFeather)
|
||||||
|
{
|
||||||
|
float t = Math.Clamp((y - bandStart) / bandFeather, 0f, 1f);
|
||||||
|
return t * t * (3f - 2f * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The y the mask geometry sees. For y ≤ bandStart returns y unchanged.</summary>
|
||||||
|
public static float StretchedY(float y, float bandStart, float bandFeather, float stretch)
|
||||||
|
{
|
||||||
|
if (y <= bandStart || stretch <= 0f) return y;
|
||||||
|
float r = Ramp(y, bandStart, bandFeather);
|
||||||
|
return bandStart + (y - bandStart) / (1f + stretch * r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/SouthernStretch.cs.uid
Normal file
1
Tools/Scripts/SouthernStretch.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dttggsatk4hh0
|
||||||
631
Tools/Scripts/SouthernStretchTool.cs
Normal file
631
Tools/Scripts/SouthernStretchTool.cs
Normal file
|
|
@ -0,0 +1,631 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Godot;
|
||||||
|
using IslaApocalypse.Core;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE SOUTHERN-STRETCH EXPLORATION BATCH (chat2/08) — map the fragmentation knob space: a
|
||||||
|
/// peninsula → few-big-pieces → gravel ladder, 5 stretch levels × 2 seeds, with the region layer
|
||||||
|
/// as the instrument. NOT a converged setting.
|
||||||
|
///
|
||||||
|
/// ═══ TWO MODES ═══
|
||||||
|
///
|
||||||
|
/// ISLA_DIAG_ONLY=1 the DIAGNOSTIC (numbers, no plates): the three southern forces along a
|
||||||
|
/// south-running profile (falloff blend / edge noise / sinker, each alone),
|
||||||
|
/// the reach table for the candidate seeds, and a stretch sweep under both
|
||||||
|
/// sinker modes — written to scratch/southern_diagnosis.md. Run first; it
|
||||||
|
/// sets the ladder.
|
||||||
|
/// (default) the BATCH: 5 levels × 2 seeds at ISLA_MAPSIZE, lean render per field
|
||||||
|
/// (labeled-regions overlay + relief + .f32), the hemisphere-split count/size
|
||||||
|
/// table, the asymmetric oracle.
|
||||||
|
///
|
||||||
|
/// Every field: pass 1 + the stretch, region labeling ON, offshore OFF, shelf OFF, speck revert OFF
|
||||||
|
/// — the pure fragmentation signal (the instrument counts "all islands" and "islands ≥ the 07
|
||||||
|
/// mid threshold" side by side). The curve is the tagged curve, unchanged.
|
||||||
|
///
|
||||||
|
/// ═══ RUNNING IT ═══
|
||||||
|
///
|
||||||
|
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
|
||||||
|
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/SouthernStretchTool.tscn
|
||||||
|
///
|
||||||
|
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
|
||||||
|
/// ISLA_MAPSIZE plate size (default 4096)
|
||||||
|
/// ISLA_CALIB_SIZE curve calibration + diagnostic size (default 2048)
|
||||||
|
/// ISLA_SEEDS the two batch seeds (default: auto — the two 07 seeds with the most southern mass)
|
||||||
|
/// ISLA_CANDIDATE_SEEDS the pool the auto-pick reads (default: the 07 table seeds)
|
||||||
|
/// ISLA_STRETCH_LEVELS the 5 stretch values (default: the diagnostic-chosen ladder below)
|
||||||
|
/// ISLA_BAND_START / ISLA_BAND_FEATHER the fixed band (fractions of the map; constants for the batch)
|
||||||
|
/// ISLA_STRETCH_SINKER 1 = the sinker rides the stretched distance (default), 0 = real y
|
||||||
|
/// ISLA_DIAG_ONLY=1 diagnostic only
|
||||||
|
/// ISLA_SKIP_8K=1 skip the 8192 band regression (a4b)
|
||||||
|
/// </summary>
|
||||||
|
public partial class SouthernStretchTool : Node
|
||||||
|
{
|
||||||
|
private static readonly int[] DefaultCandidateSeeds =
|
||||||
|
{
|
||||||
|
1063685222, 20260821, 8675309, 123456789, 271828182, 999999937, 90210, 424242,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
|
||||||
|
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE LADDER — chosen by the diagnostic (chat2/08 report §1), not linear: the stretch
|
||||||
|
/// bites unevenly, so the steps are spaced where the southern count/size actually moves.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly float[] DefaultLadder = { 0.5f, 1.0f, 2.0f, 3.0f, 5.0f };
|
||||||
|
|
||||||
|
/// <summary>The diagnostic's sweep (both sinker modes).</summary>
|
||||||
|
private static readonly float[] DiagSweep = { 0.25f, 0.5f, 1f, 1.5f, 2f, 3f, 5f, 8f, 16f };
|
||||||
|
|
||||||
|
private const int DefaultMapSize = 4096;
|
||||||
|
private const int DefaultCalibSize = 2048;
|
||||||
|
private const int GallerySize = 8192;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
try { Run(); }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr("==================================================================");
|
||||||
|
GD.PrintErr($" REFUSED: {e.Message}");
|
||||||
|
GD.PrintErr(e.StackTrace);
|
||||||
|
GD.PrintErr("==================================================================");
|
||||||
|
GetTree().Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Row
|
||||||
|
{
|
||||||
|
public int Level; public float Stretch; public int Seed;
|
||||||
|
public double ReachFrac, MedianCoastFrac; public long MainlandCells, MainlandSouthOfBand;
|
||||||
|
public int SouthAll, SouthBig, NorthAll, NorthBig;
|
||||||
|
public long SMin, SMed, SMax, NMax; public double SMean; public int[] SHist;
|
||||||
|
public bool Ok; public ulong Ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
ToolingPaths.Configure(OS.GetUserDataDir());
|
||||||
|
|
||||||
|
int task = EnvInt("ISLA_TASK", 8);
|
||||||
|
string descr = EnvStr("ISLA_BATCH", "southern_stretch_explore");
|
||||||
|
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
|
||||||
|
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
|
||||||
|
int[] candidates = EnvSeeds("ISLA_CANDIDATE_SEEDS", DefaultCandidateSeeds);
|
||||||
|
int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null);
|
||||||
|
float[] ladder = EnvFloats("ISLA_STRETCH_LEVELS", DefaultLadder);
|
||||||
|
float bandStart = EnvFloat("ISLA_BAND_START", SouthernStretch.DefaultBandStartFrac);
|
||||||
|
float bandFeather = EnvFloat("ISLA_BAND_FEATHER", SouthernStretch.DefaultBandFeatherFrac);
|
||||||
|
bool stretchSinker = EnvStr("ISLA_STRETCH_SINKER", SouthernStretch.DefaultStretchSinker ? "1" : "0") == "1";
|
||||||
|
bool diagOnly = EnvStr("ISLA_DIAG_ONLY", "0") == "1";
|
||||||
|
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
|
||||||
|
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
|
||||||
|
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
|
||||||
|
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
|
||||||
|
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
|
||||||
|
|
||||||
|
string batchRoot = ToolingPaths.BatchRoot(task, descr);
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||||||
|
string scratch = ToolingPaths.BatchScratch(batchRoot);
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(scratch);
|
||||||
|
|
||||||
|
var anchors = CurveAnchors.Default;
|
||||||
|
float sea = 0.15f;
|
||||||
|
long bigCells4k = Cells(RegionPass.ThresholdMidFrac, mapSize);
|
||||||
|
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print(" SOUTHERN STRETCH (chat2/08) — EXPLORATION: map the fragmentation knob space");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print($"MapSize : {mapSize} (plates) calibration + diagnostic at {calibSize}");
|
||||||
|
GD.Print($"band : start {bandStart:F3} of the map (row {(int)(bandStart * mapSize)} at {mapSize}), feather {bandFeather:F3} — FIXED for the batch");
|
||||||
|
GD.Print($"sinker : {(stretchSinker ? "rides the stretched distance (held back inside the band)" : "real y (keeps pulling the extended mass down)")}");
|
||||||
|
GD.Print($"ladder : {string.Join(", ", ladder)}");
|
||||||
|
GD.Print($"fields : pass 1 + stretch · labeling ON · offshore OFF · shelf OFF · speck revert OFF (\"big\" island = ≥ {bigCells4k:N0} cells at {mapSize}, the 07 mid threshold)");
|
||||||
|
GD.Print($"batch : {batchRoot}{(diagOnly ? " ⚠ ISLA_DIAG_ONLY — the diagnostic, no plates" : "")}");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
|
||||||
|
// ═══ 0. THE CURVE ═══
|
||||||
|
GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---");
|
||||||
|
var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
|
||||||
|
GD.Print($" {knots}");
|
||||||
|
|
||||||
|
TerrainGenConfig Cfg(int size, int seed, string label, float stretch, bool sinkerStretched, bool sinkerOn = true, bool edgeOn = true)
|
||||||
|
{
|
||||||
|
var c = new TerrainGenConfig
|
||||||
|
{
|
||||||
|
MapSize = size, Seed = seed, VariantLabel = label,
|
||||||
|
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
|
||||||
|
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
|
||||||
|
CoastShelf = false, Offshore = new OffshoreSettings(),
|
||||||
|
RegionLabeling = true, SpeckRevert = false,
|
||||||
|
SouthStretch = stretch, SouthBandStartFrac = bandStart, SouthBandFeatherFrac = bandFeather, StretchSinker = sinkerStretched,
|
||||||
|
SouthernSinker = sinkerOn, EdgeNoise = edgeOn,
|
||||||
|
};
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 1. THE SEEDS — southern mass, measured ═══
|
||||||
|
GD.Print($"\n--- 1. SOUTHERN REACH of the candidate seeds at {calibSize} (offshore off, stretch off) ---");
|
||||||
|
var reachRows = new List<(int seed, double reach, double median, long southCells, long mainland, int southIslands)>();
|
||||||
|
int bandRowC = (int)(bandStart * calibSize);
|
||||||
|
foreach (int s in candidates)
|
||||||
|
{
|
||||||
|
var p = Topography.Generate(Cfg(calibSize, s, "reach", 0f, stretchSinker));
|
||||||
|
var (reach, median, southCells) = Reach(p.Regions, calibSize, bandRowC);
|
||||||
|
var (n, so) = RegionLabeling.IslandsByHemisphere(p.Regions);
|
||||||
|
reachRows.Add((s, reach, median, southCells, p.Regions.Mainland.SizeCells, so));
|
||||||
|
GD.Print($" seed {s,-11} southernmost mainland row {reach:F3} of map, median coast {median:F3}, mainland cells south of band {southCells,9:N0} ({100.0 * southCells / p.Regions.Mainland.SizeCells:F1} % of mainland), natural S islands {so}");
|
||||||
|
}
|
||||||
|
int[] seeds = seedsEnv;
|
||||||
|
if (seeds == null)
|
||||||
|
{
|
||||||
|
reachRows.Sort((a, b) => b.southCells.CompareTo(a.southCells));
|
||||||
|
seeds = new[] { reachRows[0].seed, reachRows[1].seed };
|
||||||
|
}
|
||||||
|
GD.Print($" → batch seeds: {seeds[0]}, {seeds[1]}{(seedsEnv == null ? " (auto: the two with the most mainland south of the band)" : " (ISLA_SEEDS)")}");
|
||||||
|
|
||||||
|
// ═══ 2. THE DIAGNOSTIC ═══
|
||||||
|
var diag = new StringBuilder();
|
||||||
|
diag.AppendLine("# The southern diagnosis — chat2/08 (measured by SouthernStretchTool, ISLA_DIAG_ONLY)");
|
||||||
|
diag.AppendLine();
|
||||||
|
diag.AppendLine($"Size {calibSize}. Band start {bandStart:F3} (row {bandRowC}), feather {bandFeather:F3}. y runs south; fractions are y / MapSize.");
|
||||||
|
diag.AppendLine();
|
||||||
|
diag.AppendLine("## 1. Southern reach of the candidate seeds (offshore off, stretch off)");
|
||||||
|
diag.AppendLine();
|
||||||
|
diag.AppendLine("| seed | southernmost mainland row | median coast row (per column, central 60 %) | mainland cells south of band | % of mainland | natural S islands |");
|
||||||
|
diag.AppendLine("|---|---|---|---|---|---|");
|
||||||
|
foreach (var r in reachRows) diag.AppendLine($"| `{r.seed}` | {r.reach:F3} | {r.median:F3} | {r.southCells:N0} | {100.0 * r.southCells / r.mainland:F1} % | {r.southIslands} |");
|
||||||
|
diag.AppendLine();
|
||||||
|
diag.AppendLine($"**Batch seeds:** `{seeds[0]}`, `{seeds[1]}`.");
|
||||||
|
diag.AppendLine();
|
||||||
|
|
||||||
|
if (diagOnly)
|
||||||
|
{
|
||||||
|
GD.Print($"\n--- 2. THE THREE FORCES along a south-running profile (central 20 % of x, averaged) ---");
|
||||||
|
foreach (int s in seeds)
|
||||||
|
{
|
||||||
|
var full = Topography.Generate(Cfg(calibSize, s, "full", 0f, stretchSinker));
|
||||||
|
var noSink = Topography.Generate(Cfg(calibSize, s, "nosink", 0f, stretchSinker, sinkerOn: false));
|
||||||
|
var bare = Topography.Generate(Cfg(calibSize, s, "bare", 0f, stretchSinker, sinkerOn: false, edgeOn: false));
|
||||||
|
diag.AppendLine($"## 2. The three forces — seed `{s}` (central 20 % of x averaged; falloff terms are PRE-power)");
|
||||||
|
diag.AppendLine();
|
||||||
|
diag.AppendLine("| y / N | blend (ellipse+squircle) | edge noise | sinker | total pre-trench | total^2.5 | mean height | land fraction of row |");
|
||||||
|
diag.AppendLine("|---|---|---|---|---|---|---|---|");
|
||||||
|
int x0 = (int)(calibSize * 0.40), x1 = (int)(calibSize * 0.60);
|
||||||
|
for (int yi = 50; yi <= 100; yi += 2)
|
||||||
|
{
|
||||||
|
int y = Math.Min(calibSize - 1, yi * calibSize / 100);
|
||||||
|
double sb = 0, sn = 0, sf = 0, sh = 0; long land = 0; int cnt = 0;
|
||||||
|
for (int x = x0; x < x1; x++)
|
||||||
|
{
|
||||||
|
sb += bare.PreTrenchFalloff[x, y]; sn += noSink.PreTrenchFalloff[x, y]; sf += full.PreTrenchFalloff[x, y];
|
||||||
|
sh += full.Height[x, y]; if (full.Height[x, y] >= sea) land++; cnt++;
|
||||||
|
}
|
||||||
|
double blend = sb / cnt, edge = sn / cnt - blend, sink = sf / cnt - sn / cnt, total = sf / cnt;
|
||||||
|
diag.AppendLine($"| {y / (double)calibSize:F2} | {blend:F3} | {edge:F3} | {sink:F3} | {total:F3} | {Math.Pow(Math.Max(0, total), 2.5):F3} | {sh / cnt:F3} | {land / (double)cnt:P0} |");
|
||||||
|
GD.Print($" seed {s} y {y / (double)calibSize:F2}: blend {blend:F3} edge {edge:F3} sinker {sink:F3} total {total:F3} pow {Math.Pow(Math.Max(0, total), 2.5):F3} height {sh / cnt:F3} land {land / (double)cnt:P0}");
|
||||||
|
}
|
||||||
|
diag.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"\n--- 3. THE STRETCH SWEEP (both sinker modes) ---");
|
||||||
|
diag.AppendLine("## 3. The stretch sweep — reach, southern islands (all / ≥ mid threshold), sizes, and the northern control");
|
||||||
|
diag.AppendLine();
|
||||||
|
long bigC = Cells(RegionPass.ThresholdMidFrac, calibSize);
|
||||||
|
diag.AppendLine($"\"big\" = ≥ {bigC} cells at {calibSize} (the 07 `threshold_mid`). Reach = southernmost mainland row / N; median coast over the central 60 % of columns.");
|
||||||
|
diag.AppendLine();
|
||||||
|
foreach (bool mode in new[] { false, true })
|
||||||
|
{
|
||||||
|
diag.AppendLine($"### Sinker on {(mode ? "the STRETCHED distance (held back with the geometry)" : "the REAL y (unchanged)")}");
|
||||||
|
diag.AppendLine();
|
||||||
|
diag.AppendLine("| seed | stretch | reach | median coast | mainland cells south of band | S islands all / big | S size med / max | S largest island | N islands (control) |");
|
||||||
|
diag.AppendLine("|---|---|---|---|---|---|---|---|---|");
|
||||||
|
foreach (int s in seeds)
|
||||||
|
{
|
||||||
|
var sweep = new List<float> { 0f }; sweep.AddRange(DiagSweep);
|
||||||
|
foreach (float st in sweep)
|
||||||
|
{
|
||||||
|
var p = Topography.Generate(Cfg(calibSize, s, $"sweep_{st}", st, mode));
|
||||||
|
var (reach, median, southCells) = Reach(p.Regions, calibSize, bandRowC);
|
||||||
|
var stats = Hemi(p.Regions, bigC);
|
||||||
|
diag.AppendLine($"| `{s}` | {st:G3} | {reach:F3} | {median:F3} | {southCells:N0} | {stats.southAll} / {stats.southBig} | {stats.sMed} / {stats.sMax} | {stats.sMax} | {stats.northAll} |");
|
||||||
|
GD.Print($" sinker {(mode ? "stretched" : "real ")} seed {s,-11} stretch {st,5:G3} reach {reach:F3} median {median:F3} southCells {southCells,8:N0} S {stats.southAll,3}/{stats.southBig,3} med {stats.sMed,6} max {stats.sMax,7} N {stats.northAll}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diag.AppendLine();
|
||||||
|
}
|
||||||
|
WriteText(Path.Combine(scratch, "southern_diagnosis.md"), diag.ToString());
|
||||||
|
GD.Print($"\n diagnosis written: {Path.Combine(scratch, "southern_diagnosis.md")}");
|
||||||
|
GD.Print(" ISLA_DIAG_ONLY — done; no plates.");
|
||||||
|
GetTree().Quit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 3. REGRESSIONS — north bit-identical to terrain-curve-v1; stretch-off bit-identical everywhere ═══
|
||||||
|
GD.Print($"\n--- 3. REGRESSIONS at {calibSize}, seed {seeds[0]} ---");
|
||||||
|
var hard = new List<ShapingOracle.Check>();
|
||||||
|
int plate = seeds[0];
|
||||||
|
float maxStretch = ladder[ladder.Length - 1];
|
||||||
|
{
|
||||||
|
var offCfg = Cfg(calibSize, plate, "off", 0f, stretchSinker);
|
||||||
|
Pass1Result p1 = Topography.Generate(offCfg);
|
||||||
|
var curveOff = offCfg.Clone(); curveOff.Curve = false;
|
||||||
|
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plate}_full", "height.f32");
|
||||||
|
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
|
||||||
|
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plate}_continuous_restored", "height.f32");
|
||||||
|
float[,] t03 = HeightField.Load(t03Dump, calibSize);
|
||||||
|
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, stretch OFF == task-03 .f32 dump", Shaping.Shape(p1, offCfg).Height, t03, calibSize, t03Dump));
|
||||||
|
|
||||||
|
// ⭐ a3b — stretch ON at the ladder's TOP: north of the band bit-identical to the tag's own dump.
|
||||||
|
var topCfg = Cfg(calibSize, plate, "top", maxStretch, stretchSinker);
|
||||||
|
Pass2Result pTop = Shaping.Shape(Topography.Generate(topCfg), topCfg);
|
||||||
|
if (t03 != null)
|
||||||
|
hard.Add(ShapingOracle.NorthLocked("a3b", $"stretch {maxStretch:G3} ON: north of the band bit-identical to task-03 dump (terrain-curve-v1); changes only in/below the band", pTop.Height, t03, calibSize, bandRowC));
|
||||||
|
foreach (var c in hard) GD.Print(" " + c);
|
||||||
|
|
||||||
|
if (!skip8k)
|
||||||
|
{
|
||||||
|
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plate}", "height.f32");
|
||||||
|
if (File.Exists(t04Dump))
|
||||||
|
{
|
||||||
|
GD.Print($" a4b: generating {plate} at {GallerySize}, stretch {maxStretch:G3} …");
|
||||||
|
var g = Cfg(GallerySize, plate, "top", maxStretch, stretchSinker);
|
||||||
|
Pass2Result pG = Shaping.Shape(Topography.Generate(g), g);
|
||||||
|
var a4b = ShapingOracle.NorthLocked("a4b", $"stretch {maxStretch:G3} ON at {GallerySize}: north of the band bit-identical to terrain-curve-v1's 04 gallery dump",
|
||||||
|
pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, (int)(bandStart * GallerySize));
|
||||||
|
hard.Add(a4b); GD.Print(" " + a4b);
|
||||||
|
}
|
||||||
|
else GD.Print($" a4b: ⚠ skipped — no 04 gallery dump at {t04Dump}");
|
||||||
|
}
|
||||||
|
else GD.Print(" a4b: skipped (ISLA_SKIP_8K)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 4. THE LADDER — 5 levels × 2 seeds ═══
|
||||||
|
GD.Print($"\n--- 4. THE LADDER at {mapSize} ---");
|
||||||
|
int bandRow = (int)(bandStart * mapSize);
|
||||||
|
var rows = new List<Row>();
|
||||||
|
var perField = new List<ShapingOracle.Check>();
|
||||||
|
var baselineRows = new Dictionary<int, Row>();
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var c0 = Cfg(mapSize, seed, "stretch_0", 0f, stretchSinker);
|
||||||
|
Pass1Result p0 = Topography.Generate(c0);
|
||||||
|
Pass2Result q0 = Shaping.Shape(p0, c0);
|
||||||
|
baselineRows[seed] = MakeRow(0, 0f, seed, p0, mapSize, bandRow, bigCells4k, true, p0.ElapsedMs);
|
||||||
|
var r0 = baselineRows[seed];
|
||||||
|
GD.Print($" seed {seed} baseline (stretch 0): reach {r0.ReachFrac:F3}, S islands {r0.SouthAll} / big {r0.SouthBig}, N islands {r0.NorthAll}");
|
||||||
|
|
||||||
|
for (int li = 0; li < ladder.Length; li++)
|
||||||
|
{
|
||||||
|
float st = ladder[li];
|
||||||
|
string label = $"stretch_{li + 1}";
|
||||||
|
var cfg = Cfg(mapSize, seed, label, st, stretchSinker);
|
||||||
|
Pass1Result p1 = Topography.Generate(cfg);
|
||||||
|
Pass2Result p2 = Shaping.Shape(p1, cfg);
|
||||||
|
|
||||||
|
var checks = new List<ShapingOracle.Check>
|
||||||
|
{
|
||||||
|
ShapingOracle.NorthLocked("p", "north of the band bit-locked (classify) vs stretch 0", p1.Height, p0.Height, mapSize, bandRow),
|
||||||
|
ShapingOracle.NorthLocked("p2", "north of the band bit-locked (render) vs stretch 0", p2.Height, q0.Height, mapSize, bandRow),
|
||||||
|
ShapingOracle.NorthIslandsInvariant("northern islands invariant vs stretch 0", p0.Regions, p1.Regions),
|
||||||
|
ShapingOracle.CentreIsLand(p1),
|
||||||
|
ShapingOracle.TagCoastlineConsistent(p2, sea),
|
||||||
|
ShapingOracle.ClassifyFidelity(p1, p2),
|
||||||
|
};
|
||||||
|
foreach (var c in checks) { c.Name += $" [{label} = {st:G3}, {seed}]"; perField.Add(c); }
|
||||||
|
bool ok = checks.TrueForAll(c => c.Passed);
|
||||||
|
|
||||||
|
var row = MakeRow(li + 1, st, seed, p1, mapSize, bandRow, bigCells4k, ok, p1.ElapsedMs);
|
||||||
|
rows.Add(row);
|
||||||
|
WriteField(batchRoot, p1, p2, sea, anchors, skipRaw, st);
|
||||||
|
GD.Print($" {label,-10} {st,5:G3} seed {seed,-11} reach {row.ReachFrac:F3} (median coast {row.MedianCoastFrac:F3}) mainland S-of-band {row.MainlandSouthOfBand,9:N0} " +
|
||||||
|
$"S islands {row.SouthAll,3} / big {row.SouthBig,3} S size med {row.SMed,6} max {row.SMax,7} N islands {row.NorthAll,3} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determinism: the middle level on the first seed, twice
|
||||||
|
{
|
||||||
|
float st = ladder[ladder.Length / 2];
|
||||||
|
var cA = Cfg(mapSize, plate, "det", st, stretchSinker); var cB = Cfg(mapSize, plate, "det", st, stretchSinker);
|
||||||
|
var a = Topography.Generate(cA); var b = Topography.Generate(cB);
|
||||||
|
var det = ShapingOracle.LabelsDeterministic(a, b); det.Name += $" [stretch {st:G3}, {plate}]";
|
||||||
|
var bits = ShapingOracle.NorthLocked("o2", $"two generations bit-identical everywhere [stretch {st:G3}, {plate}]", a.Height, b.Height, mapSize, mapSize);
|
||||||
|
perField.Add(det); perField.Add(bits);
|
||||||
|
GD.Print(" " + det); GD.Print(" " + bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allOk = hard.TrueForAll(c => c.Passed) && perField.TrueForAll(c => c.Passed);
|
||||||
|
GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
|
||||||
|
foreach (var c in perField) if (!c.Passed) GD.PrintErr(" " + c);
|
||||||
|
|
||||||
|
WriteTable(batchRoot, mapSize, ladder, seeds, rows, baselineRows, bigCells4k, bandStart, bandFeather, stretchSinker);
|
||||||
|
WriteIndex(batchRoot, mapSize, calibSize, ladder, seeds, rows, baselineRows, bigCells4k, bandStart, bandFeather, stretchSinker, hard, perField, allOk, diag.ToString());
|
||||||
|
|
||||||
|
GD.Print("\n==================================================================");
|
||||||
|
GD.Print($" DONE — {batchRoot}");
|
||||||
|
GD.Print($" ORACLE {(allOk ? "HARD CHECKS ALL PASS" : "*** FAILURES — see the table ***")}");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GetTree().Quit(allOk ? 0 : 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the instrument --------------------------------------------------
|
||||||
|
|
||||||
|
private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size));
|
||||||
|
|
||||||
|
/// <summary>Southernmost mainland row / N, the median per-column coast row (central 60 % of columns) / N, and mainland cells at/below the band row.</summary>
|
||||||
|
private static (double reach, double median, long southCells) Reach(RegionLabels l, int n, int bandRow)
|
||||||
|
{
|
||||||
|
int main = l.MainlandId; int maxY = -1; long south = 0;
|
||||||
|
var coast = new List<int>();
|
||||||
|
int x0 = (int)(n * 0.2), x1 = (int)(n * 0.8);
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
{
|
||||||
|
int colMax = -1;
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
{
|
||||||
|
if (l.Id[x * n + y] != main) continue;
|
||||||
|
if (y > colMax) colMax = y;
|
||||||
|
if (y >= bandRow) south++;
|
||||||
|
}
|
||||||
|
if (colMax > maxY) maxY = colMax;
|
||||||
|
if (x >= x0 && x < x1 && colMax >= 0) coast.Add(colMax);
|
||||||
|
}
|
||||||
|
coast.Sort();
|
||||||
|
double median = coast.Count == 0 ? 0 : coast[coast.Count / 2] / (double)n;
|
||||||
|
return (maxY / (double)n, median, south);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int southAll, int southBig, int northAll, int northBig, long sMin, long sMed, double sMean, long sMax, long nMax, int[] sHist)
|
||||||
|
Hemi(RegionLabels l, long big)
|
||||||
|
{
|
||||||
|
var south = new List<long>(); int northAll = 0, northBig = 0; long nMax = 0;
|
||||||
|
foreach (var r in l.Regions)
|
||||||
|
{
|
||||||
|
if (r.IsMainland) continue;
|
||||||
|
if (r.Hemisphere == RegionLabeling.HemiSouth) south.Add(r.SizeCells);
|
||||||
|
else { northAll++; if (r.SizeCells >= big) northBig++; nMax = Math.Max(nMax, r.SizeCells); }
|
||||||
|
}
|
||||||
|
south.Sort();
|
||||||
|
var hist = new int[RegionLabeling.HistogramEdges.Length + 1];
|
||||||
|
int sBig = 0; double sum = 0;
|
||||||
|
foreach (long s in south) { hist[RegionLabeling.HistogramBin(s)]++; if (s >= big) sBig++; sum += s; }
|
||||||
|
return (south.Count, sBig, northAll, northBig,
|
||||||
|
south.Count == 0 ? 0 : south[0], south.Count == 0 ? 0 : south[south.Count / 2], south.Count == 0 ? 0 : sum / south.Count,
|
||||||
|
south.Count == 0 ? 0 : south[south.Count - 1], nMax, hist);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Row MakeRow(int level, float st, int seed, Pass1Result p1, int mapSize, int bandRow, long big, bool ok, ulong ms)
|
||||||
|
{
|
||||||
|
var (reach, median, southCells) = Reach(p1.Regions, mapSize, bandRow);
|
||||||
|
var h = Hemi(p1.Regions, big);
|
||||||
|
return new Row
|
||||||
|
{
|
||||||
|
Level = level, Stretch = st, Seed = seed, ReachFrac = reach, MedianCoastFrac = median,
|
||||||
|
MainlandCells = p1.Regions.Mainland.SizeCells, MainlandSouthOfBand = southCells,
|
||||||
|
SouthAll = h.southAll, SouthBig = h.southBig, NorthAll = h.northAll, NorthBig = h.northBig,
|
||||||
|
SMin = h.sMin, SMed = h.sMed, SMean = h.sMean, SMax = h.sMax, NMax = h.nMax, SHist = h.sHist, Ok = ok, Ms = ms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the curve --------------------------------------------------------
|
||||||
|
|
||||||
|
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
|
||||||
|
{
|
||||||
|
var rawPool = new LandHistogram(sea);
|
||||||
|
var pass1 = new Dictionary<int, Pass1Result>();
|
||||||
|
foreach (int s in CalibrationSeeds)
|
||||||
|
{
|
||||||
|
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // bare default: offshore / revert / stretch OFF
|
||||||
|
pass1[s] = p1;
|
||||||
|
rawPool.Accumulate(p1.Height, calibSize);
|
||||||
|
}
|
||||||
|
var knots = new CurveKnots(2, "v2_balanced",
|
||||||
|
rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]),
|
||||||
|
rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]),
|
||||||
|
rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5]));
|
||||||
|
float ceilingRaw = knots.K2;
|
||||||
|
var rawAbove = new LandHistogram(sea);
|
||||||
|
var outAbove = new LandHistogram(sea);
|
||||||
|
foreach (int s in CalibrationSeeds)
|
||||||
|
{
|
||||||
|
var scfg = new TerrainGenConfig
|
||||||
|
{
|
||||||
|
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
|
||||||
|
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
|
||||||
|
};
|
||||||
|
Pass2Result st = Shaping.Shape(pass1[s], scfg);
|
||||||
|
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
|
||||||
|
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);
|
||||||
|
}
|
||||||
|
var pcts = ClimbCalibration.DefaultPercentiles;
|
||||||
|
var rawQ = new float[pcts.Length]; var outQ = new float[pcts.Length];
|
||||||
|
for (int i = 0; i < pcts.Length; i++) { rawQ[i] = rawAbove.Quantile(pcts[i]); outQ[i] = outAbove.Quantile(pcts[i]); }
|
||||||
|
var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw,
|
||||||
|
HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors),
|
||||||
|
anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f);
|
||||||
|
return (knots, cal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- output -----------------------------------------------------------
|
||||||
|
|
||||||
|
private static void WriteField(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw, float stretch)
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||||||
|
if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32"));
|
||||||
|
var look = new LookConfig
|
||||||
|
{
|
||||||
|
Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven,
|
||||||
|
ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f, HillshadeStrength = 0.30f, SeaLevel = sea,
|
||||||
|
};
|
||||||
|
Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
|
||||||
|
LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, $"{p2.VariantLabel.ToUpperInvariant()} ({stretch:G3}) {p2.Seed}")
|
||||||
|
.SavePng(Path.Combine(dir, "relief.png"));
|
||||||
|
RegionOverlayRenderer.SavePng(p1.Regions, null, p1.MapSize, 0, 0, Path.Combine(dir, "regions.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HistRow(int[] h)
|
||||||
|
{
|
||||||
|
if (h == null) return "—";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < h.Length; i++) { if (i > 0) sb.Append(" · "); sb.Append(h[i]); }
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TableMarkdown(float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var histHead = new StringBuilder();
|
||||||
|
for (int i = 0; i <= RegionLabeling.HistogramEdges.Length; i++) { if (i > 0) histHead.Append(" · "); histHead.Append(RegionLabeling.HistogramLabel(i)); }
|
||||||
|
sb.AppendLine($"| Level | stretch | Seed | reach (southernmost mainland row / N) | median coast / N | mainland cells south of band | **SOUTH islands: all / ≥ {big:N0} cells** | **S size min / med / mean / max** | S histogram ({histHead}) | **NORTH islands (control)** | N largest | oracle |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|");
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var b = baseline[seed];
|
||||||
|
sb.AppendLine($"| *baseline* | 0 | `{seed}` | {b.ReachFrac:F3} | {b.MedianCoastFrac:F3} | {b.MainlandSouthOfBand:N0} | **{b.SouthAll} / {b.SouthBig}** | **{b.SMin} / {b.SMed} / {b.SMean:F0} / {b.SMax}** | {HistRow(b.SHist)} | **{b.NorthAll}** | {b.NMax} | — |");
|
||||||
|
foreach (var r in rows)
|
||||||
|
{
|
||||||
|
if (r.Seed != seed) continue;
|
||||||
|
sb.AppendLine($"| `stretch_{r.Level}` | {r.Stretch:G3} | `{seed}` | {r.ReachFrac:F3} | {r.MedianCoastFrac:F3} | {r.MainlandSouthOfBand:N0} | **{r.SouthAll} / {r.SouthBig}** | **{r.SMin} / {r.SMed} / {r.SMean:F0} / {r.SMax}** | {HistRow(r.SHist)} | **{r.NorthAll}**{(r.NorthAll != b.NorthAll ? " ⚠ MOVED" : "")} | {r.NMax} | {(r.Ok ? "pass" : "**FAIL**")} |");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteTable(string batchRoot, int mapSize, float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big,
|
||||||
|
float bandStart, float bandFeather, bool stretchSinker)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"# The hemisphere-split count/size table — {ladder.Length} stretch levels × {seeds.Length} seeds at {mapSize}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Band start {bandStart:F3} (row {(int)(bandStart * mapSize)}), feather {bandFeather:F3} — FIXED. Sinker {(stretchSinker ? "rides the stretched distance" : "on the real y")}.");
|
||||||
|
sb.AppendLine("Islands = non-mainland 8-connected land components of the classify field (region layer); hemisphere by centroid. Offshore OFF, speck revert OFF.");
|
||||||
|
sb.AppendLine("SOUTH = the fragmentation signal; NORTH = the should-stay-flat control (flagged if it moves).");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big));
|
||||||
|
WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString());
|
||||||
|
|
||||||
|
var csv = new StringBuilder();
|
||||||
|
csv.AppendLine("level,stretch,seed,reach,median_coast,mainland_cells,mainland_south_of_band,south_all,south_big,s_min,s_median,s_mean,s_max,s_hist,north_all,north_big,n_max,oracle,ms");
|
||||||
|
var ic = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var all = new List<Row> { baseline[seed] }; all.AddRange(rows.FindAll(r => r.Seed == seed));
|
||||||
|
foreach (var r in all)
|
||||||
|
csv.AppendLine(string.Join(",", r.Level, r.Stretch.ToString("G5", ic), r.Seed, r.ReachFrac.ToString("F4", ic), r.MedianCoastFrac.ToString("F4", ic), r.MainlandCells, r.MainlandSouthOfBand,
|
||||||
|
r.SouthAll, r.SouthBig, r.SMin, r.SMed, r.SMean.ToString("F1", ic), r.SMax, "\"" + HistRow(r.SHist) + "\"", r.NorthAll, r.NorthBig, r.NMax, r.Ok ? "pass" : "FAIL", r.Ms));
|
||||||
|
}
|
||||||
|
WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteIndex(string batchRoot, int mapSize, int calibSize, float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big,
|
||||||
|
float bandStart, float bandFeather, bool stretchSinker, List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perField, bool allOk, string diagSummary)
|
||||||
|
{
|
||||||
|
int midLevel = ladder.Length / 2 + 1;
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# Batch 08 — southern stretch, EXPLORATION: the fragmentation knob space");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**A ladder, not a setting.** Inside a FIXED feathered latitude band the falloff's southward distance is compressed");
|
||||||
|
sb.AppendLine("(`y' = yB + (y − yB) / (1 + stretch · ramp)`), so the mainland reaches further south with the elevation of the rows it came");
|
||||||
|
sb.AppendLine("from, and where the stretched thin edge thins below sea it fragments organically. North of the band the classify field is");
|
||||||
|
sb.AppendLine("bit-locked in both directions (asserted). Nothing is stamped. The region layer is the instrument: SOUTH island count/size");
|
||||||
|
sb.AppendLine("is the fragmentation signal, NORTH is the should-stay-flat control.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⭐ Open this first");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"1. **`{seeds[0]}_stretch_{midLevel}/regions.png`** — the middle of the ladder on the first seed: grey mainland, each island its own colour.");
|
||||||
|
sb.AppendLine($"2. Walk the ladder on that seed: `{seeds[0]}_stretch_1/` … `_stretch_{ladder.Length}/` (`regions.png` beside `relief.png`).");
|
||||||
|
sb.AppendLine($"3. Then the same five on `{seeds[1]}` — what repeats is the knob; what does not is the seed.");
|
||||||
|
sb.AppendLine("4. Then the table: southern count/size down the rows, the northern control beside it.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The fixed frame and the axis");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"- **Band (constant for the batch):** start `{bandStart:F3}` of the map (row {(int)(bandStart * mapSize)} at {mapSize}), feather `{bandFeather:F3}` (smoothstep). Sea identity is hard above it; ramped across; extended below.");
|
||||||
|
sb.AppendLine($"- **Sinker:** {(stretchSinker ? "rides the stretched distance (pushed out with the geometry — held back inside the band)" : "on the real y (keeps pulling the extended mass down where it always did)")}.");
|
||||||
|
sb.AppendLine($"- **The axis — stretch strength:** {string.Join(" · ", Array.ConvertAll(ladder, v => v.ToString("G3")))} (levels 1–{ladder.Length}); baseline 0 measured for the control.");
|
||||||
|
sb.AppendLine($"- Every field: pass 1 + stretch, region labeling ON, offshore OFF, shelf OFF, speck revert OFF. \"big\" island = ≥ {big:N0} cells at {mapSize} (the 07 `threshold_mid`).");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"## ⭐ The hemisphere-split count/size table — {ladder.Length} levels × {seeds.Length} seeds at {mapSize}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big));
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The diagnosis (summary — full tables in `scratch/southern_diagnosis.md`)");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(diagSummary);
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("`ProvisionalEven`, flagged. The individually-coloured scheme is only the `regions.png` overlay.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The oracle (asymmetric)");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Regressions (stretch OFF bit-identical everywhere; stretch ON at the ladder's top bit-identical NORTH OF THE BAND to the `terrain-curve-v1` dumps):");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
|
||||||
|
sb.AppendLine("Per field (north bit-locked classify p / render p2 · northern islands invariant q · centre-is-land m · tag/coastline k · classify b · determinism o):");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(ShapingOracle.ToMarkdownTable(perField));
|
||||||
|
sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : "⚠⚠ FAILURES — do not judge this batch")}**");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## Disposability");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Artifact | Keep? |");
|
||||||
|
sb.AppendLine("|---|---|");
|
||||||
|
sb.AppendLine("| `regions.png`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv`, `scratch/southern_diagnosis.md` | **keep** |");
|
||||||
|
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |");
|
||||||
|
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Plates at {mapSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}.");
|
||||||
|
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteText(string path, string text)
|
||||||
|
{
|
||||||
|
using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write);
|
||||||
|
if (f == null) { GD.PrintErr($"could not write {path}"); return; }
|
||||||
|
f.StoreString(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- env helpers --------------------------------------------------------
|
||||||
|
|
||||||
|
private static string EnvStr(string k, string fallback)
|
||||||
|
{
|
||||||
|
string v = System.Environment.GetEnvironmentVariable(k);
|
||||||
|
return string.IsNullOrWhiteSpace(v) ? fallback : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnvInt(string k, int fallback)
|
||||||
|
=> int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
|
||||||
|
|
||||||
|
private static float EnvFloat(string k, float fallback)
|
||||||
|
=> float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback;
|
||||||
|
|
||||||
|
private static float[] EnvFloats(string k, float[] fallback)
|
||||||
|
{
|
||||||
|
string v = EnvStr(k, null);
|
||||||
|
if (v == null) return fallback;
|
||||||
|
var outp = new List<float>();
|
||||||
|
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
if (float.TryParse(part.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float f)) outp.Add(f);
|
||||||
|
return outp.Count > 0 ? outp.ToArray() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] EnvSeeds(string k, int[] fallback)
|
||||||
|
{
|
||||||
|
string v = EnvStr(k, null);
|
||||||
|
if (v == null) return fallback;
|
||||||
|
var outp = new List<int>();
|
||||||
|
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
|
||||||
|
return outp.Count > 0 ? outp.ToArray() : fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/SouthernStretchTool.cs.uid
Normal file
1
Tools/Scripts/SouthernStretchTool.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cbo4e7he3x7o3
|
||||||
|
|
@ -300,6 +300,34 @@ namespace IslaApocalypse.Tools
|
||||||
/// <summary>The revert threshold, as a fraction of the map's AREA (scale-free). → <see cref="RegionPass.ThresholdMidFrac"/>.</summary>
|
/// <summary>The revert threshold, as a fraction of the map's AREA (scale-free). → <see cref="RegionPass.ThresholdMidFrac"/>.</summary>
|
||||||
public float MinLandComponentFrac = RegionPass.ThresholdMidFrac;
|
public float MinLandComponentFrac = RegionPass.ThresholdMidFrac;
|
||||||
|
|
||||||
|
// ---- PASS 1 — THE SOUTHERN STRETCH (chat2/08, exploration) ----------------
|
||||||
|
//
|
||||||
|
// ⚠ THE ONE DELIBERATE RELAXATION OF SEA IDENTITY — and only below a FIXED latitude band.
|
||||||
|
// Inside the band (feathered, keyed off a fixed y, never distance-from-coast) the falloff's
|
||||||
|
// southward distance is compressed: y' = yB + (y − yB) / (1 + stretch · ramp). The mask
|
||||||
|
// geometry is stretched south; the base noise, edge noise and latitude field are NOT — so
|
||||||
|
// the extended mass keeps the elevation/relief of the rows it came from (preserve height
|
||||||
|
// as the mass extends), and where the stretched thin edge thins below sea it fragments
|
||||||
|
// organically. Cells north of the band take the UNTOUCHED code path, so the classify field
|
||||||
|
// there is bit-identical by construction (asserted). Nothing is stamped.
|
||||||
|
|
||||||
|
/// <summary>⭐ THE SWEPT AXIS. 0 = off (bit-identical to the unstretched field everywhere). Stretch factor inside the band: 1 ⇒ the southward distance is halved, 3 ⇒ quartered.</summary>
|
||||||
|
public float SouthStretch = 0f;
|
||||||
|
|
||||||
|
/// <summary>The band's FIXED latitude line, fraction of the map (y runs south). Sea identity is hard above it. A constant for a whole batch.</summary>
|
||||||
|
public float SouthBandStartFrac = SouthernStretch.DefaultBandStartFrac;
|
||||||
|
|
||||||
|
/// <summary>The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map. A constant for a whole batch.</summary>
|
||||||
|
public float SouthBandFeatherFrac = SouthernStretch.DefaultBandFeatherFrac;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does the SOUTHERN SINKER ride the stretched distance (true — it is part of the southern
|
||||||
|
/// geometry and is pushed out with it, i.e. held back inside the band) or the real y (false —
|
||||||
|
/// it keeps pulling the extended mass down where it always did)? The chat2/08 diagnostic
|
||||||
|
/// measured both; → <see cref="SouthernStretch"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool StretchSinker = SouthernStretch.DefaultStretchSinker;
|
||||||
|
|
||||||
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
|
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
|
||||||
public string VariantLabel = "full";
|
public string VariantLabel = "full";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,11 @@ namespace IslaApocalypse.Tools
|
||||||
float hMax = float.MinValue;
|
float hMax = float.MinValue;
|
||||||
float hMin = float.MaxValue;
|
float hMin = float.MaxValue;
|
||||||
|
|
||||||
|
// ═══ THE SOUTHERN STRETCH (chat2/08) — band constants, precomputed ═══
|
||||||
|
bool stretchOn = cfg.SouthStretch > 0f;
|
||||||
|
float bandStart = cfg.SouthBandStartFrac * mapSize;
|
||||||
|
float bandFeather = Mathf.Max(1f, cfg.SouthBandFeatherFrac * mapSize);
|
||||||
|
|
||||||
// ⚠ x IS THE OUTER LOOP, as in the reference. Numerically irrelevant here, but a
|
// ⚠ x IS THE OUTER LOOP, as in the reference. Numerically irrelevant here, but a
|
||||||
// PARALLEL port must reduce hMax/hMin rather than share them — noted before someone
|
// PARALLEL port must reduce hMax/hMin rather than share them — noted before someone
|
||||||
// reaches for Parallel.For and quietly races on the running max.
|
// reaches for Parallel.For and quietly races on the running max.
|
||||||
|
|
@ -175,6 +180,21 @@ namespace IslaApocalypse.Tools
|
||||||
lat += (latNoise * LatitudeWobbleSpan) - (LatitudeWobbleSpan / 2.0f);
|
lat += (latNoise * LatitudeWobbleSpan) - (LatitudeWobbleSpan / 2.0f);
|
||||||
latitudeField[x, y] = lat;
|
latitudeField[x, y] = lat;
|
||||||
|
|
||||||
|
// ═══ THE SOUTHERN STRETCH (chat2/08) — the y the MASK GEOMETRY sees ═══
|
||||||
|
//
|
||||||
|
// North of the band fy == y exactly and every expression below is the untouched
|
||||||
|
// original, so the classify field there is bit-identical by construction. Inside
|
||||||
|
// the band the southward distance is compressed by (1 + stretch · ramp); the base
|
||||||
|
// noise, edge noise and latitude field keep the real y. → SouthernStretch.
|
||||||
|
float fy = y; // the mask's y (squircle + ellipse)
|
||||||
|
float sy = y; // the sinker's y
|
||||||
|
if (stretchOn && y > bandStart)
|
||||||
|
{
|
||||||
|
float stretched = SouthernStretch.StretchedY(y, bandStart, bandFeather, cfg.SouthStretch);
|
||||||
|
fy = stretched;
|
||||||
|
if (cfg.StretchSinker) sy = stretched;
|
||||||
|
}
|
||||||
|
|
||||||
// ═══ 2. THE ISLAND FALLOFF / MASK (ref ~:567-574) ═══
|
// ═══ 2. THE ISLAND FALLOFF / MASK (ref ~:567-574) ═══
|
||||||
float finalFalloff = 0f;
|
float finalFalloff = 0f;
|
||||||
float squircleFalloff = 0f;
|
float squircleFalloff = 0f;
|
||||||
|
|
@ -184,11 +204,11 @@ namespace IslaApocalypse.Tools
|
||||||
// Squircle — Max(nx, ny), giving squared-off corners. ISLAND-anchored:
|
// Squircle — Max(nx, ny), giving squared-off corners. ISLAND-anchored:
|
||||||
// the axis ratios are applied here.
|
// the axis ratios are applied here.
|
||||||
float nx = Mathf.Abs(x - centerX) / (halfSpan * axisX);
|
float nx = Mathf.Abs(x - centerX) / (halfSpan * axisX);
|
||||||
float ny = Mathf.Abs(y - centerY) / (halfSpan * axisY);
|
float ny = Mathf.Abs(fy - centerY) / (halfSpan * axisY);
|
||||||
squircleFalloff = Mathf.Max(nx, ny);
|
squircleFalloff = Mathf.Max(nx, ny);
|
||||||
|
|
||||||
// Ellipse — vector length, giving a rounded shape.
|
// Ellipse — vector length, giving a rounded shape.
|
||||||
var ellipticalPos = new Vector2((x - centerX) / axisX, (y - centerY) / axisY);
|
var ellipticalPos = new Vector2((x - centerX) / axisX, (fy - centerY) / axisY);
|
||||||
float ellipticalFalloff = ellipticalPos.Length() / (mapSize / EllipseScaleDivisor);
|
float ellipticalFalloff = ellipticalPos.Length() / (mapSize / EllipseScaleDivisor);
|
||||||
|
|
||||||
// 50/50 blend.
|
// 50/50 blend.
|
||||||
|
|
@ -217,9 +237,9 @@ namespace IslaApocalypse.Tools
|
||||||
// Sinks the stretched land bridges in the bottom 25%. ⚠ BEFORE the power, so its
|
// Sinks the stretched land bridges in the bottom 25%. ⚠ BEFORE the power, so its
|
||||||
// effect is superlinear — +0.6 on a falloff already near 1 costs far more height
|
// effect is superlinear — +0.6 on a falloff already near 1 costs far more height
|
||||||
// than +0.6 near 0. Moving it after the power would change the southern coast.
|
// than +0.6 near 0. Moving it after the power would change the southern coast.
|
||||||
if (cfg.IslandFalloff && cfg.SouthernSinker && y > southThreshold)
|
if (cfg.IslandFalloff && cfg.SouthernSinker && sy > southThreshold)
|
||||||
{
|
{
|
||||||
float southDepth = (y - southThreshold) / (mapSize - southThreshold);
|
float southDepth = (sy - southThreshold) / (mapSize - southThreshold);
|
||||||
finalFalloff += southDepth * SouthSinkAmount;
|
finalFalloff += southDepth * SouthSinkAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue