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
142 lines
6.8 KiB
C#
142 lines
6.8 KiB
C#
namespace IslaApocalypse.Core
|
|
{
|
|
/// <summary>
|
|
/// The redistribution curve's fixed OUTPUT anchors — the elevations the bands are mapped ONTO.
|
|
///
|
|
/// ═══ INPUT KNOTS vs OUTPUT ANCHORS — the distinction the whole curve rests on ═══
|
|
///
|
|
/// <see cref="CurveKnots"/> — WHERE the land distribution is cut. Percentiles. Measured.
|
|
/// <see cref="CurveAnchors"/> — WHAT HEIGHT each cut lands at. Storm-ladder. Chosen.
|
|
///
|
|
/// Re-measuring the knots moves how much land is in each band. Moving the anchors moves how HIGH
|
|
/// each band sits. They are independent, and conflating them is how a "recalibration" turns into
|
|
/// an unnoticed reshape.
|
|
///
|
|
/// ═══ ⚠ WHY THIS IS A PARAMETER OBJECT AND NOT A WALL OF CONSTANTS ═══
|
|
///
|
|
/// The reference held these as <c>const</c> fields on <c>HeightCurve</c> and passed only the
|
|
/// per-column modulated values (<c>benchLo</c>, <c>benchSpan</c>, …) as arguments. That made the
|
|
/// storm-ladder anchors unreachable from config: A/B-ing the 420 m cap meant editing and
|
|
/// rebuilding.
|
|
///
|
|
/// Here every anchor is an explicit parameter, in the spirit of D-035 ("every per-column input
|
|
/// and the knot set are explicit PARAMETERS"). <see cref="Default"/> reproduces the reference's
|
|
/// constants bit-for-bit, so this is an exposure, not a change.
|
|
///
|
|
/// ⚠⚠ THE BAND COUNT AND THE SEGMENT SHAPES ARE NOT EXPOSED, DELIBERATELY. Adding a knot,
|
|
/// steepening a segment or reallocating the shares is the RESHAPE — a later task with its own
|
|
/// gate. This type exposes the existing seven-band curve's dials and nothing more.
|
|
///
|
|
/// Every metre-denominated anchor is derived through <see cref="WorldScale.RawFromMetres"/> —
|
|
/// the single yardstick — never a literal <c>/251f</c>.
|
|
/// </summary>
|
|
public sealed class CurveAnchors
|
|
{
|
|
// ---- the frozen band ceilings (raw height units) ---------------------
|
|
|
|
/// <summary>
|
|
/// Sea level in raw units. ⚠ ALSO THE CURVE'S IDENTITY THRESHOLD: at and below this the
|
|
/// curve returns its input untouched, which is what keeps the waterline, the Trench
|
|
/// guarantee and (later) every water body invariant under the curve. Reference: 0.15f.
|
|
/// </summary>
|
|
public float Sea = 0.15f;
|
|
|
|
/// <summary>Top of the toe/orange band. Reference: 0.206f.</summary>
|
|
public float OrangeCeil = 0.206f;
|
|
|
|
/// <summary>Top of the red band — the floor the foothill riser climbs from. Reference: 0.27f.</summary>
|
|
public float RedCeil = 0.27f;
|
|
|
|
// ---- the modulated shelf anchors -------------------------------------
|
|
|
|
/// <summary>Bench centre, raw. Reference: <c>SEA + 100 m</c>.</summary>
|
|
public float BenchBase = 0.15f + WorldScale.RawFromMetres(100f);
|
|
|
|
/// <summary>Bench modulation amplitude, raw. Reference: <c>±12 m</c>.</summary>
|
|
public float BenchAmp = WorldScale.RawFromMetres(12f);
|
|
|
|
/// <summary>Plateau centre, raw. Reference: <c>SEA + 220 m</c>.</summary>
|
|
public float PlateauBase = 0.15f + WorldScale.RawFromMetres(220f);
|
|
|
|
/// <summary>Plateau modulation amplitude, raw. Reference: <c>±20 m</c>.</summary>
|
|
public float PlateauAmp = WorldScale.RawFromMetres(20f);
|
|
|
|
/// <summary>
|
|
/// Narrowest a shelf band may be, raw. Reference: <c>6 m</c> (v4 was 2 m — "corner fix 3":
|
|
/// a pronounced shelf keeps a gentle tilt, flat to build on but never snooker-table flat).
|
|
/// </summary>
|
|
public float ShelfSpanMin = WorldScale.RawFromMetres(6f);
|
|
|
|
/// <summary>Widest a shelf band may be, raw. Reference: 0.10f (≈ 25 m).</summary>
|
|
public float ShelfSpanMax = 0.10f;
|
|
|
|
// ---- the ceiling and its tail ---------------------------------------
|
|
|
|
/// <summary>
|
|
/// The peak cap, raw. Reference: <c>SEA + 420 m</c>. The summit spike maps
|
|
/// <c>[K6, spikeMax]</c> onto <c>[plateauTop, PeakCap]</c>, so this is the island's
|
|
/// nominal ceiling — exact, not statistical, because K6 never moves under the edge warp.
|
|
/// </summary>
|
|
public float PeakCap = 0.15f + WorldScale.RawFromMetres(420f);
|
|
|
|
/// <summary>
|
|
/// Slope above <c>spikeMax</c>. Reference: 0.25f. ⚠ A gentle TAIL, not a hard clip — a seed
|
|
/// whose max exceeds the spike range still rises, just slowly.
|
|
/// </summary>
|
|
public float TailSlope = 0.25f;
|
|
|
|
/// <summary>
|
|
/// Minimum spike span, raw. Reference: 0.01f. Guarantees a non-degenerate summit band on a
|
|
/// seed whose map-wide max lands at or below K6.
|
|
/// </summary>
|
|
public float SpikeMinSpan = 0.01f;
|
|
|
|
// ---- the modulation fields' identity ---------------------------------
|
|
//
|
|
// ⚠ THESE ARE SEED OFFSETS, NOT COORDINATE OFFSETS. The reference decorrelated its curve
|
|
// modulation fields by seeding each one at `resolvedSeed + offset` and sampling all of them
|
|
// at the bare (x, y) — there is no `GetNoise2D(x + 1000, …)` anywhere in this path. So the
|
|
// raw-pixel-offset hazard GenerationScale warns about does NOT apply here, and there is
|
|
// nothing to normalize. (Verified against every MakeModulationNoise call site; recorded
|
|
// because the absence of a bug is only reassuring if someone checked.)
|
|
|
|
/// <summary>Bench-anchor field seed offset. Reference: 7101.</summary>
|
|
public int BenchSeedOffset = 7101;
|
|
|
|
/// <summary>Plateau-anchor field seed offset. Reference: 7207.</summary>
|
|
public int PlateauSeedOffset = 7207;
|
|
|
|
/// <summary>Shelf-strength field seed offset. Reference: 7303.</summary>
|
|
public int StrengthSeedOffset = 7303;
|
|
|
|
/// <summary>
|
|
/// Anchor-field frequency, in periods per MAP WIDTH. Reference: 3.0f — a very low frequency,
|
|
/// so the bench and plateau elevations drift across the island rather than flickering.
|
|
/// ⚠ Already scale-safe by construction: stated per map width, not per pixel.
|
|
/// </summary>
|
|
public float ElevFreqPerMapWidth = 3.0f;
|
|
|
|
/// <summary>Shelf-strength field frequency, periods per map width. Reference: 5.0f.</summary>
|
|
public float StrengthFreqPerMapWidth = 5.0f;
|
|
|
|
/// <summary>
|
|
/// The reference's shipped anchors, reproduced bit-for-bit. Every metre value goes through
|
|
/// <see cref="WorldScale.RawFromMetres"/>, which divides — matching the reference's
|
|
/// <c>420f / 251f</c> exactly rather than approximating it with a reciprocal multiply.
|
|
/// </summary>
|
|
public static CurveAnchors Default => new CurveAnchors();
|
|
|
|
public CurveAnchors Clone() => (CurveAnchors)MemberwiseClone();
|
|
|
|
/// <summary>
|
|
/// The anchors as the storm ladder states them — metres above sea. For a run header, where
|
|
/// raw units mean nothing to a reader.
|
|
/// </summary>
|
|
public string DescribeMetres() =>
|
|
$"sea {Sea:F3} raw · orange {WorldScale.MetresFromRaw(OrangeCeil - Sea):F0} m · " +
|
|
$"red {WorldScale.MetresFromRaw(RedCeil - Sea):F0} m · " +
|
|
$"bench {WorldScale.MetresFromRaw(BenchBase - Sea):F0}±{WorldScale.MetresFromRaw(BenchAmp):F0} m · " +
|
|
$"plateau {WorldScale.MetresFromRaw(PlateauBase - Sea):F0}±{WorldScale.MetresFromRaw(PlateauAmp):F0} m · " +
|
|
$"cap {WorldScale.MetresFromRaw(PeakCap - Sea):F0} m";
|
|
}
|
|
}
|