diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs index d56e0fe..8fb28b0 100644 --- a/Core/Scripts/ConfigManager.cs +++ b/Core/Scripts/ConfigManager.cs @@ -44,6 +44,27 @@ namespace IslaApocalypse.Core // Change this if your namespace is different public static float ShelfReliefAmp = 3.0f; public static float ShelfEdgeVariation = 12.0f; + // Island falloff shaping (task 11). + // + // CoastProfile: "wide" adds the submarine shelf. The height curve is identity + // at and below sea, so it never reached the seabed — measured, land rises from + // the shoreline at 0.038 m/px while the seabed drops at 0.258 m/px. "steep" is + // the pre-task-11 seabed, kept for A/B. The shelf cannot move the waterline, so + // biomes and water are bit-identical either way. Default: wide. + // + // IslandAxisX/Y: the falloff axis ratios (pre-task-11: 1.15 / 0.90). NOTE, + // measured: the island is already Trench-clamped in x at ~90% of the map width, + // so AxisX is a weak lever — aspect responds almost entirely to AxisY, which + // trades against land area. These MOVE THE COASTLINE, so they move biomes by + // design; elongated seeds have a new biome baseline. + public static string CoastProfile = "wide"; + public static float IslandAxisX = 1.30f; + public static float IslandAxisY = 0.78f; + + // The pre-task-11 axis ratios, so legacy/steep runs can restore them exactly. + public const float LEGACY_AXIS_X = 1.15f; + public const float LEGACY_AXIS_Y = 0.90f; + public static void LoadConfig() { string path = "res://ServerConfig.json"; @@ -147,6 +168,27 @@ namespace IslaApocalypse.Core // Change this if your namespace is different ShelfEdgeVariation = (float)data["ShelfEdgeVariation"]; } + // Extract the island-falloff dials (task 11) + if (data.ContainsKey("CoastProfile")) + { + string coast = (string)data["CoastProfile"]; + if (coast == "steep" || coast == "wide") + CoastProfile = coast; + else + GD.PrintErr($"[ConfigManager] Unknown CoastProfile '{coast}'. Keeping '{CoastProfile}'."); + } + if (data.ContainsKey("IslandAxisX")) IslandAxisX = (float)data["IslandAxisX"]; + if (data.ContainsKey("IslandAxisY")) IslandAxisY = (float)data["IslandAxisY"]; + + // The axis ratios divide map extents; a zero or negative one is a divide-by- + // zero that would silently produce an all-ocean map. Refuse it loudly. + if (IslandAxisX <= 0.01f || IslandAxisY <= 0.01f) + { + GD.PrintErr($"[ConfigManager] IslandAxisX/Y must be > 0.01 (got {IslandAxisX}/{IslandAxisY}). Restoring legacy {LEGACY_AXIS_X}/{LEGACY_AXIS_Y}."); + IslandAxisX = LEGACY_AXIS_X; + IslandAxisY = LEGACY_AXIS_Y; + } + switch (profile) { case "4K": diff --git a/Tools/Scripts/IslandFalloff.cs b/Tools/Scripts/IslandFalloff.cs index 710d4c1..c64ec40 100644 --- a/Tools/Scripts/IslandFalloff.cs +++ b/Tools/Scripts/IslandFalloff.cs @@ -9,14 +9,26 @@ using Godot; /// terrain, that crease is the single largest slope step anywhere on the map once /// the by-design Trench walls are excluded (top 4 of 5999 columns). SmoothAbs /// rounds the crest without moving its height. +/// +/// COAST SHELF — the height curve is identity at and below sea level, so it never +/// touched the SUBMARINE slope. Measured: land rises from the shoreline at +/// 0.038 m/px while the seabed drops at 0.258 m/px — the shoreline is a shelf on +/// the land side and a ramp on the sea side. CoastShelf compresses shallow depth +/// so the shallows extend much further out, leaving deep water and the Trench +/// essentially untouched. +/// +/// Every one of these is monotone in the sign of (sea - height): none of them can +/// turn water into land or land into water on its own. The coast shelf therefore +/// leaves the biome and water classification bit-identical, which is why it is +/// separable from elongation in the batch. /// public static class IslandFalloff { // ---- centre-line crest ------------------------------------------------ - // Rounding radius in normalized spine-width units (1.0 = MapSize/2 * 1.15 - // ~ 4710 px at 8K). 0.03 ~ 141 px: it cuts the crest's 1-px kink by 99.3% - // (-7.64e-04 -> -5.41e-06 raw, against a natural profile curvature of ~1.1e-07) - // while filling at most 3.9 m, decaying under 0.5 m by ~1100 px from the axis. + // Rounding radius in normalized spine-width units (1.0 = MapSize/2 * IslandAxisX + // ~ 4710 px at 8K with the default axis). 0.03 ~ 141 px: it cuts the crest's + // 1-px kink by 99.3% (-7.64e-04 -> -5.41e-06 raw) while filling at most 3.9 m, + // decaying under 0.5 m by ~1100 px from the axis. public const float CREST_EPSILON = 0.03f; /// @@ -30,4 +42,20 @@ public static class IslandFalloff float a = Mathf.Abs(d); return a * a / Mathf.Sqrt(a * a + epsilon * epsilon); } + + // ---- coast shelf ------------------------------------------------------ + // depth' = depth * (1 - STRENGTH * exp(-depth / SCALE_M)). + // At the shoreline the seabed starts at (1 - STRENGTH) of its former gradient and + // recovers smoothly, so the shallows widen and the deep ocean keeps its shape. + // C^inf everywhere, and strictly positive for positive depth — it cannot move the + // waterline by even one pixel. + public const float SHELF_STRENGTH = 0.775f; // 0 = off, ->1 = a flat lagoon + public const float SHELF_SCALE_M = 100f; // metres of depth over which it relaxes + + /// Remaps a positive depth in metres. Returns the new depth in metres. + public static float CoastShelf(float depthMetres) + { + if (depthMetres <= 0f) return depthMetres; + return depthMetres * (1f - SHELF_STRENGTH * Mathf.Exp(-depthMetres / SHELF_SCALE_M)); + } } diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs index b463852..5b533cc 100644 --- a/Tools/Scripts/MapGenerator.cs +++ b/Tools/Scripts/MapGenerator.cs @@ -60,6 +60,11 @@ public partial class MapGenerator : TextureRect private float _edgeAmpRaw; private float _maxEdgeShiftRaw; + // Task-11 island-falloff shaping: the submarine coast shelf. Acts on BELOW-SEA + // height in pass 1; the axis ratios that reshape the island itself are read + // straight from config in GenerateTopography. + private bool _coastWide; + // The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine, // pre-carve) — the v2 curve's per-seed spike normalizer. Computed in // GenerateTopography pass 1; recorded in TCRV (effective, guard applied). @@ -144,6 +149,10 @@ public partial class MapGenerator : TextureRect else if (ConfigManager.TerrainDetail == "v1" && !_curveOn) GD.Print("[MapGenerator] TerrainDetail v1 requires the curve — no-op with TerrainCurve off."); + _coastWide = ConfigManager.CoastProfile == "wide"; + GD.Print($"[MapGenerator] Island falloff: axis {ConfigManager.IslandAxisX:F2}x/{ConfigManager.IslandAxisY:F2}y, coast '{ConfigManager.CoastProfile}'" + + (_coastWide ? $" (shelf strength {IslandFalloff.SHELF_STRENGTH:F3}, scale {IslandFalloff.SHELF_SCALE_M:F0} m)." : ".")); + // THE CRATER FIX: Push it into the ocean (scales via percentage of MapSize!) // Supposedly! We will have to test this manually on other map sizes to confirm the crater is properly scaled and submerged on the north coast! float randomX = (float)GD.RandRange(0.35f, 0.65f); @@ -432,6 +441,8 @@ public partial class MapGenerator : TextureRect private void GenerateTopography() { Vector2 center = new Vector2(MapSize / 2.0f, MapSize / 2.0f); + float axisX = ConfigManager.IslandAxisX; + float axisY = ConfigManager.IslandAxisY; for (int x = 0; x < MapSize; x++) { for (int y = 0; y < MapSize; y++) @@ -443,11 +454,13 @@ public partial class MapGenerator : TextureRect _tempMap[x, y] = temperature; // --- 2. THE ORIGINAL ISLAND FALLOFF --- - float nx = Mathf.Abs(x - center.X) / (MapSize / 2.0f * 1.15f); - float ny = Mathf.Abs(y - center.Y) / (MapSize / 2.0f * 0.90f); + // Axis ratios are config dials since task 11 (were the literals + // 1.15 / 0.90). They move the coastline, so they move biomes. + float nx = Mathf.Abs(x - center.X) / (MapSize / 2.0f * axisX); + float ny = Mathf.Abs(y - center.Y) / (MapSize / 2.0f * axisY); float squircleFalloff = Mathf.Max(nx, ny); - - Vector2 ellipticalPos = new Vector2((x - center.X) / 1.15f, (y - center.Y) / 0.90f); + + Vector2 ellipticalPos = new Vector2((x - center.X) / axisX, (y - center.Y) / axisY); float ellipticalFalloff = ellipticalPos.Length() / (MapSize / 1.3f); float finalFalloff = Mathf.Lerp(ellipticalFalloff, squircleFalloff, 0.5f); @@ -480,7 +493,7 @@ public partial class MapGenerator : TextureRect float mountainSpine = 0f; if (temperature < 0.65f) { - float distanceToCenterX = Mathf.Abs(x - center.X) / (MapSize / 2.0f * 1.15f); + float distanceToCenterX = Mathf.Abs(x - center.X) / (MapSize / 2.0f * axisX); mountainSpine = 1.0f - IslandFalloff.SmoothAbs(distanceToCenterX, IslandFalloff.CREST_EPSILON); float southernFade = Mathf.Clamp((0.65f - temperature) * 4.0f, 0.0f, 1.0f); mountainSpine = Mathf.Pow(mountainSpine, 3.0f) * southernFade * 0.6f; @@ -492,6 +505,19 @@ public partial class MapGenerator : TextureRect // hMaxSeed) and the crater carve are applied in PASS 2 below. float rawBase = (_noise.GetNoise2D(x, y) + 1.0f) / 2.0f; float finalH = rawBase + mountainSpine - (finalFalloff * FalloffStrength); + + // --- 5. THE COAST SHELF (task 11) --- + // The height curve is identity at and below sea, so it never reached the + // seabed. Compress shallow depth so the shallows reach much further out. + // Strictly positive depth stays strictly positive, so the waterline — and + // with it every biome and water body — cannot move by a single pixel. + float seaHere = GetSeaLevel(temperature); + if (_coastWide && finalH < seaHere) + { + float depthM = (seaHere - finalH) * 251f; + finalH = seaHere - IslandFalloff.CoastShelf(depthM) / 251f; + } + if (finalH > _hMaxSeed) _hMaxSeed = finalH; _heightMap[x, y] = finalH; }