islaApocalypse-v2/Tools/Scripts/Pass1Result.cs
beezm 9af5c73b04 Phase 1: port the crown-jewel noise and pass-1, and render the island
Ports MapGenerator.GenerateTopography's pass-1 loop (reference ~:553-619 at tag
pre-rewrite-reference / ab78883) into Tools/. Working code and its tuned constants
carried over verbatim, read from source rather than re-derived from a design
summary (D-050). Six elements, in the reference's execution order — the order is
load-bearing: the sinker lands before the Pow (superlinear), the Trench after it
(a raw additive wall), preTrenchFalloff captured between them.

The fractal config is now PINNED. The reference set only NoiseType/Seed/Frequency,
so the island's entire fractal character was Godot 4.7.1 constructor defaults that
nothing recorded. Measured on 4.7.2 (NoiseDefaultsProbe): Fbm / 5 / 0.5 / 2.0 / 0.0
— identical to the 4.7.1 values, so pinning reproduces the look with no delta. The
terrain no longer depends on an engine default, and the probe makes a future drift
visible instead of silent.

Two deliberate departures from the reference's literals, both flagged in-code:
  - the latitude noise offset is expressed in map widths, not the reference's raw
    +1000 px, so a resize no longer samples a different slice of the noise field
    (chat1/00 §4.2). Pinned to 1000/8192 so the 8K realization is unchanged.
  - the `if (temperature < 0.65f)` spine gate is dropped as the provable no-op it
    is — southernFade already reaches exactly 0 at 0.65. Bit-identical.

The reference's "temperature" is ported as a latitude field and deliberately NOT
named temperature: it is stage-1-local input to terrain geometry, and climate is
stage 3 (D-049 §2, D-056). The ±0.1 wobble is kept — it is what stops the spine's
southern terminus being a ruler-straight line.

Deferred to Phase 2 with the seam open: coast shelf and offshore islets (below-sea,
judged once water renders). Pass1Result exposes PreTrenchFalloff at their exact
consumption point, plus HMaxSeed for the seed-dependent curve.

Render is a direct Godot.Image, not the SubViewport capture path — that machinery
exists to composite vector overlays and is why --headless hangs; Phase 1 has none.
Hypsometric above 0.15, bathymetric below, fixed ramp anchors so seeds and ablation
rungs are comparable. No biome words anywhere.

Verified: land fraction 51.5% identical at 1024, 2048 and 10240 — the scaling
discipline holds across a 10x resize. Full-size 10240 pass: 36.3 s.

No curve, erosion, rivers, water, crater, biomes, roads, or mesher.
2026-08-19 21:27:20 -04:00

88 lines
3.6 KiB
C#

namespace IslaApocalypse.Tools
{
/// <summary>
/// Everything pass 1 produces. → <see cref="Topography"/>.
///
/// ⭐ THREE OF THESE FIELDS ARE THE PHASE-2 SEAM. The reference computed them in pass 1 and
/// consumed them in pass 2; exposing them now is what keeps the deferred ports clean, and it
/// costs nothing to carry.
/// </summary>
public sealed class Pass1Result
{
/// <summary>Map side in columns.</summary>
public readonly int MapSize;
/// <summary>The resolved seed this was generated with. Always positive, always recorded.</summary>
public readonly int Seed;
/// <summary>
/// The raw pass-1 height field, <c>[x, y]</c>. Pre-curve, pre-erosion, pre-crater.
/// The reference's <c>finalH</c> (~:619) written to <c>_heightMap</c> (~:666).
/// </summary>
public readonly float[,] Height;
/// <summary>
/// ⭐ PHASE-2 SEAM — the falloff value captured BEFORE the <c>Pow(·, 2.5f)</c> and BEFORE the
/// Trench wall (reference ~:591, <c>preTrenchFalloff</c>).
///
/// It is the "how far past the island body are we" number, uncontaminated by the power's
/// curvature and by the Trench's additive ramp. The DEFERRED offshore-islet layer reads it
/// as its distance mask (<c>OffshoreZoneWeight</c>), which is the only reason it is captured
/// mid-computation rather than recomputed.
///
/// ⚠ A port that "tidies" the falloff chain into one expression destroys this capture point,
/// and the loss would not show up until Phase 2's offshore mask moved. It is exposed here so
/// that cannot happen quietly.
/// </summary>
public readonly float[,] PreTrenchFalloff;
/// <summary>
/// ⭐ PHASE-2 SEAM — the wobbled latitude scalar, <c>[x, y]</c>.
///
/// ⚠ THIS IS NOT A CLIMATE TEMPERATURE MAP. See <see cref="Topography"/> for the full note.
/// It is a stage-1-local latitude field that terrain GEOMETRY reads. Phase 2 needs it if the
/// latitude sea-level model is ever switched on (<c>SeaLevelModel = "field"</c>); under the
/// shipped flat model it has no consumer beyond the spine.
/// </summary>
public readonly float[,] LatitudeField;
/// <summary>
/// ⭐ PHASE-2 SEAM — the maximum height over the whole map (reference <c>_hMaxSeed</c>, ~:665).
///
/// The reference's v2+ redistribution curve is SEED-DEPENDENT: its spike maps
/// <c>[t4, hMaxSeed]</c> onto the peak band, so pass 2 cannot start until pass 1 has scanned
/// every pixel. That is precisely why the pass-1/pass-2 boundary is a hard one and not an
/// interleave. Carried here so Phase 2 inherits it rather than rediscovering the dependency.
/// </summary>
public readonly float HMaxSeed;
/// <summary>The minimum height. Not a reference field — carried for the renderer's ramp and the report.</summary>
public readonly float HMinSeed;
/// <summary>Wall-clock milliseconds the pass took.</summary>
public readonly ulong ElapsedMs;
public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs)
{
MapSize = mapSize;
Seed = seed;
Height = height;
PreTrenchFalloff = preTrenchFalloff;
LatitudeField = latitudeField;
HMaxSeed = hMaxSeed;
HMinSeed = hMinSeed;
ElapsedMs = elapsedMs;
}
/// <summary>Fraction of the map at or above the sea threshold. A cheap shape sanity number.</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);
}
}
}