islaApocalypse-v2/Core/Scripts/WorldScale.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

80 lines
4.1 KiB
C#
Raw Permalink 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.

namespace IslaApocalypse.Core
{
/// <summary>
/// ⭐ THE WORLD'S VERTICAL YARDSTICK — the single metres↔raw-height conversion for the rewrite.
///
/// ═══ THE RULE ═══
///
/// THERE IS EXACTLY ONE METRES-PER-RAW-UNIT NUMBER, AND IT LIVES HERE.
/// No literal <c>251f</c> anywhere. No second <c>M_PER_UNIT</c>. Ever.
///
/// ═══ ⚠⚠ WHY THIS TYPE EXISTS — THE PROTOTYPE'S SCATTER ═══
///
/// The reference had this number in ONE derived place and then wrote it out by hand everywhere:
///
/// • <c>Core/Constants.cs</c>: <c>HEIGHT_SCALE = CHUNK_HEIGHT - 5</c> — the only DERIVED
/// definition, and the only one the runtime (<c>ServerChunkManager</c>) actually used.
/// • THREE independent hardcoded copies: <c>HydraulicErosion.M_PER_UNIT = 251f</c>,
/// <c>DrainageAnalysis.M_PER_UNIT = 251f</c>, <c>RiverCarvePass.M_PER_UNIT = 251f</c>.
/// • ~20 bare <c>251f</c> literals across <c>HeightCurve</c> and <c>MapGenerator</c>.
///
/// So the GENERATOR never used the derived constant at all. Retuning the chunk height would have
/// moved the runtime's yardstick and left every generated constant behind — silently, because a
/// literal does not throw. (chat2/00 report § C.7.)
///
/// ═══ ⚠⚠ WHERE 251 CAME FROM, AND WHAT IS STILL UNDECIDED ═══
///
/// In the prototype this equalled <c>CHUNK_HEIGHT - 5</c> = 256 - 5: a VOXEL-COLUMN BUDGET the
/// MESHER owned — the renderable height band, not a fact about the world. Every "420 m peak",
/// "220 m plateau" and "±3 m relief skin" in the generator was therefore denominated in a unit
/// defined by a rendering constant.
///
/// > ### ⚑ HERE IT IS A STANDALONE WORLD CONSTANT.
/// > **Whether it stays coupled to a future chunk height is a DEFERRED DESIGN DECISION for the
/// > vault, and this port does not settle it.** The value 251 is carried because the curve's
/// > anchors were tuned against it and D-050 says port, don't re-derive — not because a
/// > 256-voxel chunk has been decided on. If the vault later rules that the world's vertical
/// > scale is its own number, only this file changes.
///
/// The vault currently records only "roughly 251 m per raw height unit"
/// (`Design - Water - Storm Ladder.md`) and does not record the chunk-height derivation at all.
/// That gap is flagged for graduation, not fixed here — only master writes the vault.
/// </summary>
public static class WorldScale
{
/// <summary>
/// Metres of world height per raw height unit.
///
/// ⚠ Ported value, not a re-derivation: the redistribution curve's storm-ladder anchors
/// (420 m cap, 220 m plateau, 100 m bench) were calibrated against exactly this number, so
/// changing it reshapes the island. → D-050.
/// </summary>
public const float MetresPerRawUnit = 251f;
/// <summary>
/// The inverse, for callers that genuinely want a multiplier.
///
/// ⚠⚠ NOT INTERCHANGEABLE WITH <see cref="RawFromMetres"/>. In float32,
/// <c>420f * (1f/251f)</c> and <c>420f / 251f</c> are DIFFERENT NUMBERS — they differ in the
/// last bits. The reference wrote the division (<c>420f / 251f</c>), so every anchor this
/// repo derives must divide too, or the port is off by an ulp at every knot and no oracle
/// can prove fidelity. Use <see cref="RawFromMetres"/> unless you specifically need the
/// reciprocal.
/// </summary>
public const float RawUnitsPerMetre = 1f / MetresPerRawUnit;
/// <summary>
/// Metres → raw height units. **Divides**, bit-for-bit as the reference wrote it
/// (<c>420f / 251f</c>) — see the warning on <see cref="RawUnitsPerMetre"/>.
/// </summary>
public static float RawFromMetres(float metres) => metres / MetresPerRawUnit;
/// <summary>Raw height units → metres. The reference's <c>× 251f</c>.</summary>
public static float MetresFromRaw(float raw) => raw * MetresPerRawUnit;
/// <summary>One line for a run header. Print it; a yardstick worth having is worth stating.</summary>
public static string Describe() =>
$"1 raw height unit = {MetresPerRawUnit:F0} m (single source: Core/WorldScale; " +
"chunk-height coupling is a DEFERRED vault decision)";
}
}