islaApocalypse-v2/Tools/Scripts/TerrainGenConfig.cs
beezm 03fe75b378 rivers/01: re-baseline — bare defaults reproduce terrain-shape-v1 (calibration pinned family-off); retire stale oracles; namespace batch root by chat
The bare TerrainGenConfig defaults did NOT reproduce the terrain the developer
locked, so "run the default generator" was not "the terrain in the gallery" —
the single most expensive fact in the codebase, and the reason a fresh chat
would spend an afternoon chasing differences that were CONFIGURATION, not
regression. This is the deliberate task that ends that, before any river work.

A1 — the defaults ARE the locked shape now. Five fields actually move:
SpeckRevert false->true, MinLandComponentFrac 3e-5->2.5e-7 (120x smaller; the
config default would have eaten real islands, not specks), SouthStretch 0->2,
FragmentAmp 0->0.5, Erosion false->true. Seven more were already correct via
SouthernStretch.Default* / CoastalFragment.Default* and are now pinned as
literals, because TerrainShapeV1 used to do that pinning and this default set
inherits the job. CoastShelf stays OFF — the locked shape has no shelf, and
evaluating it (D-041) is its own later task once water renders. Offshore stays
Off permanently (D-063): islands are organic-only, made by the stretch +
fragmentation and identified by the region layer, never placed.

A2 — the preserve mechanism. The curve knots are percentiles of the FAMILY-OFF
land distribution; flipping the defaults would have moved the pool, the knots,
and with them the render field of every batch including terrain-shape-v1
itself. So the pool is pinned family-off (TerrainGenConfig.WithFamilyOff /
CalibrationPool) rather than the knots being baked: calibration stays live, its
INPUT distribution is held still. The pin was a no-op by construction — it sets
the values the defaults carried the instant before the flip — and re-measuring
after confirms it: pool, all six knots, per-seed spread, shaped max,
monotonicity spikeMax and all seven band shares identical.

  Applied wider than "in CalibrateCurve": OffshoreIslandsTool,
  RegionLabelingTool and SouthernStretchTool generate their own family-off
  field for the Phase-1 anchor, so the pool pin alone would NOT have covered
  them and their a1 would have failed for a configuration reason. TerrainGenTool
  too — it AUTHORED 02_pass1_port and must stay able to regenerate its own
  anchor.

  Recorded as a judged-and-parked property: knots measured family-off, applied
  family-on. Deliberate, not an oversight. Same disposition as the mid-slope
  feather.

A4 — no oracle may pass against a superseded baseline. Six anchors retired
(01/03/04/06/08/09) with their checks and ISLA_T0x_SOURCE defaults; three kept
(chat1/02_pass1_port as the family-off pass-1 guard, chat2/10 and chat2/11 as
the shape and erosion acceptance anchors). Two invariants were RE-POINTED
rather than lost — the southern stretch's north-lock and the coastal-fragment
interior-lock now compare against SAME-RUN fields, which is scale-free and
cannot be invalidated by a moved dump. The retired dumps are kept, not deleted,
and marked superseded in their INDEX.md.

  A missing anchor is now LOUD. The old pattern skipped silently, so a moved
  anchor did not make its oracle fail — it made it not RUN, and a batch with a
  skipped check prints an all-PASS table that reads like a clean one. That is
  the INVERSE of the hazard the re-baseline guards against, and the migration
  below is exactly the event that would have triggered it, on nine anchors at
  once. ShapingOracle.LoadAnchor now separates the two cases: absent -> throw;
  present at another size -> loud INCONCLUSIVE, which is a fail, never a pass.

  TerrainShapeV1 inverted from PRESET to GUARD and moved to its own file.
  Apply() is gone — stamping the values on top of the defaults would MASK a
  drift instead of catching it. Its constants are now the assertion target, and
  Assert() refuses a run whose defaults have drifted off the locked shape.

B/C — batches are namespaced by chat: batches/<chat>/NN_slug/. Task numbers
restart at 00 per chat, so a flat root collided the moment a second chat
existed — four colliding prefixes across 25 batches, separable only by slug.
ToolingPaths.ChatSlug is REQUIRED (throws if unset) and defaults per tool to
its authoring chat, so re-running reproduces a batch in place while ISLA_CHAT
redirects — which is also what stops an acceptance run from overwriting the
very anchor it checks against. Writes go through BatchRoot; historical READS
compose against BatchesRoot and so carry the prefix in their own source string
("chat1/02_pass1_port"). The 25 existing batches were migrated moves-only.

ACCEPTANCE — 16 of 16 byte-identical, 0 failed. All 8 gallery seeds at 8192
from the bare defaults are byte-identical to chat2/10_frag4_seed_gallery
(= terrain-shape-v1, a59e52f); all 8 erosion fields byte-identical to
chat2/11_erosion (= ea291ea). Every gallery table row and every erosion
statistic reproduces its recorded value exactly. DrainageTool's a11 passes
bit-identical over 67,108,864 cells, and its analysis reproduces batch 12
exactly — so the whole chain rivers depends on (shape -> erosion -> drainage)
is unchanged. All 12 edited tools re-run clean; both new guards negative-tested.

The baseline moved in DEFAULTS, not in TERRAIN.

-> XX_Human/output/rivers/01_rebaseline_and_batch_namespace.report.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
2026-08-23 04:45:10 -04:00

558 lines
29 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 STAY OFF AFTER THE rivers/01 RE-BASELINE — and each for its own reason.
//
// SHELF OFF because the LOCKED SHAPE has no shelf. `terrain-shape-v1` (a59e52f) was
// judged with `CoastShelf = false`, so turning it on here would produce terrain
// the developer never approved and would break the bit-identity this default set
// exists to guarantee. The shelf is below-sea only and invisible until water
// renders — EVALUATING IT IS ITS OWN LATER TASK (→ D-041), once water renders.
// (The rivers kickoff's "set shelf ON" was a mis-statement; corrected by the
// developer via master before rivers/01 ran.)
// ISLETS OFF because it is the DROPPED mechanism (→ D-063): islands are ORGANIC-ONLY,
// produced by the southern stretch + coastal fragmentation and then IDENTIFIED by
// the region layer — never placed. The pass is kept, not deleted, because
// `OffshoreSettings.Faithful` is a live port-fidelity control and `Pass1Result`
// carries the offshore seam. ⚠ NEVER RE-ENABLE IT IN THE BARE DEFAULT.
/// <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.
///
/// ⭐ ON BY DEFAULT since rivers/01 — it is part of the LOCKED SHAPE (`terrain-shape-v1`).
///
/// ⚠ It was default-OFF through chat 2 because turning it on moves the calibration pool's land
/// histogram, the curve knots and every pre-family regression dump at once. rivers/01 is the
/// task that owned that flip: the calibration pool is now pinned FAMILY-OFF
/// (<see cref="WithFamilyOff"/>), so the knots are unmoved and the flip is terrain-only.
/// </summary>
public bool SpeckRevert = true;
/// <summary>
/// The revert threshold, as a fraction of the map's AREA (scale-free).
///
/// ⚠⚠ THE LOCKED-SHAPE VALUE, PINNED AS A LITERAL — 2.5e-7 ≈ 4 cells at 4096, ≈ 17 at 8192.
/// It is deliberately NOT <see cref="RegionPass.ThresholdMidFrac"/> (3e-5, ~2,013 cells at
/// 8192), which was the pre-re-baseline default and is 120× larger: at that threshold the
/// revert eats real islands rather than specks. The three named `RegionPass.Threshold*Frac`
/// values are the chat2/07 exploration ladder; this is the value chat2/0910 froze and the
/// developer judged. Changing it changes `terrain-shape-v1`. (rivers/01, from
/// `FragGalleryTool.FrozenSpeckFrac`.)
/// </summary>
public float MinLandComponentFrac = 2.5e-7f;
// ---- 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.
///
/// ⭐ 2 IS THE LOCKED-SHAPE VALUE since rivers/01 (→ D-065). With coastal fragmentation it is
/// one of the two mechanisms that MAKE the islands (→ D-063).
/// </summary>
public float SouthStretch = 2f;
/// <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.
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultBandStartFrac`
/// today). The literal is the pin: retuning that constant must not silently move
/// `terrain-shape-v1`. (rivers/01.)
/// </summary>
public float SouthBandStartFrac = 0.70f;
/// <summary>
/// The feather width across which the stretch ramps 0 → 1 (smoothstep), fraction of the map.
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultBandFeatherFrac`
/// today) — same reason as <see cref="SouthBandStartFrac"/>. (rivers/01.)
/// </summary>
public float SouthBandFeatherFrac = 0.05f;
/// <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>
/// <remarks>⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultStretchSinker` today). rivers/01.</remarks>
public bool StretchSinker = true;
// ---- 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.
///
/// ⭐ 0.5 IS THE LOCKED-SHAPE VALUE since rivers/01 — chat2/09's `frag_4`, frozen by chat2/10
/// across an 8-seed gallery and tagged `terrain-shape-v1`. With the southern stretch it is one
/// of the two mechanisms that MAKE the islands (→ D-063).
/// </summary>
public float FragmentAmp = 0.5f;
/// <summary>
/// The fragmentation noise's frequency, periods per map width — the neck/lobe scale.
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultFreqPerMapWidth`
/// today); the literal is the pin. (rivers/01.)
/// </summary>
public float FragmentFreqPerMapWidth = 12f;
/// <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.
/// ⚠ Pinned as LITERALS at the locked-shape values (equal `CoastalFragment.DefaultBandCentre` /
/// `DefaultBandHalfWidth` today). (rivers/01.)
/// </summary>
public float FragmentBandCentre = 0.66f;
public float FragmentBandHalfWidth = 0.18f;
/// <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>
/// <remarks>⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultBitesOnly` today). rivers/01.</remarks>
public bool FragmentBitesOnly = false;
// ---- PASS 2b — HYDRAULIC EROSION (chat2/11) — RENDER MAP ONLY ------------------
//
// The reference's droplet erosion, ported verbatim (Core.HydraulicErosion), run on the render
// field AFTER shaping (after detail, before the crater carve — which does not exist yet). The
// classify field never sees it (D-046); the caller's flood guard proves no waterline moved.
// ⭐ DEFAULT ON since rivers/01 — the locked baseline the rivers epic routes on is the ERODED
// render field. The governors + physics are the reference ConfigManager's declared defaults,
// clamped as it clamped them (→ ErosionPass).
//
// ⚠ This flag is INERT unless a caller explicitly runs `ErosionPass.Apply` — nothing in
// `Topography.Generate` or `Shaping.Shape` reads it. So flipping it moves no field on its own;
// it makes "erode by default" the answer for the callers that DO ask.
/// <summary>
/// ⭐ Erosion on/off. RENDER MAP ONLY — the classify field never sees it (→ D-046), proven by
/// `ErosionPass`'s flood guard. Default ON since rivers/01 (see above); the erosion A/B sets
/// it to false explicitly for its OFF half.
/// </summary>
public bool Erosion = true;
/// <summary>Governor 1 — droplet count. Reference 250000, clamp [0, 50,000,000].</summary>
public int ErosionDropletCount = 250000;
/// <summary>Governor 2 — max steps per droplet. Reference 384, clamp [1, 4096].</summary>
public int ErosionDropletLifetime = 384;
/// <summary>Governor 3 — max carve per cell, metres (net ledger). Reference 15, clamp [0, 60].</summary>
public float ErosionCarveCapM = 15.0f;
/// <summary>Governor 4 — max build-up per cell, metres (the ledger read the other way). Reference 6, clamp [0, 60]; ≤ 0 = unbounded.</summary>
public float ErosionDepositCapM = 6.0f;
/// <summary>The sea clamp's carve floor above sea, metres. Reference 0.5, clamp [0, 5].</summary>
public float ErosionSeaMarginM = 0.5f;
/// <summary>Brush radius, px (the cone brush shared by erode and deposit). Reference 2.</summary>
public int ErosionBrushRadius = 2;
public float ErosionInertia = 0.35f; // clamp [0, 0.99]
public float ErosionCapacity = 4.0f;
public float ErosionMinSlopeM = 0.02f; // metres per px
public float ErosionErodeRate = 0.12f;
public float ErosionDepositRate = 0.15f;
public float ErosionEvaporation = 0.004f; // clamp [0, 0.5]
public float ErosionGravity = 4.0f;
/// <summary>
/// The crater exclusion (task 19), PORTED BUT INERT: with no crater (<see cref="CraterRadius"/> 0)
/// the weight is 1 everywhere. Core ×radius — the reference's ConfigManager shipped 0.80 (the
/// pass's own default constant is 0.50); feather ×radius 1.05; mode feather. Activates when
/// the crater carve lands; the reference's "core &lt; carve factor" warning is dormant until then.
/// </summary>
public float CraterErosionCore = 0.80f;
public float CraterErosionFeather = 1.05f;
public bool CraterErosionFeatherMode = true;
/// <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);
// ═══ ⭐⭐ THE FAMILY-OFF PIN (rivers/01) ═══════════════════════════════════════════════════
//
// ═══ WHY THIS EXISTS — the preserve mechanism for `terrain-shape-v1` ═══
//
// The locked shape's curve knots are PERCENTILES OF THE FAMILY-OFF LAND DISTRIBUTION, measured
// by chat2/01 over a 6-seed pool at 2048 and applied to FAMILY-ON generation. That was not a
// choice at the time — it was simply what the bare defaults produced, because the shape family
// defaulted off.
//
// rivers/01 flipped those defaults ON. Left alone, every calibration pool would have moved
// with them (fragmentation removes coastal land, the stretch adds southern land — both change
// the land CDF), the six knots would have moved, and with them the render field of EVERY
// batch, including `10_frag4_seed_gallery` (= `terrain-shape-v1`) and `11_erosion`. The
// developer's ruling was to PRESERVE the locked terrain bit-identically, so the pool is pinned
// here instead of the knots being baked: CALIBRATION STAYS LIVE, its INPUT DISTRIBUTION is
// what is held still.
//
// ⚠ At the moment it was introduced this was a NO-OP BY CONSTRUCTION: it sets exactly the
// values the bare defaults carried the instant before the flip. That is what made the flip
// provably terrain-only — and what the rivers/01 acceptance confirmed byte-for-byte at 8192².
//
// > ### ⚑ THE JUDGED-AND-PARKED PROPERTY (recorded for the vault, rivers/01)
// > The curve knots are percentiles of the FAMILY-OFF land distribution, applied to FAMILY-ON
// > terrain. That is a real asymmetry and it is DELIBERATE, not an oversight: re-pooling on
// > family-on land would move the locked shape the developer judged. Same disposition as the
// > mid-slope feather — documented, revisit only at the final palette / in the mesher if it
// > ever visibly bothers. → `Vision - Threads - Open Questions.md`.
//
// ═══ WHAT IT IS FOR, AND WHAT IT IS NOT FOR ═══
//
// USE IT for a config whose job is to REPRODUCE A PRE-FAMILY FIELD: a curve-calibration
// pool, or the "off" half of a regression check against a family-off `.f32` dump.
// DO NOT use it for generation — the locked shape IS the family, and the bare defaults now
// carry it.
/// <summary>
/// ⭐ Pin the SHAPE FAMILY and erosion OFF on this config, independent of this class's
/// evolving defaults, and return it for chaining. → the block above for why.
///
/// Sets: <see cref="CoastShelf"/> false · <see cref="Offshore"/> Off ·
/// <see cref="SpeckRevert"/> false · <see cref="SouthStretch"/> 0 ·
/// <see cref="FragmentAmp"/> 0 · <see cref="Erosion"/> false.
///
/// ⚠ It deliberately does NOT touch <see cref="MinLandComponentFrac"/> or the band/window
/// shape dials: with the revert off and the amplitudes at zero those are unread, so pinning
/// them would assert an independence that does not exist. It also does not touch the CURVE
/// (knots, anchors, calibration, climb knobs) — the family and the curve are separate axes,
/// and a calibration pool is pass-1 only.
/// </summary>
public TerrainGenConfig WithFamilyOff()
{
CoastShelf = false;
Offshore = new OffshoreSettings(); // Mode = Off
SpeckRevert = false;
SouthStretch = 0f;
FragmentAmp = 0f;
Erosion = false;
return this;
}
/// <summary>
/// ⭐ A bare pass-1 config with the shape family pinned OFF — THE CALIBRATION POOL'S CONFIG.
/// Every curve-calibration pool in `Tools/` builds its fields through this, so there is one
/// place where "what distribution were the knots measured on?" is answered.
/// → <see cref="WithFamilyOff"/>.
/// </summary>
public static TerrainGenConfig CalibrationPool(int mapSize, int seed) =>
new TerrainGenConfig { MapSize = mapSize, Seed = seed }.WithFamilyOff();
/// <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)}] " +
// rivers/01: the shape family is now a DEFAULT, so a run header must state it — otherwise
// "the defaults" stops being a readable claim the moment anyone asks which defaults.
$"[stretch={SouthStretch:G3} frag={FragmentAmp:G3} speck={(SpeckRevert ? $"{MinLandComponentFrac:G3}" : "off")} " +
$"shelf={CoastShelf} islets={Offshore?.Mode} erosion={Erosion}]";
}
}