The gate's two river notes. FILL THE TRIBUTARIES. RiverTribWaterMinFlow defaults to 0, so every promoted tributary carries water its whole length (800 of 1574 reaches are now on tributaries, 65,364 of 268,863 wet px). The graceful tip survives: the fade zone is now always TaperPx long starting wherever water begins, so with a threshold it spans (WetFrom-Taper -> WetFrom) and with full watering it spans the first TaperPx from the HEAD. Fill is about LENGTH, taper is about the END. FILL THE CHANNEL, NOT THE FLOOR. The water surface is now RiverFillFraction (80%) of the LOCAL bed depth rather than a fixed height, floored by RiverWaterDepthM as an absolute minimum. A fixed height is wrong at both ends — a trickle in a 13 m mouth and over the rim at a 1.5 m head — where a fraction is right at every scale and cannot spill onto the plain. BANKS AS SHORES. The shoulder flares RiverBankFlare (3.2, was a hard 2x) half-widths with a smootherstep instead of smoothstep, so the rim leaves nearly tangent to the water plane. That alone cut too much where a course crosses a ridge (cells deeper than 20 m went 339 -> 1917, max 26 -> 37 m), so the SHOULDER may not lower a cell more than RiverBankMaxCutM (3 m) — measured against the PRE-PASS surface, because a per-write cap let overlapping stamps each take another 3 m and changed the carved volume by 1 m3 out of 1.29 M. Gentle ground still flares into a shore; a ridge crossing keeps steep walls, which is what a gorge looks like. Net result is better than BOTH predecessors: max cut 16.8 m (task 24: 26.0), zero cells past 20 m (task 24: 339), carve p95 5.26 m. Guards unchanged: BIOME oracle md5-identical to the task-22 baseline, 0 newly-below-sea, 0 below-sea cells modified, lowest carved cell exactly sea+margin, island top 457.65 m exact, crater core excluded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
482 lines
24 KiB
C#
482 lines
24 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;
|
||
|
||
// Hydraulic erosion (task 17, Phase C0): droplet-based carve-and-deposit on
|
||
// the RENDER height map only — the classify path (biomes/water) never sees
|
||
// it. "off" until the developer's gate approves it; the batch that turns it
|
||
// on does so explicitly. The three GOVERNORS hard-bound the pass:
|
||
// DropletCount (cost/detail), DropletLifetime (max steps per droplet),
|
||
// CarveCap (max erosion depth per cell, metres — the runaway-trench guard
|
||
// and what keeps erosion a detailing pass). ErosionSeaMargin is the flood
|
||
// guard: no cell is ever carved below sea + margin, and below-sea cells are
|
||
// never touched at all, so the rendered coastline cannot move. The remaining
|
||
// dials are the standard droplet-model strength constants; slopes/amounts
|
||
// are in METRES (1 raw height unit = 251 m).
|
||
// Task-18 defaults tune for a DRAINAGE HIERARCHY: long-lived, committed
|
||
// droplets (lifetime 384 at inertia 0.35, evaporation 0.004) travel far
|
||
// enough down a flank that their paths overlap and deepen shared low lines
|
||
// into trunk channels, instead of dying as independent 48-px scratches;
|
||
// the carve cap is raised to 15 m so trunks can separate from the fine
|
||
// rills instead of both piling up against the same ceiling. A modest
|
||
// erode rate keeps the total material moved in detailing range.
|
||
// ErosionDepositCap is governor 4 (task 18): brush-spread deposition alone
|
||
// does not bound a spike once droplets carry long-path loads.
|
||
public static string Erosion = "off";
|
||
public static int ErosionDropletCount = 250000;
|
||
public static int ErosionDropletLifetime = 384;
|
||
public static float ErosionCarveCap = 15.0f; // m per cell
|
||
public static float ErosionDepositCap = 6.0f; // m per cell; <= 0 = unbounded
|
||
public static float ErosionSeaMargin = 0.5f; // m above sea, carve floor
|
||
public static int ErosionBrushRadius = 2; // px
|
||
public static float ErosionInertia = 0.35f;
|
||
public static float ErosionCapacity = 4.0f;
|
||
public static float ErosionMinSlope = 0.02f; // m per px, capacity floor
|
||
public static float ErosionErodeRate = 0.12f;
|
||
public static float ErosionDepositRate = 0.15f;
|
||
public static float ErosionEvaporation = 0.004f;
|
||
public static float ErosionGravity = 4.0f;
|
||
|
||
// How erosion treats the crater surrounds (task 19). The task-17 hard
|
||
// 1.2 × CraterRadius cutoff left a visible un-eroded disc: measured, the carve
|
||
// writes only inside 0.80 × and its displacement is exactly 0 beyond that, so
|
||
// 620 811 LAND cells of ordinary terrain were being held smooth for no
|
||
// geometric reason. Now only the deep strike core is protected.
|
||
// "full" — full-strength erosion right up to the core boundary. The
|
||
// crater formed after the terrain and has weathered since.
|
||
// "feather" — erosion ramps 0→full across CraterErosionCore →
|
||
// CraterErosionFeather (the detail pass's shape), so the crater
|
||
// reads as younger, less-weathered, and there is no seam at all.
|
||
// Radii are FACTORS of CraterRadius. The flooded bay and its sea connection do
|
||
// NOT depend on these: below-sea cells are read-only in both directions.
|
||
public static string CraterErosionMode = "feather";
|
||
public static float CraterErosionCore = CRATER_EROSION_CORE_DEFAULT;
|
||
public static float CraterErosionFeather = CRATER_EROSION_FEATHER_DEFAULT;
|
||
// 0.80 = the carve's OWN extent (MapGenerator's physicalCraterRadius). Keeping the
|
||
// core at least this wide is what makes erosion and the carve touch DISJOINT cells,
|
||
// which is what keeps the post-carve flood guard exactly zero: the carve runs after
|
||
// erosion and scales height toward the sea target, so it AMPLIFIES any erosion delta
|
||
// inside its radius and can push a hair-above-sea cell across the waterline. Measured
|
||
// at a 0.50 core: 79 cells newly below the rendered sea, 113 310 below-sea cells
|
||
// disturbed. A smaller core reclaims nothing extra either — the over-protected annulus
|
||
// is 0.80x-1.2x, entirely outside the carve.
|
||
public const float CRATER_EROSION_CORE_DEFAULT = 0.80f;
|
||
public const float CRATER_EROSION_FEATHER_DEFAULT = 1.05f;
|
||
|
||
// Rivers (task 22, C0b part 2a): carve the frozen task-21b river plan's
|
||
// beds into the RENDER map — ocean trunks, routed giants (lowland reach to
|
||
// the sea), lake-enders. NO WATER yet (part 2b). "off" until the routing-
|
||
// style gate; the A/B batch turns it on explicitly. RiverRoutingStyle picks
|
||
// the lowland routing for routed giants: "short" heads direct (lightly
|
||
// terrain-aware), "lowground" follows the lowest ground and wanders like a
|
||
// real river — the task-22 gate decides which ships; "lowground" is the
|
||
// provisional default pending that verdict. Width/depth scales are taste
|
||
// dials on the flow-proportional bed profile. LOWGROUND is the LOCKED
|
||
// default — the task-22/23 gate verdict ("short" stays available for the
|
||
// record). RiverSeaMargin is the bed's
|
||
// absolute floor above sea — the erosion flood-guard discipline: no river
|
||
// bed may create inland below-sea cells, so the rendered coastline cannot
|
||
// move even with rivers carved.
|
||
// RiverStepDropM/RiverWaterDepthM (task 23): the stepped-water dials — each
|
||
// river is a chain of flat water-body reaches; a new reach starts every
|
||
// StepDrop metres of bed descent and sits WaterDepth metres above its bed.
|
||
// Smaller drop = more, finer steps = smoother water (the smoothing dial;
|
||
// tilted continuous-slope water is the deferred model B).
|
||
public static string Rivers = "off";
|
||
public static string RiverRoutingStyle = "lowground";
|
||
public static float RiverWidthScale = 1.75f; // widened at the task-23 gate's ask
|
||
// Task 24 (the gate's polish): finer steps read as a descending river rather
|
||
// than a pond staircase; deeper water sits contained in its banks.
|
||
// RiverTribWaterMinFlow is the drainage a TRIBUTARY reach needs to carry
|
||
// water (0 = water them end to end); above it the water TAPERS to dry over
|
||
// RiverTribTaperPx so a stream head fades instead of ending in a wall.
|
||
// RiverLakeMinTargetPx is the smallest water body a lake-ender may target.
|
||
// Task 25: RiverFillFraction fills the channel to a fraction of its LOCAL bed
|
||
// depth (a fixed height was a trickle at deep mouths and overtopped shallow
|
||
// heads); RiverWaterDepthM stays as the absolute minimum. RiverBankFlare
|
||
// widens the bank shoulder (half-widths beyond the channel) and it now uses a
|
||
// smootherstep, so water meets land as a shore instead of a wall.
|
||
// RiverTribWaterMinFlow defaults to 0 — tributaries carry water their full
|
||
// promoted length (the gate's preference), still fading at the tip.
|
||
public static float RiverStepDropM = 0.6f;
|
||
public static float RiverWaterDepthM = 1.0f;
|
||
public static float RiverFillFraction = 0.80f;
|
||
public static float RiverBankFlare = 3.2f;
|
||
public static float RiverBankMaxCutM = 3.0f; // shoulder-only cut cap (task 25)
|
||
public static int RiverTribWaterMinFlow = 0;
|
||
public static int RiverTribTaperPx = 120;
|
||
public static int RiverLakeMinTargetPx = 20000;
|
||
public static float RiverDepthScale = 1.5f; // deepened at the task-24 gate's ask
|
||
public static float RiverSeaMargin = 0.2f; // m above sea, bed floor
|
||
|
||
// 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. These MOVE THE COASTLINE and
|
||
// therefore move biomes, so the DEFAULT is the shape the gate approved.
|
||
// Task 11 shipped 1.30/0.78 as the default and the developer's gate REJECTED
|
||
// that elongation; the defaults are back to 1.15/0.90 so that omitting the
|
||
// keys can no longer silently produce the rejected island (task 12 §4).
|
||
// NOTE, measured in task 11: 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.
|
||
//
|
||
// 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 = LEGACY_AXIS_X; // 1.15 — the gate's verdict
|
||
public static float IslandAxisY = LEGACY_AXIS_Y; // 0.90
|
||
public static float OffshoreIslandDensity = 0.02f;
|
||
|
||
// The gate-approved island shape, named so the defaults above and the bad-value
|
||
// restore below both point at one place. (`const`, so using it in a field
|
||
// initialiser declared earlier resolves at compile time.)
|
||
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";
|
||
|
||
// Iteration override (task 22): ISLA_SERVER_CONFIG names an alternate
|
||
// config FILE to load instead of the repo's ServerConfig.json. Batch and
|
||
// A/B runs point this at a scratch config, so the developer's live
|
||
// ServerConfig.json is never written by tooling again — the whole
|
||
// backup/restore dance (and its task-19 near-miss) goes away. Loud, so
|
||
// a forgotten env var cannot silently masquerade as the repo config.
|
||
string envPath = OS.GetEnvironment("ISLA_SERVER_CONFIG");
|
||
if (!string.IsNullOrEmpty(envPath))
|
||
{
|
||
if (FileAccess.FileExists(envPath))
|
||
{
|
||
path = envPath;
|
||
GD.Print($"[ConfigManager] ⚠ ISLA_SERVER_CONFIG override: loading '{envPath}' (NOT the repo ServerConfig.json).");
|
||
}
|
||
else
|
||
GD.PrintErr($"[ConfigManager] ISLA_SERVER_CONFIG set but '{envPath}' does not exist — falling back to the repo config.");
|
||
}
|
||
|
||
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 erosion gate + dials (task 17)
|
||
if (data.ContainsKey("Erosion"))
|
||
{
|
||
string erosion = (string)data["Erosion"];
|
||
if (erosion == "off" || erosion == "v1")
|
||
Erosion = erosion;
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown Erosion '{erosion}'. Keeping '{Erosion}'.");
|
||
}
|
||
if (data.ContainsKey("ErosionDropletCount")) ErosionDropletCount = (int)data["ErosionDropletCount"];
|
||
if (data.ContainsKey("ErosionDropletLifetime")) ErosionDropletLifetime = (int)data["ErosionDropletLifetime"];
|
||
if (data.ContainsKey("ErosionCarveCap")) ErosionCarveCap = (float)data["ErosionCarveCap"];
|
||
if (data.ContainsKey("ErosionDepositCap")) ErosionDepositCap = (float)data["ErosionDepositCap"];
|
||
if (data.ContainsKey("ErosionSeaMargin")) ErosionSeaMargin = (float)data["ErosionSeaMargin"];
|
||
if (data.ContainsKey("ErosionBrushRadius")) ErosionBrushRadius = (int)data["ErosionBrushRadius"];
|
||
if (data.ContainsKey("ErosionInertia")) ErosionInertia = (float)data["ErosionInertia"];
|
||
if (data.ContainsKey("ErosionCapacity")) ErosionCapacity = (float)data["ErosionCapacity"];
|
||
if (data.ContainsKey("ErosionMinSlope")) ErosionMinSlope = (float)data["ErosionMinSlope"];
|
||
if (data.ContainsKey("ErosionErodeRate")) ErosionErodeRate = (float)data["ErosionErodeRate"];
|
||
if (data.ContainsKey("ErosionDepositRate")) ErosionDepositRate = (float)data["ErosionDepositRate"];
|
||
if (data.ContainsKey("ErosionEvaporation")) ErosionEvaporation = (float)data["ErosionEvaporation"];
|
||
if (data.ContainsKey("ErosionGravity")) ErosionGravity = (float)data["ErosionGravity"];
|
||
|
||
// Governor bounds are enforced HERE, loudly, so a bad dial is a refused
|
||
// dial rather than a silently absurd generation. The clamps are wide —
|
||
// they exist to catch typos (an extra zero), not to tune.
|
||
int rawCount = ErosionDropletCount; int rawLife = ErosionDropletLifetime;
|
||
float rawCap = ErosionCarveCap;
|
||
ErosionDropletCount = Mathf.Clamp(ErosionDropletCount, 0, 50_000_000);
|
||
ErosionDropletLifetime = Mathf.Clamp(ErosionDropletLifetime, 1, 4096);
|
||
ErosionCarveCap = Mathf.Clamp(ErosionCarveCap, 0f, 60f);
|
||
if (rawCount != ErosionDropletCount || rawLife != ErosionDropletLifetime || rawCap != ErosionCarveCap)
|
||
GD.PrintErr($"[ConfigManager] Erosion governor out of bounds — clamped: count {rawCount}->{ErosionDropletCount}, lifetime {rawLife}->{ErosionDropletLifetime}, cap {rawCap}->{ErosionCarveCap} m.");
|
||
// Rivers gate + dials (task 22)
|
||
if (data.ContainsKey("Rivers"))
|
||
{
|
||
string rv = (string)data["Rivers"];
|
||
if (rv == "off" || rv == "v1")
|
||
Rivers = rv;
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown Rivers '{rv}'. Keeping '{Rivers}'.");
|
||
}
|
||
if (data.ContainsKey("RiverRoutingStyle"))
|
||
{
|
||
string st = (string)data["RiverRoutingStyle"];
|
||
if (st == "short" || st == "lowground")
|
||
RiverRoutingStyle = st;
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown RiverRoutingStyle '{st}'. Keeping '{RiverRoutingStyle}'.");
|
||
}
|
||
if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"];
|
||
if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"];
|
||
if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"];
|
||
if (data.ContainsKey("RiverStepDropM")) RiverStepDropM = (float)data["RiverStepDropM"];
|
||
if (data.ContainsKey("RiverWaterDepthM")) RiverWaterDepthM = (float)data["RiverWaterDepthM"];
|
||
if (data.ContainsKey("RiverTribWaterMinFlow")) RiverTribWaterMinFlow = (int)data["RiverTribWaterMinFlow"];
|
||
if (data.ContainsKey("RiverTribTaperPx")) RiverTribTaperPx = (int)data["RiverTribTaperPx"];
|
||
if (data.ContainsKey("RiverLakeMinTargetPx")) RiverLakeMinTargetPx = (int)data["RiverLakeMinTargetPx"];
|
||
if (data.ContainsKey("RiverFillFraction")) RiverFillFraction = (float)data["RiverFillFraction"];
|
||
if (data.ContainsKey("RiverBankFlare")) RiverBankFlare = (float)data["RiverBankFlare"];
|
||
RiverFillFraction = Mathf.Clamp(RiverFillFraction, 0.1f, 1f);
|
||
if (data.ContainsKey("RiverBankMaxCutM")) RiverBankMaxCutM = (float)data["RiverBankMaxCutM"];
|
||
RiverBankFlare = Mathf.Clamp(RiverBankFlare, 1.2f, 8f);
|
||
RiverBankMaxCutM = Mathf.Clamp(RiverBankMaxCutM, 0f, 30f);
|
||
RiverTribWaterMinFlow = Mathf.Max(RiverTribWaterMinFlow, 0);
|
||
RiverTribTaperPx = Mathf.Clamp(RiverTribTaperPx, 0, 2000);
|
||
RiverLakeMinTargetPx = Mathf.Max(RiverLakeMinTargetPx, 0);
|
||
RiverStepDropM = Mathf.Clamp(RiverStepDropM, 0.25f, 10f);
|
||
RiverWaterDepthM = Mathf.Clamp(RiverWaterDepthM, 0.2f, 5f);
|
||
RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f);
|
||
RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f);
|
||
RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f);
|
||
|
||
// Crater erosion treatment (task 19)
|
||
if (data.ContainsKey("CraterErosionMode"))
|
||
{
|
||
string cm = (string)data["CraterErosionMode"];
|
||
if (cm == "full" || cm == "feather")
|
||
CraterErosionMode = cm;
|
||
else
|
||
GD.PrintErr($"[ConfigManager] Unknown CraterErosionMode '{cm}'. Keeping '{CraterErosionMode}'.");
|
||
}
|
||
if (data.ContainsKey("CraterErosionCore")) CraterErosionCore = (float)data["CraterErosionCore"];
|
||
if (data.ContainsKey("CraterErosionFeather")) CraterErosionFeather = (float)data["CraterErosionFeather"];
|
||
CraterErosionCore = Mathf.Clamp(CraterErosionCore, 0f, 3f);
|
||
CraterErosionFeather = Mathf.Clamp(CraterErosionFeather, 0f, 4f);
|
||
// A feather band that does not extend past the core is not a band; say so
|
||
// rather than silently behaving like "full".
|
||
if (CraterErosionMode == "feather" && CraterErosionFeather <= CraterErosionCore)
|
||
{
|
||
GD.PrintErr($"[ConfigManager] CraterErosionFeather {CraterErosionFeather:F2} must exceed " +
|
||
$"CraterErosionCore {CraterErosionCore:F2} — the ramp would have zero width. " +
|
||
$"Restoring {CRATER_EROSION_FEATHER_DEFAULT:F2}.");
|
||
CraterErosionFeather = CRATER_EROSION_FEATHER_DEFAULT;
|
||
}
|
||
|
||
// Negative is meaningless; 0 is the documented "unbounded" escape hatch.
|
||
ErosionDepositCap = Mathf.Clamp(ErosionDepositCap, 0f, 60f);
|
||
ErosionSeaMargin = Mathf.Clamp(ErosionSeaMargin, 0f, 5f);
|
||
ErosionBrushRadius = Mathf.Clamp(ErosionBrushRadius, 0, 8);
|
||
ErosionInertia = Mathf.Clamp(ErosionInertia, 0f, 0.99f);
|
||
ErosionCapacity = Mathf.Max(ErosionCapacity, 0f);
|
||
ErosionMinSlope = Mathf.Max(ErosionMinSlope, 0f);
|
||
ErosionErodeRate = Mathf.Clamp(ErosionErodeRate, 0f, 1f);
|
||
ErosionDepositRate = Mathf.Clamp(ErosionDepositRate, 0f, 1f);
|
||
ErosionEvaporation = Mathf.Clamp(ErosionEvaporation, 0f, 0.5f);
|
||
ErosionGravity = Mathf.Max(ErosionGravity, 0f);
|
||
|
||
// 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}.");
|
||
}
|
||
}
|
||
}
|