Sparse discoverable islets from a low-frequency ocean noise layer, seeded
resolved+7607 at 14 undulations/island (~585 px blobs).
Placed by LERPING the seabed toward a target crest (34 m raw above sea)
rather than adding to it, so an islet can surface at any ambient depth
instead of only where the seafloor happens to be shallow. After the curve's
toe compresses them they stand 5-8 m above the water: low, sandy, ringed by
beach -- which is what the existing biome rules make of that height, with no
biome-rule change.
THE THREE CONSTRAINTS, each by construction rather than by hope:
Never merges with the mainland. The raise is EXACTLY zero wherever the
ambient water is shallower than 14 m, so any continuous path from the
mainland shore to an islet must cross that depth contour, and every pixel
on it is untouched water. Measured: 0 of 13 islets merged, closest
approach 78 px.
Never touches the Trench. A distance mask zeroes the layer by 0.86 of
half-map, and the ramp starts at 0.90. Measured max islet reach 0.795.
All four corners stay below sea.
Never spawns inland. Depth alone is not a test -- a deep lake and the
carved crater bay are also below sea. The pre-Trench falloff is the honest
discriminator (mainland coast sits near f = 0.66; islets need f > 0.72),
and it is axis-invariant, because elongation moves WHERE a given f occurs,
not the f at which land ends.
Two bugs found and fixed by measuring instead of trusting the first run:
1. The density dial treated [-1,1] as the noise's actual range. Simplex
is concentrated well inside it -- this field measures 0.050..0.958 --
so "top 6%" resolved to a threshold almost nothing cleared: the first
build raised 171 px on the entire map, none above sea. The threshold is
now CALIBRATED against the field's own distribution (quantile over a
1M-sample stride grid, deterministic from the seed), so the dial means
what it says whatever FastNoiseLite returns.
2. At 26/island the layer produced 77 islets under 200 px -- scatter, not
"a handful". Retuned to 14/island at density 0.02: 13 islets of
200-4792 px.
The pass prints its own islet area, so every run says what it did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
243 lines
8.8 KiB
C#
243 lines
8.8 KiB
C#
|
||
using Godot;
|
||
using Godot.Collections; // Required for Godot's built-in JSON parser
|
||
|
||
namespace IslaApocalypse.Core // Change this if your namespace is different
|
||
{
|
||
public static class ConfigManager
|
||
{
|
||
// Default Fallbacks
|
||
// public static int WorldSeed = 1063685222;
|
||
public static int WorldSeed = (int)GD.Randi(); // if 0 also randomizes
|
||
public static int MapSize = 8192;
|
||
public static float CraterRadius = 400f;
|
||
public static float DensityMultiplier = 1.0f; // <-- Replaces TownCount
|
||
public static int ChunkRadius = 24; // Default chunk size, can be overridden by config
|
||
|
||
// Iteration toggle: skip the ~25-min A* road pass entirely. The blueprint is
|
||
// still exported (road sections present but empty) — an iteration artifact,
|
||
// not a shippable world. Default false.
|
||
public static bool SkipRoads = false;
|
||
|
||
// Sea-level model (D-033, terrain-water task 04): "flat" = one scalar sea
|
||
// level everywhere (SeaLevelValue); "field" = the legacy latitude Lerp
|
||
// (0.26 north .. 0.15 south). Generator-only — the runtime never computes
|
||
// sea level. Default: flat 0.15.
|
||
public static string SeaLevelModel = "flat";
|
||
public static float SeaLevelValue = 0.15f;
|
||
|
||
// Height-redistribution curve (tasks 05–10, graduation M-7): "v5" is the
|
||
// task-09 gate's winner — the BALANCED terraced ascent with corner easing and
|
||
// modulated shelves; "off" is the raw legacy profile. "v1"–"v4" and the
|
||
// taste-batch tri-state ("v5-compact" lost, "v5-balanced" became plain "v5")
|
||
// are retired. Default: v5.
|
||
public static string TerrainCurve = "v5";
|
||
|
||
// Terrain detail passes (task 10): "v1" = shelf micro-relief + shelf-edge
|
||
// variation as one judged unit (requires the curve; no-op when it is off);
|
||
// "off" disables both. ShelfReliefAmp is the micro-relief amplitude in metres
|
||
// of OUTPUT height. ShelfEdgeVariation is the shelf-edge warp amplitude in
|
||
// metres of INPUT height — how far the shelf/riser boundary contour is
|
||
// displaced, not an elevation change; it is clamped at load time to the
|
||
// largest shift the curve's bands can absorb. Defaults: v1, 3 m, 12 m.
|
||
public static string TerrainDetail = "v1";
|
||
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, which still dropped ~6.7x
|
||
// steeper than the land it meets. "steep" is the pre-task-11 seabed, kept for
|
||
// A/B. The shelf cannot move the waterline, so biomes and water are 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.
|
||
// These MOVE THE COASTLINE and therefore move biomes, by design.
|
||
//
|
||
// OffshoreIslandDensity: fraction of the ocean noise field above the islet
|
||
// threshold. 0 disables the layer. Islets never touch the Trench and are held
|
||
// off the mainland by a depth moat.
|
||
public static string CoastProfile = "wide";
|
||
public static float IslandAxisX = 1.30f;
|
||
public static float IslandAxisY = 0.78f;
|
||
public static float OffshoreIslandDensity = 0.02f;
|
||
|
||
// The pre-task-11 axis ratios, so "steep"/legacy 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";
|
||
|
||
if (!FileAccess.FileExists(path))
|
||
{
|
||
GD.PrintErr("[ConfigManager] ServerConfig.json not found! Defaulting to 8K.");
|
||
return;
|
||
}
|
||
|
||
// Read the file
|
||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||
string content = file.GetAsText();
|
||
|
||
// Parse the JSON
|
||
var json = new Json();
|
||
var error = json.Parse(content);
|
||
if (error != Error.Ok)
|
||
{
|
||
GD.PrintErr($"[ConfigManager] JSON Parse Error: {json.GetErrorMessage()}");
|
||
return;
|
||
}
|
||
|
||
var data = (Dictionary)json.Data;
|
||
|
||
// Extract the Seed
|
||
if (data.ContainsKey("WorldSeed"))
|
||
{
|
||
WorldSeed = (int)data["WorldSeed"];
|
||
}
|
||
|
||
// Extract the MapProfile and run your Switch/Case logic!
|
||
string profile = "8K";
|
||
if (data.ContainsKey("MapProfile"))
|
||
{
|
||
profile = (string)data["MapProfile"];
|
||
}
|
||
|
||
// Extract the TownDensity and run your Switch/Case logic!
|
||
string townDensity = "Normal";
|
||
if (data.ContainsKey("TownDensity"))
|
||
{
|
||
townDensity = (string)data["TownDensity"];
|
||
}
|
||
// Extract the ChunkRadius
|
||
if (data.ContainsKey("ChunkRadius")) {
|
||
ChunkRadius = (int)data["ChunkRadius"];
|
||
}
|
||
|
||
// Extract the SkipRoads iteration toggle
|
||
if (data.ContainsKey("SkipRoads"))
|
||
{
|
||
SkipRoads = (bool)data["SkipRoads"];
|
||
}
|
||
|
||
// Extract the sea-level model
|
||
if (data.ContainsKey("SeaLevelModel"))
|
||
{
|
||
string model = (string)data["SeaLevelModel"];
|
||
if (model == "flat" || model == "field")
|
||
SeaLevelModel = model;
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown SeaLevelModel '{model}'. Keeping '{SeaLevelModel}'.");
|
||
}
|
||
if (data.ContainsKey("SeaLevelValue"))
|
||
{
|
||
SeaLevelValue = (float)data["SeaLevelValue"];
|
||
}
|
||
|
||
// Extract the terrain-curve gate
|
||
if (data.ContainsKey("TerrainCurve"))
|
||
{
|
||
string curve = (string)data["TerrainCurve"];
|
||
if (curve == "off" || curve == "v5")
|
||
TerrainCurve = curve;
|
||
else if (curve == "v5-balanced")
|
||
GD.PrintErr($"[ConfigManager] TerrainCurve 'v5-balanced' won the task-09 gate and is now plain \"v5\". Keeping '{TerrainCurve}'.");
|
||
else if (curve == "v5-compact")
|
||
GD.PrintErr($"[ConfigManager] TerrainCurve 'v5-compact' was retired by the task-09 verdict (BALANCED won). Keeping '{TerrainCurve}' — use \"v5\" or \"off\".");
|
||
else if (curve == "v1" || curve == "v2" || curve == "v3" || curve == "v4")
|
||
GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v5, tasks 09/10). Keeping '{TerrainCurve}' — use \"v5\" or \"off\".");
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
|
||
}
|
||
|
||
// Extract the terrain-detail gate + relief amplitude
|
||
if (data.ContainsKey("TerrainDetail"))
|
||
{
|
||
string detail = (string)data["TerrainDetail"];
|
||
if (detail == "off" || detail == "v1")
|
||
TerrainDetail = detail;
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown TerrainDetail '{detail}'. Keeping '{TerrainDetail}'.");
|
||
}
|
||
if (data.ContainsKey("ShelfReliefAmp"))
|
||
{
|
||
ShelfReliefAmp = (float)data["ShelfReliefAmp"];
|
||
}
|
||
if (data.ContainsKey("ShelfEdgeVariation"))
|
||
{
|
||
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"];
|
||
if (data.ContainsKey("OffshoreIslandDensity")) OffshoreIslandDensity = (float)data["OffshoreIslandDensity"];
|
||
|
||
// 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;
|
||
}
|
||
OffshoreIslandDensity = Mathf.Clamp(OffshoreIslandDensity, 0f, 0.5f);
|
||
|
||
switch (profile)
|
||
{
|
||
case "4K":
|
||
MapSize = 4096;
|
||
CraterRadius = 400f; // Scaled down crater
|
||
break;
|
||
case "6K":
|
||
MapSize = 6144;
|
||
CraterRadius = 600f;
|
||
break;
|
||
case "8K":
|
||
MapSize = 8192;
|
||
CraterRadius = 800f; // Your original crater size spread over an 8K map
|
||
break;
|
||
case "10K":
|
||
MapSize = 10240;
|
||
CraterRadius = 1000f;
|
||
break;
|
||
default:
|
||
GD.PrintErr($"[ConfigManager] Unknown MapProfile '{profile}'. Defaulting to 8K.");
|
||
MapSize = 8192;
|
||
CraterRadius = 800f;
|
||
break;
|
||
}
|
||
|
||
switch (townDensity)
|
||
{
|
||
case "Sparse":
|
||
DensityMultiplier = 0.5f;
|
||
break;
|
||
case "Normal":
|
||
DensityMultiplier = 1.0f;
|
||
break;
|
||
case "Dense":
|
||
DensityMultiplier = 2.0f;
|
||
break;
|
||
default:
|
||
GD.PrintErr($"[ConfigManager] Unknown TownDensity '{townDensity}'. Defaulting to Normal.");
|
||
DensityMultiplier = 1.0f;
|
||
break;
|
||
}
|
||
|
||
GD.Print($"[ConfigManager] Successfully loaded {profile} Profile. MapSize set to {MapSize}.");
|
||
}
|
||
}
|
||
}
|