islaApocalypse-v2/Tools/Scripts/TerrainGenConfig.cs
beezm bddd3636dd chat2/09: coastal fragmentation — a perimeter-wide, band-limited noise, amplitude laddered at a fixed stretch
A dedicated term, not the edge noise scaled: pass 1's edge noise is positive-only, squircle-
modulated, 2.5x the base frequency - the coastline's jitter; scaling it would roughen the whole
rim and bias the coast inward. CoastalFragment adds, to the PRE-power falloff, amp * window(f) *
noise(x + off, y + off) with its own deterministic field (seed offset 9109, coordinate offset
0.37 map widths - D-059), zero-mean, at 12 periods per map width (the lobe/neck scale, chosen by
probe against 20 and 32: 20 climbs into smalls, 32 is fuzz), weighted by a smooth window on the
falloff value itself, centred 0.66 (the coast's falloff for a median base noise, from the 08
diagnosis), half-width 0.18, EXACTLY ZERO beyond - so the interior, the massif and the deep sea
are bit-identical by construction. Thin necks of barely-land flip first; nothing is detected,
nothing is stamped. FragmentBitesOnly is exposed (probed: it erodes the coast inward - mainland
-3/-8/-14 % at 0.06/0.15/0.30 - rather than detaching pieces; off by default).

Batch: BatchRoot(9, "coastal_fragment") - 4 amplitudes (0.06, 0.15, 0.30, 0.50) x task 08's two
seeds at 4096, stretch fixed at 2 (08's stretch_3 rung, whose dump is the bit-identical baseline,
a8), sinker untouched, speck revert at < 4 cells, offshore/shelf off; lean render (regions overlay
+ relief + .f32), the hemisphere-split count/size table with largest-three and histograms.

Oracle, all passing: a1 (the curve untouched), a8 x2 (frag OFF at stretch 2 == the 08 field,
16.8 M cells each), interior locked (r: every cell clear of the window bit-identical, 8/8),
high-ground report (s, informational), centre is land, tag/coastline, classify == raw, ids and
heights deterministic.

The read: both hemispheres fragment; the north more readily (N 10 -> 28, 13 -> 40 islands across
the ladder; S 4 -> 31, 14 -> 28) and the south's big stretch pieces are the zero-mean noise's
other edge: on 999999937 the 83k-cell southern fragment is BRIDGED back onto the mainland at amp
0.50 (mainland +183k). The working range is amp 0.15-0.30; 0.50 is the bookend. Graduation held.

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

384 lines
20 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();
// ---- PASS 1c — region labeling + the speck revert (chat2/07) ----------
/// <summary>
/// ⭐ THE REGION-LABELING LAYER (<c>Core.RegionLabeling</c>, via <c>RegionPass</c>): 8-connected
/// land components on the classify field, mainland = the centre component, the island tag BY
/// CONSTRUCTION. Pure analysis — it changes no height — so it is ON by default without touching
/// any regression anchor. Off ⇒ no tag, no region table (the tag arrays are null).
/// </summary>
public bool RegionLabeling = true;
/// <summary>
/// ⭐ THE SPECK REVERT — lower every non-mainland land component smaller than
/// <see cref="MinLandComponentFrac"/> of the map to seabed. Origin-blind; lower-only and
/// component-only, asserted; mainland never a candidate.
///
/// ⚠ DEFAULT OFF IN THE BARE CONFIG, for exactly the reason the shelf and the islets are: the
/// raw field has small natural nubs, so with this ON the calibration pool's land histogram, the
/// curve knots and every Phase-1 / task-03 / task-04 regression dump would move at once.
/// The region batch turns it on explicitly (its preset is ON); flipping the bare default is
/// the act that re-baselines the oracles — own task, not a side effect.
/// </summary>
public bool SpeckRevert = false;
/// <summary>The revert threshold, as a fraction of the map's AREA (scale-free). → <see cref="RegionPass.ThresholdMidFrac"/>.</summary>
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;
// ---- PASS 1 — COASTAL FRAGMENTATION (chat2/09, exploration) ------------------
//
// A band-limited, zero-mean noise added to the PRE-power falloff only where the falloff sits in
// the coastal window (≈ the barely-land / barely-sea margin, around the whole perimeter). It
// self-targets thin necks: the cells closest to the sea threshold flip first, so lobes pinch off
// into islands while the interior — window weight exactly zero — is bit-identical by
// construction. Nothing is detected, nothing is stamped. → CoastalFragment.
/// <summary>⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the window's centre.</summary>
public float FragmentAmp = 0f;
/// <summary>The fragmentation noise's frequency, periods per map width — the neck/lobe scale. The secondary dial (fixed this round). → <see cref="CoastalFragment.DefaultFreqPerMapWidth"/>.</summary>
public float FragmentFreqPerMapWidth = CoastalFragment.DefaultFreqPerMapWidth;
/// <summary>The coastal window's centre and half-width in PRE-power falloff units. Weight 1 at the centre, smooth to 0 at ± half-width; exactly 0 beyond.</summary>
public float FragmentBandCentre = CoastalFragment.DefaultBandCentre;
public float FragmentBandHalfWidth = CoastalFragment.DefaultBandHalfWidth;
/// <summary>
/// false (default) ⇒ zero-mean noise: the margin is redrawn — bites AND builds (which can also
/// bridge an island back onto the mainland). true ⇒ bites only ((noise+1)/2 ≥ 0): land can only
/// recede, necks are cut, nothing is bridged, the coast net-recedes. → <see cref="CoastalFragment"/>.
/// </summary>
public bool FragmentBitesOnly = CoastalFragment.DefaultBitesOnly;
/// <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)}]";
}
}