islaApocalypse-v2/Tools/Scripts/TerrainGenConfig.cs
beezm 35b4818e9e Phase 2a: the faithful redistribution curve, re-measured against v2's own output
Ports the reference's v5 height curve and shelf-detail passes onto Phase 1's shape
and re-calibrates them against this repo's actual pass-1 distribution. This is the
BASELINE the reshape gets judged against, not the reshape.

Core (engine-free, D-060):
- WorldScale — THE vertical yardstick. One metres/raw number (251), replacing the
  prototype's three duplicate M_PER_UNIT constants and ~20 bare literals. The
  chunk-height coupling it had there is recorded as a DEFERRED vault decision, not
  inherited. RawFromMetres divides, matching the reference bit-for-bit.
- HeightCurve — the 7 bands, the frozen corner-fix blends, the per-seed spike
  normalization, the 24-corner monotonicity sweep that throws and refuses.
  Identity at and below sea, which everything downstream rests on.
- CurveKnots / CurveAnchors — input knots (measured percentiles) and output anchors
  (storm ladder) split apart and both made parameters, so the anchors are A/B-able
  without editing source. The reference's shipped knots are kept beside the measured
  ones as the fidelity yardstick.
- TerrainDetailPass — micro-relief skin plus the shelf-edge KNOT warp (which slides
  K3/K4/K5, not height — that is what keeps monotonicity structural). The crater
  exclusion is ported and inert until the carve lands.

Tools:
- Shaping — pass 2a, producing the two height fields. classify is bit-for-bit the
  raw pass-1 field; render is curved and detailed. Aliased when the curve is off,
  as the reference did. Pass1Result is left immutable so the oracle can compare.
- LandHistogram — the calibration engine AND the diagnostic. The reference shipped
  six knot literals and threw the measuring instrument away; this rebuilds it.
- ShapingOracle + CurveBaselineTool — four automatic checks before anything is
  looked at, and the batch that runs them.

Measured, not assumed:
- Knots re-measured over a 6-seed / 12.8M-sample pool. They differ from the
  reference's by at most 5.6 m of world height, against a 44.7 m per-seed spread —
  the pass-1 port is faithful.
- Oracle all pass, including pass 1 bit-identical to Phase 1's own .f32 dump.
- Band shares land on 60/13/10/5/8/3/1 to 0.00 pp.
- Knots hold across map size: the 8K delta (5.8 m) sits inside seed noise.

The finding the histograms deliver: 83% of land ends below 100 m and 96% below
220 m, with the median column at 13 m. That is the share targets doing exactly what
they say, not a bug — and it is the developer's call, which is why nothing here
reshapes it and the palette was deliberately left mis-fitted rather than recalibrated
to disguise it.

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

178 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// The generator's configuration, including the per-element ABLATION TOGGLES.
///
/// ═══ WHY EVERY PASS-1 ELEMENT IS GATED ═══
///
/// This is the standing A/B discipline: every shaping change ships behind a gate with the
/// previous behaviour surviving on the other side, so changes stay comparable as a pair rather
/// than as a memory of last week's render, and a rejected change costs a flipped default rather
/// than a reverted commit. → `Design - Tooling - Iteration and Batching.md`.
///
/// For THIS task the gates do a second job: they are the PORT-FIDELITY CHECK. Generating
/// base-noise-only, then adding one element at a time, shows each ported element doing what the
/// reference's did — rather than judging six simultaneous changes by their sum.
///
/// ⚠ Lives in Tools/, not Core/. It configures the generator specifically; Core carries the
/// world's data contracts and the scaling rule, not a tool's dials.
/// </summary>
public sealed class TerrainGenConfig
{
// ---- world ----------------------------------------------------------
/// <summary>
/// Map side in columns. A GENERATION PARAMETER — never baked in.
/// Iteration runs small (20484096); the shape is scale-invariant by construction, so the
/// island reads the same at any size and a full-size pass is confirmation, not iteration.
/// </summary>
public int MapSize = 2048;
/// <summary>
/// The noise seed. POSITIVE ONLY. Zero or negative means "pick one and print it" — a run
/// whose seed is not recorded is a run that cannot be reproduced, so the resolved seed is
/// always printed and always in the filename.
/// </summary>
public int Seed = 0;
// ---- island shape (reference ConfigManager defaults, READ from source) ----
/// <summary>Falloff X axis ratio. Reference: <c>ConfigManager.LEGACY_AXIS_X = 1.15f</c>.</summary>
public float IslandAxisX = 1.15f;
/// <summary>Falloff Y axis ratio. Reference: <c>ConfigManager.LEGACY_AXIS_Y = 0.90f</c>.</summary>
public float IslandAxisY = 0.90f;
/// <summary>
/// Multiplier on the falloff term in the combine.
/// Reference: <c>[Export] public float FalloffStrength = 1.0f;</c> (MapGenerator ~:11),
/// and NOT overridden in MapPreview.tscn — so 1.0f is the value the island was tuned at.
/// </summary>
public float FalloffStrength = 1.0f;
/// <summary>
/// The flat sea level, in raw height units. Reference: <c>ConfigManager.SeaLevelValue = 0.15f</c>
/// with <c>SeaLevelModel = "flat"</c>, so <c>GetSeaLevel</c> ignores latitude entirely.
///
/// ⚠ PHASE 1 USES THIS AS A VISUALIZATION THRESHOLD ONLY — the boundary between the
/// bathymetric and hypsometric colour ramps. No water is modelled, no water bodies are
/// identified, nothing floods. That is Phase 2.
/// </summary>
public float SeaLevel = 0.15f;
// ---- ABLATION TOGGLES — the pass-1 ladder ---------------------------
/// <summary>Rung 1: the base terrain noise, <c>(noise(x,y)+1)/2</c>. Off = flat zero.</summary>
public bool BaseNoise = true;
/// <summary>
/// Rung 2: the island falloff/mask — squircle + ellipse blend, and the <c>Pow(·, 2.5f)</c>.
/// ⚠ Off also disables rungs 35 in effect: edge noise, the sinker and the Trench are all
/// modifiers OF the falloff, so with no falloff there is nothing for them to modify. The
/// toggles stay independent so the ladder reads honestly; the report says so.
/// </summary>
public bool IslandFalloff = true;
/// <summary>Rung 3: coastline edge roughness, modulated by the squircle.</summary>
public bool EdgeNoise = true;
/// <summary>Rung 4: the southern sinker — extra sinking pressure in the bottom 25%.</summary>
public bool SouthernSinker = true;
/// <summary>Rung 5: the Trench — the map-anchored outer-band wall that guarantees an ocean border.</summary>
public bool Trench = true;
/// <summary>Rung 6: the mountain spine up the centre-X axis.</summary>
public bool MountainSpine = true;
// ---- PASS 2a — the redistribution curve and shelf detail (Phase 2) ----
//
// ⚠ THE PRIMARY A/B OF THIS PHASE IS `Curve`. Off must reproduce Phase 1's pass-1 output
// BIT-IDENTICALLY — that is the regression oracle, not a figure of speech.
/// <summary>
/// ⭐ Pass 2a rung 1: the height-redistribution curve. → <see cref="HeightCurve"/>.
/// Off = raw pass-1 height, unshaped (the control half of every A/B in this phase).
/// </summary>
public bool Curve = true;
/// <summary>
/// ⭐ 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)}]";
}
}