islaApocalypse-v2/Tools/Scripts/TerrainGenConfig.cs
beezm e8571b2778 chat2/06: offshore islands — revert the forced floor, tune the organic layer for coverage
The seeded floor from chat2/05 — FloorNorth/FloorSouth, the PCG32 placement, StampWeight,
StampRadiusFrac/StampCoreFrac/StampEdgeJitter, the centre separation and land gap, the
floor self-check and oracle (h), the centre rings on the tag overlay — is reverted out
whole, as a forward commit. It looked stamped. It is one checkout away at 3b96e06. What
remains is ONE island mechanism: the organic noise-field layer, with chat2/05 stage 2's
shape untouched (freq 16, crest 24 m pre-curve, core 0.25, sharpness 2.5, corners on,
moat + falloff test + outer bound unmoved), now OffshoreSettings.Organic(). The count per
hemisphere is a statistical outcome of the tuning, read off a table, never guaranteed.

The south was measured before anything moved (OffshoreDiagnosis, over task 01's pool at
2048): the premise that the south generates fewer islands is not what the field says.
The NORTH has 1.75x less island-eligible ocean (56 % of its ocean is zone vs 69 %; 233
zone rows per column vs 407) and under half the island candidates (1,183 over-threshold
peaks in zone vs 2,422). Per cell the south is richer too (484 vs 414 candidates per
Mcell). The gate that loses candidates is the moat in the north and the falloff test in
the south — the bulging south coast pushes the 0.72 contour out — but neither binds
enough to suppress it. So the fix is density, and the binding constraint for "a couple
north, consistently" is the north.

The knobs: Density (the main one, the calibration quantile) and SouthWeight (south density
= Density x weight — applied to the quantile, so a south island looks exactly like a north
one; there are just more). Three levels batched on 12 seeds at 4096, SouthWeight 1.25:
0.016 N 2/4.4/10 S 8/13.8/21 (every seed clears N >= 2, S >= 3, but N's minimum IS 2),
0.022 N 3/6.3/12 S 15/19.9/29 (the preset — the first level whose north minimum sits
comfortably above the target), 0.030 N 3/7.8/15 S 19/25.2/33 (the too-many bookend). The
south weight is the developer's stated preference, not a fix; the SouthWeight 1.0 tables
(S 6/10.9/16 at 0.022) are in the batch's scratch/ for the comparison.

The guards, so more density does not buy slop, each a revert by component membership:
specks (< 3e-5 of the map's area, ~500 cells at 4096 — 70-100 per field at these
densities, the noise caps the reference also surfaced), clusters (the smaller of two
islands whose shores are within 0.8 % of the map width — 0-7 per field), blobs (> 6e-4 —
never bit). And the reference's submerged humps, now attributed by HUMP rather than by
bounding box: the first 4096 plate probe showed hollow ghost rings beside islands — a
guard-dropped island's cap went back to seabed but its rim survived inside its
neighbour's bbox keep-region. A hump (one connected raised region) now stays only if it
holds a kept island, and a dropped island's own cap goes even inside a kept hump.

Oracle, all passing: a1 (Phase-1 dump), a3 (task-03 dump), a4 NEW — offshore OFF at 8192
bit-identical to the terrain-curve-v1 tag's own 04 gallery dump over 67,108,864 cells —
j0 shelf inert on land, and per field moat / mainland unmoved / tag-coastline / HMaxSeed
(unchanged, 1.289169) / classify on all 36 table fields and 4 plates. Batch:
BatchRoot(6, "offshore_organic_tune") — exactly 4 plates (three densities on 1063685222,
density_mid on 424242, the table's sparsest south) + count_table.md/.csv + diagnosis.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
2026-08-21 06:22:54 -04:00

305 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// Which redistribution curve pass 2a applies (chat2/02). One seam, three occupants:
///
/// Staircase ⭐ the faithful v5 port (task 01) — toe/red/riser/bench/riser/plateau/spike.
/// THE CONTROL. Bit-identical to task 01's output, always present in a batch.
/// Continuous ⭐ the rev-3 redesign — the staircase's toe+red lowland PRESERVED bit-for-bit,
/// everything above it replaced by one smooth monotone climb to the 420 m cap.
/// → Core/ContinuousCurve.
/// LiftedWrong ⚠⚠ DELIBERATELY THE WRONG DIRECTION — an even linear remap of ALL land onto
/// [SEA, PEAK_CAP], which lifts the entire island off its shoreline and destroys
/// the low plain the developer likes. It exists as a CONTRAST BOOKEND so the
/// preserved-lowland variants can be judged against the mistake, and for no other
/// purpose. Do not ship it, do not tune it, do not "fix" it.
/// </summary>
public enum CurveModeKind { Staircase, Continuous, LiftedWrong }
/// <summary>
/// The generator's configuration, including the per-element ABLATION TOGGLES.
///
/// ═══ WHY EVERY PASS-1 ELEMENT IS GATED ═══
///
/// This is the standing A/B discipline: every shaping change ships behind a gate with the
/// previous behaviour surviving on the other side, so changes stay comparable as a pair rather
/// than as a memory of last week's render, and a rejected change costs a flipped default rather
/// than a reverted commit. → `Design - Tooling - Iteration and Batching.md`.
///
/// For THIS task the gates do a second job: they are the PORT-FIDELITY CHECK. Generating
/// base-noise-only, then adding one element at a time, shows each ported element doing what the
/// reference's did — rather than judging six simultaneous changes by their sum.
///
/// ⚠ Lives in Tools/, not Core/. It configures the generator specifically; Core carries the
/// world's data contracts and the scaling rule, not a tool's dials.
/// </summary>
public sealed class TerrainGenConfig
{
// ---- world ----------------------------------------------------------
/// <summary>
/// Map side in columns. A GENERATION PARAMETER — never baked in.
/// Iteration runs small (20484096); the shape is scale-invariant by construction, so the
/// island reads the same at any size and a full-size pass is confirmation, not iteration.
/// </summary>
public int MapSize = 2048;
/// <summary>
/// The noise seed. POSITIVE ONLY. Zero or negative means "pick one and print it" — a run
/// whose seed is not recorded is a run that cannot be reproduced, so the resolved seed is
/// always printed and always in the filename.
/// </summary>
public int Seed = 0;
// ---- island shape (reference ConfigManager defaults, READ from source) ----
/// <summary>Falloff X axis ratio. Reference: <c>ConfigManager.LEGACY_AXIS_X = 1.15f</c>.</summary>
public float IslandAxisX = 1.15f;
/// <summary>Falloff Y axis ratio. Reference: <c>ConfigManager.LEGACY_AXIS_Y = 0.90f</c>.</summary>
public float IslandAxisY = 0.90f;
/// <summary>
/// Multiplier on the falloff term in the combine.
/// Reference: <c>[Export] public float FalloffStrength = 1.0f;</c> (MapGenerator ~:11),
/// and NOT overridden in MapPreview.tscn — so 1.0f is the value the island was tuned at.
/// </summary>
public float FalloffStrength = 1.0f;
/// <summary>
/// The flat sea level, in raw height units. Reference: <c>ConfigManager.SeaLevelValue = 0.15f</c>
/// with <c>SeaLevelModel = "flat"</c>, so <c>GetSeaLevel</c> ignores latitude entirely.
///
/// ⚠ PHASE 1 USES THIS AS A VISUALIZATION THRESHOLD ONLY — the boundary between the
/// bathymetric and hypsometric colour ramps. No water is modelled, no water bodies are
/// identified, nothing floods. That is Phase 2.
/// </summary>
public float SeaLevel = 0.15f;
// ---- ABLATION TOGGLES — the pass-1 ladder ---------------------------
/// <summary>Rung 1: the base terrain noise, <c>(noise(x,y)+1)/2</c>. Off = flat zero.</summary>
public bool BaseNoise = true;
/// <summary>
/// Rung 2: the island falloff/mask — squircle + ellipse blend, and the <c>Pow(·, 2.5f)</c>.
/// ⚠ Off also disables rungs 35 in effect: edge noise, the sinker and the Trench are all
/// modifiers OF the falloff, so with no falloff there is nothing for them to modify. The
/// toggles stay independent so the ladder reads honestly; the report says so.
/// </summary>
public bool IslandFalloff = true;
/// <summary>Rung 3: coastline edge roughness, modulated by the squircle.</summary>
public bool EdgeNoise = true;
/// <summary>Rung 4: the southern sinker — extra sinking pressure in the bottom 25%.</summary>
public bool SouthernSinker = true;
/// <summary>Rung 5: the Trench — the map-anchored outer-band wall that guarantees an ocean border.</summary>
public bool Trench = true;
/// <summary>Rung 6: the mountain spine up the centre-X axis.</summary>
public bool MountainSpine = true;
// ---- PASS 2a — the redistribution curve and shelf detail (Phase 2) ----
//
// ⚠ THE PRIMARY A/B OF THIS PHASE IS `Curve`. Off must reproduce Phase 1's pass-1 output
// BIT-IDENTICALLY — that is the regression oracle, not a figure of speech.
/// <summary>
/// ⭐ Pass 2a rung 1: the height-redistribution curve. → <see cref="HeightCurve"/>.
/// Off = raw pass-1 height, unshaped (the control half of every A/B in this phase).
/// </summary>
public bool Curve = true;
/// <summary>
/// ⭐ WHICH curve (chat2/02). → <see cref="CurveModeKind"/>.
///
/// Default CONTINUOUS per the rev-3 task — the exploration direction. Tools that exist to
/// reproduce the task-01 staircase (CurveBaselineTool) set Staircase EXPLICITLY, so the
/// default changing does not silently move a control batch.
/// </summary>
public CurveModeKind CurveMode = CurveModeKind.Continuous;
// ---- the continuous climb's knobs — ALL act above the lowland ceiling only ----
/// <summary>
/// How high the preserved lowland holds before the climb takes over, in METRES of output
/// height above sea. Default 30 = RED_CEIL, the flood line — the exact top of the
/// staircase's toe+red band, so nothing at all is re-mapped below it.
///
/// ⚠ Constrained to [30, 80] m by <see cref="ContinuousCurve.Build"/>: below 30 would cut
/// the preserved band; at ~100 it could preserve a flat bench, the artifact this mode
/// exists to remove. Raising it extends the red band's gentle grade linearly before the
/// climb begins. THE PRIMARY VARIANT AXIS.
/// </summary>
public float LowlandCeilingM = 30f;
/// <summary>
/// The shape of the climb's departure from the lowland, 0..1: how long it hugs the red
/// band's exit slope before steepening. Replaces the old ambiguous "bow". Acts only above
/// the ceiling; CANNOT touch the low band.
/// </summary>
public float ClimbFeather = 0.4f;
/// <summary>
/// ⚠ chat2/02's ANALYTIC summit knob — SUPERSEDED by <see cref="PeakSharpness"/>.
///
/// It steepened the peak by pulling the summit ONSET DOWN, which lowered the whole mid-massif
/// with it: at 4.5 the p99 land height collapsed from 199 m to 121 m. A bad trade, and the
/// bug chat2/03 §3 exists to fix. It survives ONLY so the 02 curve stays reproducible as a
/// contrast variant; it is read only when <see cref="ClimbCalibration"/> is null.
/// </summary>
public float SummitDrama = 2.5f;
// ---- chat2/03: the CALIBRATED climb ---------------------------------
/// <summary>
/// ⭐ The measured climb calibration. Non-null ⇒ the climb reproduces the staircase's
/// above-ceiling elevation distribution as a smooth slope. Null ⇒ chat2/02's analytic
/// feather/drama curve (kept only as the "before" contrast).
///
/// ⚠ Not a value knob — it is MEASURED, per calibration pool, by the batch tool. Two configs
/// may share one instance safely: it is immutable.
/// </summary>
public ClimbCalibration ClimbCalibration = null;
/// <summary>
/// ⭐ How big the mountain is, relative to the staircase's.
///
/// 1.0 reproduce the staircase's mountain (the default — the least-surprising baseline)
/// &gt;1 lift the mid-massif higher: more land at 150300 m
/// &lt;1 a smaller mountain, toward chat2/02's bottom-heavy climb
///
/// Applied as <c>v ← v^(1/lift)</c> on the calibrated control points: monotone, and it fixes
/// both endpoints, so it moves the massif without touching the lowland handover or the cap.
/// ⚠ It scales the CLIMB only. It cannot move a lowland cell — oracle (d) proves that.
/// </summary>
public float MountainLift = 1.0f;
/// <summary>
/// ⭐ How pointy the summit is — and, unlike <see cref="SummitDrama"/>, <b>nothing else</b>.
///
/// It reshapes only the span above the last measured percentile, leaving that percentile's
/// height fixed. Raising it therefore cannot reduce the land below the onset: peak sharpness
/// and mountain mass are independent knobs. → chat2/03 §3.
///
/// 1.0 = a straight run to the cap; higher defers the rise so the final approach steepens.
/// </summary>
public float PeakSharpness = 1.0f;
/// <summary>
/// ⭐ Pass 2a rung 2: the shelf detail passes — micro-relief skin + shelf-edge knot warp.
/// ⚠ REQUIRES <see cref="Curve"/>: the edge warp slides the CURVE's knots, so with no curve
/// there is nothing to warp. Requesting it with the curve off is a logged no-op, not an error.
/// </summary>
public bool ShelfDetail = true;
/// <summary>
/// Micro-relief amplitude, in METRES of output height. Reference default: 3 m.
/// Converted through <see cref="WorldScale"/> at the call site — never a literal /251.
/// </summary>
public float ShelfReliefAmpM = TerrainDetailPass.ReliefAmpDefaultM;
/// <summary>
/// Shelf-edge warp amplitude, in METRES OF INPUT HEIGHT (not output elevation — see
/// <see cref="TerrainDetailPass"/>). Reference default: 12 m.
///
/// ⚠ CLAMPED, LOUDLY, to the knot set's safe bound (<c>TerrainDetailPass.MaxEdgeShift</c>).
/// Monotonicity is never a tuning question; an ignored dial is always reported.
/// </summary>
public float ShelfEdgeVariationM = TerrainDetailPass.EdgeAmpDefaultM;
/// <summary>
/// The input knot set — WHERE the land distribution is cut.
/// Default: <see cref="CurveKnots.V2Baseline"/>, re-measured on v2's own pass-1 output.
/// <see cref="CurveKnots.Reference"/> is available for a fidelity A/B against the prototype's.
/// </summary>
public CurveKnots Knots = CurveKnots.V2Baseline;
/// <summary>
/// The output anchors — WHAT HEIGHT each cut lands at. Default: the storm-ladder values,
/// reproducing the reference's constants bit-for-bit.
/// </summary>
public CurveAnchors Anchors = CurveAnchors.Default;
// ---- the crater seam — INERT THIS PHASE -----------------------------
/// <summary>
/// Crater radius in columns. ⚠ <b>0 = NO CRATER, which is this phase's state.</b> The detail
/// pass's crater exclusion is ported and wired, but with no crater it evaluates to "detail
/// everywhere" and the distance is never computed. It is exercised when the carve lands.
/// </summary>
public float CraterRadius = 0f;
/// <summary>Crater centre X, columns. Unused while <see cref="CraterRadius"/> is 0.</summary>
public float CraterCenterX = 0f;
/// <summary>Crater centre Y, columns. Unused while <see cref="CraterRadius"/> is 0.</summary>
public float CraterCenterY = 0f;
// ---- PASS 1b — the coast shelf + offshore islets (chat2/05) ----------
//
// ⚠⚠ BOTH DEFAULT OFF, DELIBERATELY — and that is a decision to revisit, not an oversight.
//
// Every oracle in this phase holds pass 1 against Phase 1's `.f32` dumps (curve-off ==
// `02_pass1_port`), and the curve tools hold it against task 01/03's. The shelf changes every
// below-sea cell and the islets ADD LAND, so the moment either defaults ON, every one of
// those regression anchors goes stale at once. The batch tools that want them turn them on
// explicitly. FLIPPING THESE DEFAULTS IS THE ACT THAT RETIRES THE PHASE-1 REGRESSION DUMPS —
// do it deliberately, in a task that re-baselines the oracles, not as a side effect here.
/// <summary>
/// The submarine coast shelf (<c>IslandFalloff.CoastShelf</c>). Below-sea only,
/// depth-preserving, held strictly below sea by <c>MathF.BitDecrement</c>. Invisible until
/// water renders; ported faithfully now, judged then.
/// </summary>
public bool CoastShelf = false;
// ⚠ Fully qualified: this class's own `IslandFalloff` ablation toggle shadows the static
// type of the same name inside field initializers.
/// <summary>Shelf strength, 0 = off → 1 = a flat lagoon. Reference 0.775.</summary>
public float ShelfStrength = IslaApocalypse.Tools.IslandFalloff.SHELF_STRENGTH;
/// <summary>Metres of depth over which the shelf relaxes. Reference 100.</summary>
public float ShelfScaleM = IslaApocalypse.Tools.IslandFalloff.SHELF_SCALE_M;
/// <summary>
/// ⭐ The offshore islet system — every dial in one object. <c>Mode = Off</c> by default
/// (see the note above). <see cref="OffshoreSettings.Faithful"/> is the reference verbatim;
/// <see cref="OffshoreSettings.Organic"/> is the reshape, tuned (chat2/06).
/// </summary>
public OffshoreSettings Offshore = new OffshoreSettings();
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
public string VariantLabel = "full";
/// <summary>The scale object every distance and frequency in the generator derives from.</summary>
public GenerationScale Scale => new GenerationScale(MapSize);
/// <summary>
/// ⚠ DEEP on <see cref="Anchors"/>. <c>MemberwiseClone</c> is shallow, so two configs cloned
/// from one parent would share a single mutable anchor object and an A/B that edited one
/// would silently move the other. The one reference type that is a DIAL gets copied; the one
/// that is immutable (<see cref="CurveKnots"/>) does not need to be.
/// </summary>
public TerrainGenConfig Clone()
{
var c = (TerrainGenConfig)MemberwiseClone();
c.Anchors = Anchors?.Clone();
c.Offshore = Offshore?.Clone(); // same reason: a mutable dial object, deep-copied
return c;
}
public override string ToString() =>
$"MapSize={MapSize} Seed={Seed} axis={IslandAxisX:F2}x/{IslandAxisY:F2}y " +
$"falloffStrength={FalloffStrength:F2} sea={SeaLevel:F2} variant={VariantLabel} " +
$"[base={BaseNoise} falloff={IslandFalloff} edge={EdgeNoise} sinker={SouthernSinker} " +
$"trench={Trench} spine={MountainSpine}] " +
$"[curve={Curve} detail={ShelfDetail} relief={ShelfReliefAmpM:F1}m edge={ShelfEdgeVariationM:F1}m " +
$"knots={(Knots == null ? "-" : Knots.Name)}]";
}
}