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
130 lines
5.4 KiB
C#
130 lines
5.4 KiB
C#
using System.Collections.Generic;
|
|
using IslaApocalypse.Core;
|
|
|
|
namespace IslaApocalypse.Tools
|
|
{
|
|
/// <summary>
|
|
/// Everything pass 2 produces: THE TWO HEIGHT FIELDS. → <see cref="Shaping"/>.
|
|
///
|
|
/// ═══ ⭐⭐ THE TWO-FIELD SPLIT (D-046) — THE DISCIPLINE THIS CLASS EXISTS TO HOLD ═══
|
|
///
|
|
/// <see cref="HeightClassify"/> RAW. Uncurved, un-detailed. The ORACLE.
|
|
/// <see cref="Height"/> RENDER. Curved, detailed, and later eroded and carved.
|
|
///
|
|
/// Everything that CLASSIFIES the world — biomes, water bodies, the ocean flood fill — reads the
|
|
/// classify field. Everything that DRAWS or MESHES it reads the render field. The reference's
|
|
/// hardest-won lesson is that this split is what made five rounds of taste-iteration safe: the
|
|
/// biome and water maps stayed md5-identical across every shaping change, so correctness was
|
|
/// never being judged by eye. → `Design - Tooling - Iteration and Batching.md`,
|
|
/// "build the oracle before the taste-iteration".
|
|
///
|
|
/// ⚠ NOTHING CONSUMES THE CLASSIFY FIELD YET. No water, no biomes exist in this phase. The split
|
|
/// is established HERE, at the curve, because the curve is where the second field is BORN — and
|
|
/// retrofitting a classify path after three passes already ran on one array is how the two
|
|
/// silently diverge. The field is produced and asserted now so that when water lands it has
|
|
/// something correct to read.
|
|
///
|
|
/// ═══ ⚠ WHEN THE TWO FIELDS ARE THE SAME ARRAY ═══
|
|
///
|
|
/// With the curve OFF there is nothing to separate, so both properties reference ONE array —
|
|
/// exactly as the reference did (<c>_heightMapClassify = (_curveOn || _erosionOn) ? new float[…]
|
|
/// : _heightMap</c>). <see cref="FieldsAreAliased"/> says so out loud, because a later pass that
|
|
/// writes through one reference while reading the other MUST know: the reference's crater carve
|
|
/// reads both into locals before writing either for precisely this reason, and that is the trap
|
|
/// this flag is here to keep visible until the carve lands.
|
|
/// </summary>
|
|
public sealed class Pass2Result
|
|
{
|
|
/// <summary>Map side in columns.</summary>
|
|
public readonly int MapSize;
|
|
|
|
/// <summary>The resolved seed. Same seed, same two fields.</summary>
|
|
public readonly int Seed;
|
|
|
|
/// <summary>
|
|
/// ⭐ THE RENDER FIELD, <c>[x, y]</c> — curved and detailed. What gets drawn, dumped and
|
|
/// (later) eroded, carved and meshed.
|
|
/// </summary>
|
|
public readonly float[,] Height;
|
|
|
|
/// <summary>
|
|
/// ⭐ THE CLASSIFY FIELD, <c>[x, y]</c> — bit-for-bit the raw pre-curve pass-1 height.
|
|
///
|
|
/// ⚠ Nothing may write to this after pass 2 except the crater carve, which is the one pass
|
|
/// that legitimately moves both fields. Erosion, rivers and detail are render-only.
|
|
/// </summary>
|
|
public readonly float[,] HeightClassify;
|
|
|
|
/// <summary>Was the curve applied? The primary A/B gate.</summary>
|
|
public readonly bool CurveOn;
|
|
|
|
/// <summary>Was shelf detail applied? Requires <see cref="CurveOn"/> — it warps the curve's knots.</summary>
|
|
public readonly bool DetailOn;
|
|
|
|
/// <summary>The knot set used. Null when the curve is off.</summary>
|
|
public readonly CurveKnots Knots;
|
|
|
|
/// <summary>The output anchors used. Null when the curve is off.</summary>
|
|
public readonly CurveAnchors Anchors;
|
|
|
|
/// <summary>The per-seed spike input, carried through from pass 1.</summary>
|
|
public readonly float HMaxSeed;
|
|
|
|
/// <summary>The edge-warp amplitude actually APPLIED, raw units (post-clamp). Zero when detail is off.</summary>
|
|
public readonly float EdgeAmpRaw;
|
|
|
|
/// <summary>The knot set's safe warp bound, raw units — what <see cref="EdgeAmpRaw"/> was clamped to.</summary>
|
|
public readonly float MaxEdgeShiftRaw;
|
|
|
|
/// <summary>Render-field extremes after shaping. For the ramp and the report.</summary>
|
|
public readonly float HMin, HMax;
|
|
|
|
/// <summary>Wall-clock milliseconds pass 2 took.</summary>
|
|
public readonly ulong ElapsedMs;
|
|
|
|
/// <summary>
|
|
/// Lines worth printing: the monotonicity confirmation, any loud clamp. Collected rather than
|
|
/// printed inside the pass so the shaping code stays a pure function of its inputs and the
|
|
/// tool owns the console.
|
|
/// </summary>
|
|
public readonly List<string> Notes;
|
|
|
|
public Pass2Result(int mapSize, int seed, float[,] height, float[,] heightClassify,
|
|
bool curveOn, bool detailOn, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
|
|
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
|
|
List<string> notes)
|
|
{
|
|
MapSize = mapSize;
|
|
Seed = seed;
|
|
Height = height;
|
|
HeightClassify = heightClassify;
|
|
CurveOn = curveOn;
|
|
DetailOn = detailOn;
|
|
Knots = knots;
|
|
Anchors = anchors;
|
|
HMaxSeed = hMaxSeed;
|
|
EdgeAmpRaw = edgeAmpRaw;
|
|
MaxEdgeShiftRaw = maxEdgeShiftRaw;
|
|
HMin = hMin;
|
|
HMax = hMax;
|
|
ElapsedMs = elapsedMs;
|
|
Notes = notes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⚠ True when the two fields ARE the same array (curve off). Any pass that writes one while
|
|
/// reading the other must read both into locals first. See the type header.
|
|
/// </summary>
|
|
public bool FieldsAreAliased => ReferenceEquals(Height, HeightClassify);
|
|
|
|
/// <summary>Fraction of the RENDER field at or above the sea threshold.</summary>
|
|
public float LandFraction(float seaLevel)
|
|
{
|
|
long land = 0;
|
|
for (int x = 0; x < MapSize; x++)
|
|
for (int y = 0; y < MapSize; y++)
|
|
if (Height[x, y] >= seaLevel) land++;
|
|
return land / (float)((long)MapSize * MapSize);
|
|
}
|
|
}
|
|
}
|