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. /// /// ═══ ⚠ WHAT IS DELIBERATELY NOT PORTED HERE ═══ /// /// 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). Both are /// DEFERRED to Phase 2 by the developer's ruling — they act only on below-sea height and are /// judged once water renders. is exposed for them. /// /// 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 CONTINUES HERE with the coast shelf (~:621-640) and // the offshore islets (~:641-664). Both DEFERRED to Phase 2 — below-sea only, // judged once water renders. preTrenchFalloff above is their inlet. } } return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField, hMax, hMin, Time.GetTicksMsec() - t0); } } }