Stage 1 fills the IslandFalloff.cs stub from the reference, verbatim: the coast shelf (below-sea only, depth-preserving, held strictly below sea by MathF.BitDecrement — the clamp that makes "cannot move the waterline" exact rather than statistical) and the islet layer (OffshoreBlob, CalibrateThreshold against the field's ACTUAL distribution, the OffshoreZoneWeight moat + pre-Trench-falloff test). Both run as pass 1b — OffshorePass, a second sweep over the finished pass-1 arrays with the same per-pixel arithmetic in the same order — and HMaxSeed is retaken AFTER them, as the reference did. That closes chat2/00 Drift §2. The value did not move on any of 15 runs; the order is now right by construction and oracle (l) prints it every time. Stage 2 is the reshape, OffshoreSettings.Hybrid(): a seeded floor of ≥2 N / ≥4 S islands placed by a PCG32 off the world seed — min-separated, clear of ALL existing land by a gap so each stamp is its own connected component by construction, fully inside the zone so the moat and falloff protections gate the floor exactly as they gate the organic layer — plus the noise layer on top, smaller (freq 16), lower (24 m pre-curve, which the preserved toe squashes to ~5 m and keeps there across curve tweaks), flatter (core 0.25), crisper (edge sharpness 2.5, stamp rim jittered so it is rigid without being a compass disc), south-weighted (0.007 N / 0.012 S, blended across the midline), corners allowed (trench mask 0.90→0.97). Every lifted cell is tagged with its hemisphere — NORTH is rows [0, N/2), y runs south, read off the spine's southern fade and the southern sinker, not invented — and carried through Pass1Result → Pass2Result for a consumer that does not exist yet. Two things the probes taught, both now in the code: the first organic densities produced 183 blobs of which 174 were noise debris (a six-config sweep fixed that), and the reference's lerp-to-crest leaves shallow humps across the seabed wherever a blob fails to surface (a guard reverts them outside any surviving island's skirt; specks by component membership, bumps by location). The faithful control keeps both, because it is the reference — its islets measure min 1 cell, median 28. Oracle, all hard checks passing: floor met on every hybrid seed (N 2–4, S 9–16); zero land bridges; every offshore-OFF land cell bit-identical with offshore ON; tag ↔ coastline consistent in both fields; curve-off still bit-identical to Phase 1's dump and continuous_restored to task 03's. Both gates default OFF, deliberately: flipping them is the act that retires the Phase-1 regression dumps, and that belongs in a task that re-baselines the oracles. The faithful control on seed 8675309 produced one north island. That is the gap the floor exists to close. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
305 lines
15 KiB
C#
305 lines
15 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 DEFAULT OFF, DELIBERATELY — and that is a decision to revisit, not an oversight.
|
||
//
|
||
// Every oracle in this phase holds pass 1 against Phase 1's `.f32` dumps (curve-off ==
|
||
// `02_pass1_port`), and the curve tools hold it against task 01/03's. The shelf changes every
|
||
// below-sea cell and the islets ADD LAND, so the moment either defaults ON, every one of
|
||
// those regression anchors goes stale at once. The batch tools that want them turn them on
|
||
// explicitly. FLIPPING THESE DEFAULTS IS THE ACT THAT RETIRES THE PHASE-1 REGRESSION DUMPS —
|
||
// do it deliberately, in a task that re-baselines the oracles, not as a side effect here.
|
||
|
||
/// <summary>
|
||
/// The submarine coast shelf (<c>IslandFalloff.CoastShelf</c>). Below-sea only,
|
||
/// depth-preserving, held strictly below sea by <c>MathF.BitDecrement</c>. Invisible until
|
||
/// water renders; ported faithfully now, judged then.
|
||
/// </summary>
|
||
public bool CoastShelf = false;
|
||
|
||
// ⚠ Fully qualified: this class's own `IslandFalloff` ablation toggle shadows the static
|
||
// type of the same name inside field initializers.
|
||
|
||
/// <summary>Shelf strength, 0 = off → 1 = a flat lagoon. Reference 0.775.</summary>
|
||
public float ShelfStrength = IslaApocalypse.Tools.IslandFalloff.SHELF_STRENGTH;
|
||
|
||
/// <summary>Metres of depth over which the shelf relaxes. Reference 100.</summary>
|
||
public float ShelfScaleM = IslaApocalypse.Tools.IslandFalloff.SHELF_SCALE_M;
|
||
|
||
/// <summary>
|
||
/// ⭐ The offshore islet system — every dial in one object. <c>Mode = Off</c> by default
|
||
/// (see the note above). <see cref="OffshoreSettings.Faithful"/> is the reference verbatim;
|
||
/// <see cref="OffshoreSettings.Hybrid"/> is the reshape.
|
||
/// </summary>
|
||
public OffshoreSettings Offshore = new OffshoreSettings();
|
||
|
||
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
|
||
public string VariantLabel = "full";
|
||
|
||
/// <summary>The scale object every distance and frequency in the generator derives from.</summary>
|
||
public GenerationScale Scale => new GenerationScale(MapSize);
|
||
|
||
/// <summary>
|
||
/// ⚠ DEEP on <see cref="Anchors"/>. <c>MemberwiseClone</c> is shallow, so two configs cloned
|
||
/// from one parent would share a single mutable anchor object and an A/B that edited one
|
||
/// would silently move the other. The one reference type that is a DIAL gets copied; the one
|
||
/// that is immutable (<see cref="CurveKnots"/>) does not need to be.
|
||
/// </summary>
|
||
public TerrainGenConfig Clone()
|
||
{
|
||
var c = (TerrainGenConfig)MemberwiseClone();
|
||
c.Anchors = Anchors?.Clone();
|
||
c.Offshore = Offshore?.Clone(); // same reason: a mutable dial object, deep-copied
|
||
return c;
|
||
}
|
||
|
||
public override string ToString() =>
|
||
$"MapSize={MapSize} Seed={Seed} axis={IslandAxisX:F2}x/{IslandAxisY:F2}y " +
|
||
$"falloffStrength={FalloffStrength:F2} sea={SeaLevel:F2} variant={VariantLabel} " +
|
||
$"[base={BaseNoise} falloff={IslandFalloff} edge={EdgeNoise} sinker={SouthernSinker} " +
|
||
$"trench={Trench} spine={MountainSpine}] " +
|
||
$"[curve={Curve} detail={ShelfDetail} relief={ShelfReliefAmpM:F1}m edge={ShelfEdgeVariationM:F1}m " +
|
||
$"knots={(Knots == null ? "-" : Knots.Name)}]";
|
||
}
|
||
}
|