rev 3 of the curve redesign. The developer's verdict on the 01 baseline was that the LOWLANDS ARE GOOD; the fault is the terracing above them. So this adds a second curve mode that preserves the low plain bit-for-bit and replaces everything above the flood line with one smooth monotone climb. Core/ContinuousCurve — piecewise, and the pieces have different loyalties: - at/below sea: identity, as ever. - above sea to K2: DELEGATES to HeightCurve's own toe+red branches. Not "equivalent" — the same code path, so the same floats. Oracle (d) holds it to that. - above the ceiling: a Fritsch-Carlson (PCHIP) monotone spline to the 420 m cap, C1-joined to the red band's exit slope. Monotone by construction for any ordered control points, which retires the 24-corner sweep; a 10k strict-increase sample runs per seed anyway, because "cannot fail" is worth a millisecond. - Build() REFUSES rather than degrades: a ceiling near the old bench, a drama that folds the summit under its own onset, control-point secants that are not strictly increasing (the no-magnet rule, enforced rather than hoped for). Only BENCH_*/PLATEAU_* are dropped. SEA/ORANGE_CEIL/RED_CEIL survive because they are the storm-ladder FLOOD TIERS and they live inside the preserved lowland; PEAK_CAP and the per-seed spikeMax normalization survive as the summit. Shelf detail is forced off in continuous mode: the flat benches it de-slabbed no longer exist, and painting noise on the climb now would pre-judge what erosion should carve. Oracle, all hard checks passing: - (a1) curve off is bit-identical to Phase 1's dump. - (a2) staircase mode is bit-identical to TASK 01's dump — the control is provably the control, not a re-derivation. (CurveBaselineTool is pinned to Staircase so the config default moving to Continuous cannot drift it.) - (d) lowlands bit-identical to the staircase over 3.6M cells, every continuous variant, both seeds. The lifted_WRONG bookend fails it on 1.6M cells, as intended. - (f) sea identity per CELL, not per count, including 67M cells at 8192. The finding, measured and recorded in the batch scratch: the massif SHRANK. Land above 100 m goes 14.9% -> 4.8%, above 220 m 4.5% -> 0.6%. A feather sweep to the practical floor recovers ~1.3 points, so this is structural, not a tuning miss: the staircase's highland area was an artifact of the bench and plateau acting as magnets, and a curve with no magnets preserves the raw distribution's bottom-heavy shape. "No terraces" and "the same land up high" are not both available from curve work alone. Exploration batch, not convergence. A tuning pass follows once a direction is picked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
225 lines
11 KiB
C#
225 lines
11 KiB
C#
using Godot;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// The hypsometric palettes — a continuous elevation tint, in the physical-atlas tradition.
|
||
///
|
||
/// ═══ ⚠⚠ THIS IS CARTOGRAPHY, NOT CLASSIFICATION ═══
|
||
///
|
||
/// There are NO BIOME WORDS here and there must never be any. Nothing in this file knows about
|
||
/// jungle, wasteland, desert or snow. It maps ONE FLOAT — height — onto a continuous colour
|
||
/// ramp. White at the top is snow-COLOURED cartography; green at the bottom is
|
||
/// lowland-COLOURED. Biomes are a stage-5 CLASSIFICATION of finished shape (D-049 §2, D-056),
|
||
/// and the entire point of the rewrite's seam is that terrain never learns their names.
|
||
///
|
||
/// ⚠ CONTINUOUS, NOT BANDED. The stops below are interpolated, never quantized. Hard bands
|
||
/// would read as discrete classes — exactly the visual language of a biome map, and exactly the
|
||
/// wrong thing to suggest.
|
||
///
|
||
/// ═══ ⚠ THE STOPS ARE IN ABSOLUTE HEIGHT, AND THEY ARE FIXED ═══
|
||
///
|
||
/// Every stop is a raw height value, not a normalized fraction, and the same stops are used for
|
||
/// every image in a batch. A map coloured on its own min/max cannot be compared with its
|
||
/// neighbour — and comparison is the entire point of a batch. Out-of-range values clamp; the
|
||
/// run prints the true min/max so saturation shows up as a number rather than a guess.
|
||
///
|
||
/// ═══ WHY THE STOPS SIT WHERE THEY DO — measured, not eyeballed ═══
|
||
///
|
||
/// Land height is very bottom-heavy. Measured over all four pinned seeds at 2048 (8.6M land
|
||
/// columns): p10 0.214 · p25 0.306 · MEDIAN 0.456 · p75 0.648 · p90 0.835 · p99 1.113 ·
|
||
/// p99.9 1.267 · max 1.415.
|
||
///
|
||
/// A ramp spread linearly from 0.15 to 1.45 would therefore spend 99% of its colour range on
|
||
/// 1% of the land, and the map would read as "green with a few white dots". So the land stops
|
||
/// are placed on PERCENTILES of the measured distribution instead — the same thing an atlas
|
||
/// plate does when it picks non-uniform contour intervals.
|
||
///
|
||
/// Sea is the mirror problem: depth runs to −6.7 but 43% of it is shallower than 0.5 and the
|
||
/// deep floor is featureless. So the bathymetric ramp is COMPRESSED — it is indexed by
|
||
/// <c>d / (d + k)</c>, which gives the shallow shelf most of the range and lets the abyss
|
||
/// flatten to one dark tone. The interesting bathymetry is near shore.
|
||
///
|
||
/// ⚠ THE SEABED IS RAW, AND IT IS SUPPOSED TO BE. The submarine coast shelf and the offshore
|
||
/// islets are DEFERRED Phase-2 items. Plain, steep bathymetry here is the deferral showing, not
|
||
/// a broken render.
|
||
/// </summary>
|
||
public static class ReliefPalette
|
||
{
|
||
/// <summary>
|
||
/// Named palettes. Gated so the look can be A/B'd like any other change.
|
||
/// ⚠ ProvisionalEven is chat2/02's stopgap for the continuous curve — see its array's note.
|
||
/// </summary>
|
||
public enum Kind { Atlas, Dusk, CostaRica, ProvisionalEven }
|
||
|
||
/// <summary>
|
||
/// Bathymetric compression constant, in raw height units. The sea ramp is indexed by
|
||
/// <c>d / (d + SeaCompression)</c>. Smaller = more range spent on the shallows.
|
||
/// At 0.35: depth 0.05 → 12.5% of the ramp, 0.5 → 59%, 2.0 → 85%, 6.7 → 95%.
|
||
/// </summary>
|
||
public const float SeaCompression = 0.35f;
|
||
|
||
// ---- LAND: (absolute height, colour), interpolated. Placed on measured percentiles. ----
|
||
private static readonly (float h, Color c)[] AtlasLand =
|
||
{
|
||
(0.150f, new Color(0.427f, 0.561f, 0.376f)), // shoreline — soft green
|
||
(0.220f, new Color(0.475f, 0.596f, 0.388f)), // p10 lowland green
|
||
(0.310f, new Color(0.549f, 0.635f, 0.404f)), // p25
|
||
(0.460f, new Color(0.667f, 0.686f, 0.435f)), // p50 yellow-green
|
||
(0.650f, new Color(0.780f, 0.729f, 0.494f)), // p75 khaki
|
||
(0.840f, new Color(0.808f, 0.678f, 0.478f)), // p90 tan
|
||
(0.980f, new Color(0.757f, 0.588f, 0.427f)), // p95 light brown
|
||
(1.120f, new Color(0.667f, 0.510f, 0.404f)), // p99 brown
|
||
(1.270f, new Color(0.706f, 0.667f, 0.639f)), // p99.9 grey rock
|
||
(1.450f, new Color(0.988f, 0.988f, 0.980f)), // peak white
|
||
};
|
||
|
||
private static readonly (float h, Color c)[] AtlasSea =
|
||
{
|
||
(0.000f, new Color(0.478f, 0.729f, 0.769f)), // waterline — pale teal
|
||
(0.125f, new Color(0.325f, 0.596f, 0.706f)), // shelf
|
||
(0.310f, new Color(0.196f, 0.451f, 0.627f)), // mid blue
|
||
(0.590f, new Color(0.114f, 0.310f, 0.510f)), // deep
|
||
(0.760f, new Color(0.063f, 0.196f, 0.376f)), // deeper
|
||
(0.880f, new Color(0.035f, 0.118f, 0.259f)), // navy
|
||
(1.000f, new Color(0.020f, 0.063f, 0.157f)), // abyss floor — flat
|
||
};
|
||
|
||
// ---- DUSK: warmer land, deeper cooler water. A genuine alternative, not a tweak. ----
|
||
private static readonly (float h, Color c)[] DuskLand =
|
||
{
|
||
(0.150f, new Color(0.353f, 0.494f, 0.353f)),
|
||
(0.220f, new Color(0.427f, 0.541f, 0.361f)),
|
||
(0.310f, new Color(0.529f, 0.588f, 0.376f)),
|
||
(0.460f, new Color(0.663f, 0.639f, 0.408f)),
|
||
(0.650f, new Color(0.796f, 0.690f, 0.451f)),
|
||
(0.840f, new Color(0.831f, 0.627f, 0.412f)),
|
||
(0.980f, new Color(0.780f, 0.522f, 0.361f)),
|
||
(1.120f, new Color(0.663f, 0.435f, 0.353f)),
|
||
(1.270f, new Color(0.678f, 0.612f, 0.588f)),
|
||
(1.450f, new Color(0.980f, 0.973f, 0.949f)),
|
||
};
|
||
|
||
private static readonly (float h, Color c)[] DuskSea =
|
||
{
|
||
(0.000f, new Color(0.412f, 0.686f, 0.706f)),
|
||
(0.125f, new Color(0.263f, 0.545f, 0.647f)),
|
||
(0.310f, new Color(0.145f, 0.396f, 0.565f)),
|
||
(0.590f, new Color(0.078f, 0.263f, 0.443f)),
|
||
(0.760f, new Color(0.043f, 0.161f, 0.318f)),
|
||
(0.880f, new Color(0.024f, 0.094f, 0.212f)),
|
||
(1.000f, new Color(0.012f, 0.047f, 0.125f)),
|
||
};
|
||
|
||
// ---- COSTA RICA: the WIDE gradient. The hero. -------------------------
|
||
//
|
||
// ⭐ The point of this ramp is RANGE. The atlas palettes are muted by design — they are
|
||
// imitating a printed plate, where ink is expensive and subtlety is the aesthetic. This one
|
||
// imitates the modern relief maps the developer pointed at (Costa Rica, Hispaniola), which
|
||
// use a wide, vivid gradient so that ELEVATION IS LEGIBLE AT A GLANCE without a hillshade
|
||
// doing the work.
|
||
//
|
||
// Same percentile placement as the others — the stops sit on the measured land distribution,
|
||
// not spread evenly — but the colours travel much further: deep green all the way to white,
|
||
// through a full yellow and orange stage the atlas ramps skip.
|
||
//
|
||
// ⚠ HONEST EXPECTATION: this terrain is mostly lowland (median 0.456 of a 1.415 peak), so it
|
||
// reads green→yellow with the spine in orange/brown/white. That is ACCURATE for a lowland
|
||
// island with a central range, not a palette failure. The fullest spread arrives when Phase
|
||
// 2's redistribution curve moves the height distribution; the palette is got right now so
|
||
// that Phase 2 inherits it rather than re-tuning it.
|
||
private static readonly (float h, Color c)[] CostaRicaLand =
|
||
{
|
||
(0.150f, new Color(0.078f, 0.361f, 0.216f)), // shoreline — deep green
|
||
(0.220f, new Color(0.153f, 0.475f, 0.243f)), // p10 green
|
||
(0.310f, new Color(0.322f, 0.596f, 0.259f)), // p25 mid green
|
||
(0.460f, new Color(0.569f, 0.706f, 0.278f)), // p50 yellow-green
|
||
(0.560f, new Color(0.784f, 0.796f, 0.298f)), // yellow
|
||
(0.650f, new Color(0.914f, 0.792f, 0.310f)), // p75 golden
|
||
(0.750f, new Color(0.933f, 0.667f, 0.271f)), // yellow-orange
|
||
(0.840f, new Color(0.882f, 0.541f, 0.239f)), // p90 orange
|
||
(0.980f, new Color(0.749f, 0.412f, 0.220f)), // p95 tan-brown
|
||
(1.120f, new Color(0.573f, 0.318f, 0.208f)), // p99 brown
|
||
(1.220f, new Color(0.427f, 0.271f, 0.216f)), // dark brown
|
||
(1.320f, new Color(0.667f, 0.635f, 0.620f)), // p99.9 grey
|
||
(1.450f, new Color(1.000f, 1.000f, 1.000f)), // peak white
|
||
};
|
||
|
||
private static readonly (float h, Color c)[] CostaRicaSea =
|
||
{
|
||
(0.000f, new Color(0.616f, 0.851f, 0.859f)), // waterline — bright shallow teal
|
||
(0.125f, new Color(0.376f, 0.694f, 0.780f)),
|
||
(0.310f, new Color(0.208f, 0.510f, 0.686f)),
|
||
(0.590f, new Color(0.114f, 0.341f, 0.557f)),
|
||
(0.760f, new Color(0.059f, 0.212f, 0.408f)),
|
||
(0.880f, new Color(0.031f, 0.125f, 0.278f)),
|
||
(1.000f, new Color(0.016f, 0.063f, 0.165f)), // abyss — flat
|
||
};
|
||
|
||
// ---- PROVISIONAL EVEN (chat2/02): the CostaRica colours, re-spaced evenly SEA → PEAK_CAP --
|
||
//
|
||
// ⚠⚠ PROVISIONAL, AND FLAGGED AS SUCH EVERYWHERE IT APPEARS. The continuous curve spreads
|
||
// land across the whole 0–420 m band, so the raw-percentile CostaRica stops no longer sit
|
||
// where the land is. This ramp is deliberately UNOPINIONATED: the same colour sequence at
|
||
// EVEN height spacing, so equal colour distance = equal height distance and nothing is
|
||
// emphasized. Final palette calibration waits until a curve profile is CHOSEN — placing
|
||
// stops on a distribution still being explored would bake taste into the instrument.
|
||
// Grayscale + the histogram stay the honest instruments; this is only so the relief plates
|
||
// are readable meanwhile.
|
||
private static readonly (float h, Color c)[] ProvisionalEvenLand = BuildProvisionalEven();
|
||
|
||
private static (float h, Color c)[] BuildProvisionalEven()
|
||
{
|
||
// SEA → PEAK_CAP through the single yardstick — no new literal for the cap.
|
||
float lo = 0.15f;
|
||
float hi = 0.15f + IslaApocalypse.Core.WorldScale.RawFromMetres(420f);
|
||
var stops = new (float h, Color c)[CostaRicaLand.Length];
|
||
for (int i = 0; i < CostaRicaLand.Length; i++)
|
||
stops[i] = (lo + (hi - lo) * i / (CostaRicaLand.Length - 1), CostaRicaLand[i].c);
|
||
return stops;
|
||
}
|
||
|
||
/// <summary>The hypsometric tint for a height, before any relief shading.</summary>
|
||
public static Color Tint(Kind kind, float height, float seaLevel)
|
||
{
|
||
var (land, sea) = Ramps(kind);
|
||
|
||
if (height >= seaLevel)
|
||
return Sample(land, height);
|
||
|
||
// Compressed bathymetry: index by d/(d+k), so the shallows get the range.
|
||
float depth = seaLevel - height;
|
||
float t = depth / (depth + SeaCompression);
|
||
return Sample(sea, t);
|
||
}
|
||
|
||
private static ((float h, Color c)[] land, (float h, Color c)[] sea) Ramps(Kind kind) => kind switch
|
||
{
|
||
Kind.Dusk => (DuskLand, DuskSea),
|
||
Kind.CostaRica => (CostaRicaLand, CostaRicaSea),
|
||
Kind.ProvisionalEven => (ProvisionalEvenLand, CostaRicaSea), // same sea; land re-spaced
|
||
_ => (AtlasLand, AtlasSea),
|
||
};
|
||
|
||
/// <summary>The land stops of a palette, for drawing a legend.</summary>
|
||
public static (float h, Color c)[] LandStops(Kind kind) => Ramps(kind).land;
|
||
|
||
private static Color Sample((float h, Color c)[] stops, float v)
|
||
{
|
||
if (v <= stops[0].h) return stops[0].c;
|
||
for (int i = 1; i < stops.Length; i++)
|
||
{
|
||
if (v <= stops[i].h)
|
||
{
|
||
float span = stops[i].h - stops[i - 1].h;
|
||
float local = span <= 0f ? 0f : (v - stops[i - 1].h) / span;
|
||
return stops[i - 1].c.Lerp(stops[i].c, local);
|
||
}
|
||
}
|
||
return stops[stops.Length - 1].c;
|
||
}
|
||
|
||
/// <summary>The land range the stops cover, for a run header / INDEX.</summary>
|
||
public static (float lo, float hi) LandAnchors => (AtlasLand[0].h, AtlasLand[AtlasLand.Length - 1].h);
|
||
}
|
||
}
|