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
558 lines
29 KiB
C#
558 lines
29 KiB
C#
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 (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.
|
||
/// </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 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.
|
||
/// </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)
|
||
/// >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 <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/09–10 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 < 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}]";
|
||
}
|
||
}
|