chat2/02 dissolved the terraces and lost two thirds of the mountain with them, then
concluded the loss was structural and needed a Phase-1 noise change. That conclusion
was wrong, and this commit is the refutation.
A monotone curve is a free reparametrization: it may be gentle in one place and steep
in another, and can lift bottom-heavy input into a substantial massif without ever
going flat. The area above a height is set by where the percentile->height mapping
crosses it, and that mapping is entirely ours to choose. The 02 sweep that "proved"
the loss structural varied climbFeather, which shapes the JOIN, not the mass
distribution — the wrong knob, and too strong a conclusion drawn from it.
Core/ClimbCalibration — the climb's control points are now MEASURED off the staircase
instead of invented from shape knobs. For p in {10,30,50,70,85,95} of above-ceiling
land, take that percentile's raw height and its staircase output height; PCHIP through
the pairs. That reproduces the staircase's elevation envelope, so the mountain mass
returns, while MinNormalizedSecant floors every grade so the flat bench and plateau
interiors become slope. The floor bites on exactly one segment — the plateau — which
is precisely where the staircase was flat.
ContinuousCurve.BuildCalibrated joins it to the same pinned lowland handover, the same
C1 join and the same per-seed spikeMax. The 02 analytic path survives unchanged as the
"before" contrast, and deliberately keeps its strictly-increasing-secant rule: a
calibrated curve is WAVY by design, so convexity is the wrong invariant for it and the
secant floor is the right one.
peakSharpness replaces summitDrama and fixes its bad trade. Drama steepened the peak by
pulling the summit ONSET down, dragging the whole massif with it (p99 199 -> 121 m).
Sharpness reshapes only above the last measured percentile, leaving that height fixed,
so peak and massif are independent: raising it leaves p90, >100 m and >220 m untouched
and only moves land within the summit.
Measured, both seeds, 2048:
variant >100 m >220 m p90
staircase (target) 16.05% 4.53% 127.4 m
continuous_02default 4.80% 0.63% 58.4 m
continuous_restored 14.25% 3.45% 123.2 m
continuous_bigger 19.70% 5.80% 163.5 m
Oracle all hard checks pass, including (a2) staircase still bit-identical to task 01's
dump and (d) lowlands bit-identical across every calibrated variant. New (g) reports
land above 100/220 m per variant and is deliberately NOT gated — it is a taste target
the developer tunes, and gating it would make mountainLift unusable. What it must never
do is stay silent, which is how 02 lost the mountain unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
270 lines
13 KiB
C#
270 lines
13 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;
|
||
|
||
/// <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();
|
||
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)}]";
|
||
}
|
||
}
|