COAST. The task's premise -- "the island edge is the steepest part" -- is
right, and the diagnostic locates it precisely: it is the SEABED, not the
land. HeightCurve.Apply returns early at and below sea level, so tasks
05-09 reshaped the land and never touched the water. Measured on the same
seed, distance-from-shore vs height:
land reaches 10 m seabed reaches 10 m gradient
curve OFF 31 px 31 px 0.254/0.258
curve ON (ships) 186 px 31 px 0.038/0.258
So the landward side is ALREADY the broad shallow shelf terrain_design.md
asks for -- 54% of the island sits under 14 m -- and flattening it further
would only make it mushy. What is left is a shoreline that shelves gently
on land and then drops 6.7x steeper the moment it goes under.
CoastShelf fixes that side: depth' = depth * (1 - 0.775*exp(-depth/100 m)).
The seabed leaves the waterline at 22.5% of its former gradient and
recovers smoothly, so deep water and the Trench keep their shape. Measured
after: 5 m depth at 43 px (was 15), 10 m at 75 (was 31), 30 m at 150 (was
94) -- a 2.4-2.9x wider shallows.
It is strictly positive for positive depth, so it CANNOT move the waterline
by one pixel. Biomes and water bodies come out bit-identical, which is why
the coast is separable from elongation in the batch rather than tangled
with it -- contrary to the task's expectation that all coast work moves
biomes.
ELONGATION. IslandAxisX/Y replace the 1.15/0.90 literals (the spine shares
AxisX, as it always shared the literal). Sized by replaying candidate
falloffs against real terrain rather than guessing -- the replay reproduces
the shipped extents exactly, bbox and land count.
That replay says the task's suggested 1.30/0.85 buys +2.0% aspect, because
THE ISLAND IS ALREADY TRENCH-CLAMPED IN X: it spans 90.5% of the map width,
and 1.15 -> 1.40 moves the west coast only 393 -> ~360 px. Aspect responds
almost entirely to AxisY, which costs land:
1.30/0.85 +2.0% aspect +2.9% land 1.30/0.80 +4.9% -2.2%
1.30/0.82 +3.7% aspect -0.1% land 1.30/0.75 +11.5% -7.3%
Default 1.30/0.78 -- a visible step (+~8% aspect, measured 1.137 -> 1.200)
at ~4% land. The developer's eye tunes it from the batch; the table above
is the price list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
237 lines
8.4 KiB
C#
237 lines
8.4 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 — 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";
|
||
|
||
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"];
|
||
|
||
// 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":
|
||
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}.");
|
||
}
|
||
}
|
||
}
|