islaApocalypse/Core/Scripts/ConfigManager.cs
beezm 492b56a87c feat: shelf-edge variation -- the red-line scalloping (terrain-water task 10)
PASS B, replacing the reverted incision. The developer's sketch asked for
the shelf/riser BOUNDARY to be organic, not for water: notches, coves and
small peninsulas where a flat shelf meets its riser, instead of the clean
oval contour the curve produces.

Mechanism (the task's preferred "boundary warp", in its most
monotonic-safe form): every shelf/riser boundary is the contour where the
raw height crosses K3, K4 or K5, so the boundary is warped by SLIDING
THOSE THREE KNOTS per column -- edgeShift = simplex(resolvedSeed + 7507,
12 per island width) x ShelfEdgeVariation. The contours then wander in and
out of the terrain instead of tracing an iso-height line. Noise-warped by
construction, so there is no grid direction for an artifact to line up on
-- the failure mode of the thing this replaces.

Why slide knots rather than perturb a weight or the input height:
monotonicity becomes structural instead of conditional. The curve is
strictly monotonic for ANY ordered knot set, so no derivative bound, no
amplitude-vs-feather-width tuning, no way for a dial to invert a column.
MaxEdgeShift keeps the set ordered (half the smallest margin to a fixed
knot = 12.3 m of input height for the v5 knots); the config dial is
clamped to it, loudly. AssertMonotonic now sweeps 24 corners -- the 8
modulation extremes x {-max, 0, +max} shift -- and checks the bound first.

K1, K2 and K6 never move, which buys the guarantees exactly rather than
statistically: below K2 and above K6 a warped column is bit-identical to
an unwarped one, so the red ceiling still floors every shelf edge (storm
ladder safe), the 420 m cap still caps, and the toe and summit spikes are
untouched. The block slides rigidly, so the bench and mid-riser keep their
exact widths -- shelf interiors stay flat, riser interiors keep their
profile, and only the foothill riser and plateau stretch to absorb it.
The micro-relief mask takes the same shift, so pass A's skin follows the
shelf wherever pass B moved its edge.

Dial ShelfEdgeVariation (default 5 m of INPUT height -- a boundary
displacement, not an elevation change) under the existing TerrainDetail
gate; both passes stay one judged unit. TDTL v2 body records relief and
edge amp/frequency/seed-offset plus the applied clamp bound; writer,
parser and harness follow. No blueprint was written between the revert
and this commit, so v2 only ever means relief + edge warp on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:31:41 -04:00

195 lines
6.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 0510, 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 that keeps the curve's knots ordered. Defaults: v1, 3 m, 5 m.
public static string TerrainDetail = "v1";
public static float ShelfReliefAmp = 3.0f;
public static float ShelfEdgeVariation = 5.0f;
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"];
}
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}.");
}
}
}