using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// 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.
///
public enum CurveModeKind { Staircase, Continuous, LiftedWrong }
///
/// 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.
///
public sealed class TerrainGenConfig
{
// ---- world ----------------------------------------------------------
///
/// Map side in columns. A GENERATION PARAMETER — never baked in.
/// Iteration runs small (2048–4096); 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.
///
public int MapSize = 2048;
///
/// 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.
///
public int Seed = 0;
// ---- island shape (reference ConfigManager defaults, READ from source) ----
/// Falloff X axis ratio. Reference: ConfigManager.LEGACY_AXIS_X = 1.15f.
public float IslandAxisX = 1.15f;
/// Falloff Y axis ratio. Reference: ConfigManager.LEGACY_AXIS_Y = 0.90f.
public float IslandAxisY = 0.90f;
///
/// Multiplier on the falloff term in the combine.
/// Reference: [Export] public float FalloffStrength = 1.0f; (MapGenerator ~:11),
/// and NOT overridden in MapPreview.tscn — so 1.0f is the value the island was tuned at.
///
public float FalloffStrength = 1.0f;
///
/// The flat sea level, in raw height units. Reference: ConfigManager.SeaLevelValue = 0.15f
/// with SeaLevelModel = "flat", so GetSeaLevel 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.
///
public float SeaLevel = 0.15f;
// ---- ABLATION TOGGLES — the pass-1 ladder ---------------------------
/// Rung 1: the base terrain noise, (noise(x,y)+1)/2. Off = flat zero.
public bool BaseNoise = true;
///
/// Rung 2: the island falloff/mask — squircle + ellipse blend, and the Pow(·, 2.5f).
/// ⚠ Off also disables rungs 3–5 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.
///
public bool IslandFalloff = true;
/// Rung 3: coastline edge roughness, modulated by the squircle.
public bool EdgeNoise = true;
/// Rung 4: the southern sinker — extra sinking pressure in the bottom 25%.
public bool SouthernSinker = true;
/// Rung 5: the Trench — the map-anchored outer-band wall that guarantees an ocean border.
public bool Trench = true;
/// Rung 6: the mountain spine up the centre-X axis.
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.
///
/// ⭐ Pass 2a rung 1: the height-redistribution curve. → .
/// Off = raw pass-1 height, unshaped (the control half of every A/B in this phase).
///
public bool Curve = true;
///
/// ⭐ WHICH curve (chat2/02). → .
///
/// 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.
///
public CurveModeKind CurveMode = CurveModeKind.Continuous;
// ---- the continuous climb's knobs — ALL act above the lowland ceiling only ----
///
/// 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 : 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.
///
public float LowlandCeilingM = 30f;
///
/// 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.
///
public float ClimbFeather = 0.4f;
///
/// ⚠ chat2/02's ANALYTIC summit knob — SUPERSEDED by .
///
/// 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 is null.
///
public float SummitDrama = 2.5f;
// ---- chat2/03: the CALIBRATED climb ---------------------------------
///
/// ⭐ 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.
///
public ClimbCalibration ClimbCalibration = null;
///
/// ⭐ How big the mountain is, relative to the staircase's.
///
/// 1.0 reproduce the staircase's mountain (the default — the least-surprising baseline)
/// >1 lift the mid-massif higher: more land at 150–300 m
/// <1 a smaller mountain, toward chat2/02's bottom-heavy climb
///
/// Applied as v ← v^(1/lift) 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.
///
public float MountainLift = 1.0f;
///
/// ⭐ How pointy the summit is — and, unlike , nothing else.
///
/// 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.
///
public float PeakSharpness = 1.0f;
///
/// ⭐ Pass 2a rung 2: the shelf detail passes — micro-relief skin + shelf-edge knot warp.
/// ⚠ REQUIRES : 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.
///
public bool ShelfDetail = true;
///
/// Micro-relief amplitude, in METRES of output height. Reference default: 3 m.
/// Converted through at the call site — never a literal /251.
///
public float ShelfReliefAmpM = TerrainDetailPass.ReliefAmpDefaultM;
///
/// Shelf-edge warp amplitude, in METRES OF INPUT HEIGHT (not output elevation — see
/// ). Reference default: 12 m.
///
/// ⚠ CLAMPED, LOUDLY, to the knot set's safe bound (TerrainDetailPass.MaxEdgeShift).
/// Monotonicity is never a tuning question; an ignored dial is always reported.
///
public float ShelfEdgeVariationM = TerrainDetailPass.EdgeAmpDefaultM;
///
/// The input knot set — WHERE the land distribution is cut.
/// Default: , re-measured on v2's own pass-1 output.
/// is available for a fidelity A/B against the prototype's.
///
public CurveKnots Knots = CurveKnots.V2Baseline;
///
/// The output anchors — WHAT HEIGHT each cut lands at. Default: the storm-ladder values,
/// reproducing the reference's constants bit-for-bit.
///
public CurveAnchors Anchors = CurveAnchors.Default;
// ---- the crater seam — INERT THIS PHASE -----------------------------
///
/// Crater radius in columns. ⚠ 0 = NO CRATER, which is this phase's state. 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.
///
public float CraterRadius = 0f;
/// Crater centre X, columns. Unused while is 0.
public float CraterCenterX = 0f;
/// Crater centre Y, columns. Unused while is 0.
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.
///
/// The submarine coast shelf (IslandFalloff.CoastShelf). Below-sea only,
/// depth-preserving, held strictly below sea by MathF.BitDecrement. Invisible until
/// water renders; ported faithfully now, judged then.
///
public bool CoastShelf = false;
// ⚠ Fully qualified: this class's own `IslandFalloff` ablation toggle shadows the static
// type of the same name inside field initializers.
/// Shelf strength, 0 = off → 1 = a flat lagoon. Reference 0.775.
public float ShelfStrength = IslaApocalypse.Tools.IslandFalloff.SHELF_STRENGTH;
/// Metres of depth over which the shelf relaxes. Reference 100.
public float ShelfScaleM = IslaApocalypse.Tools.IslandFalloff.SHELF_SCALE_M;
///
/// ⭐ The offshore islet system — every dial in one object. Mode = Off by default
/// (see the note above). is the reference verbatim;
/// is the reshape, tuned (chat2/06).
///
public OffshoreSettings Offshore = new OffshoreSettings();
// ---- PASS 1c — region labeling + the speck revert (chat2/07) ----------
///
/// ⭐ THE REGION-LABELING LAYER (Core.RegionLabeling, via RegionPass): 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).
///
public bool RegionLabeling = true;
///
/// ⭐ THE SPECK REVERT — lower every non-mainland land component smaller than
/// 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
/// (), so the knots are unmoved and the flip is terrain-only.
///
public bool SpeckRevert = true;
///
/// 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 (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/09–10 froze and the
/// developer judged. Changing it changes `terrain-shape-v1`. (rivers/01, from
/// `FragGalleryTool.FrozenSpeckFrac`.)
///
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.
///
/// ⭐ 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).
///
public float SouthStretch = 2f;
///
/// 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.)
///
public float SouthBandStartFrac = 0.70f;
///
/// 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 . (rivers/01.)
///
public float SouthBandFeatherFrac = 0.05f;
///
/// 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; → .
///
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `SouthernStretch.DefaultStretchSinker` today). rivers/01.
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.
///
/// ⭐ 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).
///
public float FragmentAmp = 0.5f;
///
/// 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.)
///
public float FragmentFreqPerMapWidth = 12f;
///
/// 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.)
///
public float FragmentBandCentre = 0.66f;
public float FragmentBandHalfWidth = 0.18f;
///
/// 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. → .
///
/// ⚠ Pinned as a LITERAL at the locked-shape value (equals `CoastalFragment.DefaultBitesOnly` today). rivers/01.
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.
///
/// ⭐ 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.
///
public bool Erosion = true;
/// Governor 1 — droplet count. Reference 250000, clamp [0, 50,000,000].
public int ErosionDropletCount = 250000;
/// Governor 2 — max steps per droplet. Reference 384, clamp [1, 4096].
public int ErosionDropletLifetime = 384;
/// Governor 3 — max carve per cell, metres (net ledger). Reference 15, clamp [0, 60].
public float ErosionCarveCapM = 15.0f;
/// Governor 4 — max build-up per cell, metres (the ledger read the other way). Reference 6, clamp [0, 60]; ≤ 0 = unbounded.
public float ErosionDepositCapM = 6.0f;
/// The sea clamp's carve floor above sea, metres. Reference 0.5, clamp [0, 5].
public float ErosionSeaMarginM = 0.5f;
/// Brush radius, px (the cone brush shared by erode and deposit). Reference 2.
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;
///
/// The crater exclusion (task 19), PORTED BUT INERT: with no crater ( 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 < carve factor" warning is dormant until then.
///
public float CraterErosionCore = 0.80f;
public float CraterErosionFeather = 1.05f;
public bool CraterErosionFeatherMode = true;
/// A short label for this variant, used in output filenames. E.g. "full", "base_only".
public string VariantLabel = "full";
/// The scale object every distance and frequency in the generator derives from.
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.
///
/// ⭐ 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: false · Off ·
/// false · 0 ·
/// 0 · false.
///
/// ⚠ It deliberately does NOT touch 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.
///
public TerrainGenConfig WithFamilyOff()
{
CoastShelf = false;
Offshore = new OffshoreSettings(); // Mode = Off
SpeckRevert = false;
SouthStretch = 0f;
FragmentAmp = 0f;
Erosion = false;
return this;
}
///
/// ⭐ 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.
/// → .
///
public static TerrainGenConfig CalibrationPool(int mapSize, int seed) =>
new TerrainGenConfig { MapSize = mapSize, Seed = seed }.WithFamilyOff();
///
/// ⚠ DEEP on . MemberwiseClone 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 () does not need to be.
///
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}]";
}
}