using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐⭐ PASS 1 — the raw island height field. THE CROWN-JEWEL PORT.
/// → D-050 ("port, don't re-derive"), `Design - Rewrite - Extraction Manifest.md`.
///
/// Ported from the reference's MapGenerator.GenerateTopography pass-1 loop
/// (Tools/Scripts/MapGenerator.cs ~:553-619 at tag pre-rewrite-reference,
/// commit ab78883). Constants are READ FROM THAT SOURCE, not re-derived from a design
/// summary — that distinction is the whole point of D-050.
///
/// ═══ THE SIX ELEMENTS, IN THE REFERENCE'S EXECUTION ORDER ═══
///
/// 1. wobbled latitude scalar (ref ~:559-562)
/// 2. island falloff / mask (ref ~:567-574, 593)
/// 3. edge / coastline roughness (ref ~:577-578)
/// 4. southern sinker (ref ~:582-587)
/// 5. the Trench (ref ~:595-598)
/// 6. mountain spine (ref ~:605-612)
/// → combine and write (ref ~:618-619, 665-666)
///
/// ⚠⚠ THE ORDER IS LOAD-BEARING AND IS NOT AN IMPLEMENTATION DETAIL.
/// The sinker lands BEFORE the power, so its effect is superlinear. The Trench lands AFTER it,
/// so it is a raw additive wall the exponent never softens. preTrenchFalloff is captured
/// between them. Reordering any of it changes the island.
///
/// ═══ PASS 1b — THE SHELF AND THE ISLETS (chat2/05) ═══
///
/// The reference's pass-1 loop continues past the height write with two more task-11 passes:
/// the submarine COAST SHELF (~:621-640) and the OFFSHORE ISLET layer (~:641-664). v2 runs them
/// as a second sweep over the finished arrays — — with the same
/// per-pixel arithmetic in the same order, and then recomputes HMaxSeed AFTER them, as
/// the reference did. Config-gated; both default off (see TerrainGenConfig for why).
///
/// Nothing from pass 2 is here at all: no redistribution curve, no shelf detail, no erosion,
/// no rivers, no water bodies, no crater carve, no biomes.
///
public static class Topography
{
// ═══ CONSTANTS, ALL READ FROM THE REFERENCE SOURCE ═══
// Named rather than inlined so each one is greppable and has a home for its reason.
/// The squircle/ellipse blend weight. Reference: Mathf.Lerp(ellipse, squircle, 0.5f) (~:574).
private const float BlendSquircleWeight = 0.5f;
///
/// The ellipse's scale divisor: ellipticalPos.Length() / (MapSize / 1.3f) (~:572).
///
/// ⚠ AN UNCOMMENTED SHAPE DIAL IN THE REFERENCE, PORTED AS ONE. It sets the ellipse's
/// absolute scale against the squircle's, so it moves the coastline — but the reference
/// carried no comment and the vault's "50/50 blend of two falloffs" description does not
/// expose it. Ported verbatim because the island was tuned with it; named here so the next
/// reader knows it is a dial and not arithmetic. (chat1/00 §2.2.)
///
private const float EllipseScaleDivisor = 1.3f;
/// Edge-noise coordinate multiplier. Reference: GetNoise2D(x * 2.5f, y * 2.5f) (~:577).
private const float EdgeNoiseCoordScale = 2.5f;
/// Edge-noise amplitude. Reference: finalFalloff += (edgeNoise * 0.15f) * squircleFalloff (~:578).
private const float EdgeNoiseAmplitude = 0.15f;
/// Southern sinker threshold, as a fraction of the map. Reference: MapSize * 0.75f (~:582).
private const float SouthThresholdFraction = 0.75f;
/// Southern sinker depth. Reference: finalFalloff += southDepth * 0.6f (~:586).
private const float SouthSinkAmount = 0.6f;
/// The falloff exponent. Reference: Mathf.Pow(finalFalloff, 2.5f) (~:593).
private const float FalloffExponent = 2.5f;
/// Trench onset, as a fraction of the half-axis. Reference: if (distX > 0.90f) (~:597).
private const float TrenchOnset = 0.90f;
/// Trench slope. Reference: finalFalloff += (distX - 0.90f) * 15.0f (~:597-598).
private const float TrenchSlope = 15.0f;
/// Spine gate / fade anchor. Reference: if (temperature < 0.65f) and (0.65f - temperature) (~:606, 610).
private const float SpineLatitudeAnchor = 0.65f;
/// Spine southern-fade slope. Reference: Clamp((0.65f - temperature) * 4.0f, 0, 1) (~:610).
private const float SpineFadeSlope = 4.0f;
/// Spine cubic concentration. Reference: Mathf.Pow(mountainSpine, 3.0f) (~:611).
private const float SpineConcentration = 3.0f;
/// Spine amplitude in raw height units. Reference: * 0.6f (~:611).
private const float SpineAmplitude = 0.6f;
/// Latitude wobble amplitude. Reference: temperature += (tempNoise * 0.2f) - 0.1f (~:561) — i.e. ±0.1.
private const float LatitudeWobbleSpan = 0.2f;
///
/// The latitude noise's decorrelation offset, IN MAP WIDTHS.
///
/// ⚠⚠ THE ONE PLACE THIS PORT DELIBERATELY DOES NOT COPY THE REFERENCE'S LITERAL.
///
/// The reference wrote GetNoise2D(x + 1000, y + 1000) — an offset in RAW PIXELS.
/// Because frequency already carries a 1/MapSize normalization, a pixel offset does not hold
/// still across map sizes: 1000 px is 1.0 normalized units at 4K but 0.5 at 8K, so a resize
/// silently samples a DIFFERENT SLICE of the noise field. Same feature scale, different
/// realization — the same seed gives a different world at a different size, for no reason
/// anyone wrote down. (chat1/00 §4.2; the §6 tightening carried in from chat1/01.)
///
/// 1000 px at the reference's canonical MapSize of 8192 = 0.1220703125 map widths. Pinning
/// THAT reproduces the reference's realization exactly at 8K, and holds it still everywhere
/// else — which the reference did not.
///
private const float LatitudeNoiseOffsetMapWidths = 1000f / 8192f;
///
/// Generate the raw pass-1 height field.
///
public static Pass1Result Generate(TerrainGenConfig cfg)
{
ulong t0 = Time.GetTicksMsec();
GenerationScale scale = cfg.Scale;
int mapSize = cfg.MapSize;
FastNoiseLite noise = TerrainNoise.Create(cfg.Seed, scale);
var height = new float[mapSize, mapSize];
var preTrenchFalloff = new float[mapSize, mapSize];
var latitudeField = new float[mapSize, mapSize];
// ⚠ MAP-CENTRED, not crater-centred. Reference ~:550:
// Vector2 center = new Vector2(MapSize / 2.0f, MapSize / 2.0f);
// The crater's centre (_impactCenter) is a separate, randomly placed field used only in
// pass 2 and the biome pass. The spine and the mask both key off the MAP centre — the
// spine is a map-centred ridge, per `Design - Terrain - Mountain Spine.md`. Confirmed
// against source; no crater coupling exists in pass 1.
float centerX = mapSize / 2.0f;
float centerY = mapSize / 2.0f;
float axisX = cfg.IslandAxisX;
float axisY = cfg.IslandAxisY;
// Precomputed once — the reference recomputed these per pixel; identical results.
float latitudeNoiseOffset = scale.OffsetInMapWidths(LatitudeNoiseOffsetMapWidths);
float southThreshold = scale.Fraction(SouthThresholdFraction);
float halfSpan = mapSize / 2.0f;
float hMax = float.MinValue;
float hMin = float.MaxValue;
// ⚠ x IS THE OUTER LOOP, as in the reference. Numerically irrelevant here, but a
// PARALLEL port must reduce hMax/hMin rather than share them — noted before someone
// reaches for Parallel.For and quietly races on the running max.
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
// ═══ 1. WOBBLED LATITUDE SCALAR (ref ~:559-562) ═══
//
// ⚠⚠ THE REFERENCE CALLED THIS "temperature". IT IS NOT CLIMATE, AND THIS PORT
// WILL NOT CALL IT THAT.
//
// It is a stage-1-local LATITUDE FIELD that terrain GEOMETRY reads — the spine's
// southern fade is its only pass-1 consumer. The prototype named it temperature,
// then let biomes read the same array, and that shared name is a large part of
// how climate and terrain got fused in the first place. In the rewrite,
// CLIMATE IS STAGE 3 and classifies finished shape (D-049 §2, D-056); this
// scalar is stage 1 and must never be stored as, aliased to, or mistaken for it.
//
// The ±0.1 WOBBLE IS KEPT, and it is load-bearing: it makes the spine's southern
// terminus a low-frequency wave rather than a ruler-straight latitude line.
// Replacing this with a bare y/MapSize would straighten it — the exact opposite
// of the filed spine-meander want. (chat1/00 §2.3.)
float lat = (float)y / mapSize;
float latNoise = (noise.GetNoise2D(x + latitudeNoiseOffset, y + latitudeNoiseOffset) + 1.0f) / 2.0f;
lat += (latNoise * LatitudeWobbleSpan) - (LatitudeWobbleSpan / 2.0f);
latitudeField[x, y] = lat;
// ═══ 2. THE ISLAND FALLOFF / MASK (ref ~:567-574) ═══
float finalFalloff = 0f;
float squircleFalloff = 0f;
if (cfg.IslandFalloff)
{
// Squircle — Max(nx, ny), giving squared-off corners. ISLAND-anchored:
// the axis ratios are applied here.
float nx = Mathf.Abs(x - centerX) / (halfSpan * axisX);
float ny = Mathf.Abs(y - centerY) / (halfSpan * axisY);
squircleFalloff = Mathf.Max(nx, ny);
// Ellipse — vector length, giving a rounded shape.
var ellipticalPos = new Vector2((x - centerX) / axisX, (y - centerY) / axisY);
float ellipticalFalloff = ellipticalPos.Length() / (mapSize / EllipseScaleDivisor);
// 50/50 blend.
finalFalloff = Mathf.Lerp(ellipticalFalloff, squircleFalloff, BlendSquircleWeight);
}
// ═══ 3. EDGE / COASTLINE ROUGHNESS (ref ~:577-578) ═══
//
// ⚠ THE `* squircleFalloff` MODULATION IS PART OF IT AND IS EASY TO DROP.
// It makes the roughness ZERO AT THE ISLAND CENTRE and strongest at the rim —
// which is why it reads as coastline jitter instead of interior grain. The
// vault's description of this element omits it. (chat1/00 §2.2, §4.5.)
//
// The x2.5 coordinate multiplier is NOT an unscaled quantity: it multiplies
// coordinates that the already-normalized frequency then scales, so it is a
// fixed 2.5x RATIO to the base noise at every map size. (chat1/00 §4.2 —
// which corrected the vault on exactly this point.)
if (cfg.IslandFalloff && cfg.EdgeNoise)
{
float edgeNoise = (noise.GetNoise2D(x * EdgeNoiseCoordScale, y * EdgeNoiseCoordScale) + 1.0f) / 2.0f;
finalFalloff += (edgeNoise * EdgeNoiseAmplitude) * squircleFalloff;
}
// ═══ 4. THE SOUTHERN SINKER (ref ~:582-587) ═══
//
// Sinks the stretched land bridges in the bottom 25%. ⚠ BEFORE the power, so its
// effect is superlinear — +0.6 on a falloff already near 1 costs far more height
// than +0.6 near 0. Moving it after the power would change the southern coast.
if (cfg.IslandFalloff && cfg.SouthernSinker && y > southThreshold)
{
float southDepth = (y - southThreshold) / (mapSize - southThreshold);
finalFalloff += southDepth * SouthSinkAmount;
}
// ═══ ⭐ THE PHASE-2 SEAM — captured BEFORE the power and BEFORE the Trench ═══
// (ref ~:591). See Pass1Result.PreTrenchFalloff for why this exact point.
preTrenchFalloff[x, y] = finalFalloff;
if (cfg.IslandFalloff)
finalFalloff = Mathf.Pow(finalFalloff, FalloffExponent);
// ═══ 5. THE TRENCH (ref ~:595-598) ═══
//
// ⚠⚠ MAP-ANCHORED, NOT ISLAND-ANCHORED — distX/distY carry NO axis ratio, unlike
// nx/ny above. That is deliberate and load-bearing: the Trench is a guarantee
// about the MAP BORDER, not about the island's ellipse. It is what makes the
// flood fill's unchecked (0,0) ocean seed safe — the corner is guaranteed
// underwater by a x15 wall. DO NOT "unify" it with the mask's normalization.
// → `Design - Terrain - Island Mask.md`, chat1/00 §4.5.
//
// ⚠ The two clauses are ADDITIVE — a corner pixel takes both.
// ⚠ AFTER the power, so it is a raw additive wall the exponent never softens.
if (cfg.IslandFalloff && cfg.Trench)
{
float distX = Mathf.Abs(x - centerX) / halfSpan;
float distY = Mathf.Abs(y - centerY) / halfSpan;
if (distX > TrenchOnset) finalFalloff += (distX - TrenchOnset) * TrenchSlope;
if (distY > TrenchOnset) finalFalloff += (distY - TrenchOnset) * TrenchSlope;
}
// ═══ 6. THE MOUNTAIN SPINE (ref ~:605-612) ═══
//
// ⚠ THE REFERENCE'S `if (temperature < 0.65f)` GATE IS DROPPED, AND THE RESULT IS
// BIT-IDENTICAL. southernFade = Clamp((0.65 - lat) * 4, 0, 1) already reaches
// EXACTLY ZERO at lat = 0.65 — the same constant the gate tested — so the gate
// guarded a term that had already faded to nothing. It introduced no
// discontinuity and skipped no visible work. southernFade, not the gate, is what
// actually shapes the spine's southern end. (chat1/00 §2.3.)
//
// The gate is expressed below as `fade > 0`, which is the same predicate stated
// where it is true rather than where it is incidental.
float mountainSpine = 0f;
if (cfg.MountainSpine)
{
float southernFade = Mathf.Clamp((SpineLatitudeAnchor - lat) * SpineFadeSlope, 0.0f, 1.0f);
if (southernFade > 0f)
{
// ISLAND-anchored: axisX applies here (ref ~:608).
float distanceToCenterX = Mathf.Abs(x - centerX) / (halfSpan * axisX);
float crest = 1.0f - IslandFalloff.SmoothAbs(distanceToCenterX, IslandFalloff.CREST_EPSILON);
mountainSpine = Mathf.Pow(crest, SpineConcentration) * southernFade * SpineAmplitude;
}
}
// ═══ COMBINE AND WRITE (ref ~:618-619, 665-666) ═══
float rawBase = cfg.BaseNoise ? (noise.GetNoise2D(x, y) + 1.0f) / 2.0f : 0f;
float finalH = rawBase + mountainSpine - (finalFalloff * cfg.FalloffStrength);
if (finalH > hMax) hMax = finalH;
if (finalH < hMin) hMin = finalH;
height[x, y] = finalH;
// The reference's pass 1 CONTINUED HERE with the coast shelf (~:621-640) and the
// offshore islets (~:641-664). v2 runs them as PASS 1b, a second sweep over these
// arrays, immediately below — same per-pixel arithmetic, same order, and the
// slop guards need the whole field to see whole islands. → OffshorePass.
}
}
// ═══ PASS 1b — THE COAST SHELF + OFFSHORE ISLETS (chat2/05) ═══
//
// In place, on `height`. Config-gated; returns null when both are off, in which case
// nothing above is touched and this pass-1 output is bit-identical to Phase 1's.
//
// ⚠⚠ HMaxSeed IS RECOMPUTED AFTER THIS — closing chat2/00 Drift §2. The reference took
// `_hMaxSeed` after the shelf and islets inside the same loop; v2 used to take it before
// they existed. The curve normalizes its summit spike against this value, so the order is
// load-bearing even when the number does not move (an islet crest is ~34 m; a peak is
// ~290 m). Both values are carried so the report states whether it moved, not guesses.
float hMaxBeforeOffshore = hMax;
OffshorePass.Result offshore = OffshorePass.Apply(height, preTrenchFalloff, mapSize, cfg.Seed, cfg.SeaLevel, cfg);
if (offshore != null)
{
hMax = float.MinValue;
hMin = float.MaxValue;
for (int x = 0; x < mapSize; x++)
for (int y = 0; y < mapSize; y++)
{
float h = height[x, y];
if (h > hMax) hMax = h;
if (h < hMin) hMin = h;
}
}
return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField,
hMax, hMin, Time.GetTicksMsec() - t0,
hMaxSeedBeforeOffshore: hMaxBeforeOffshore,
isOffshoreIsland: offshore?.Tag,
islandHemisphere: offshore?.Hemi,
offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted,
notes: offshore?.Notes,
offshoreLedger: offshore?.ToLedger());
}
}
}