using Godot;
namespace IslaApocalypse.Tools
{
///
/// 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
/// d / (d + k), 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.
///
public static class ReliefPalette
{
///
/// 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.
///
public enum Kind { Atlas, Dusk, CostaRica, ProvisionalEven }
///
/// Bathymetric compression constant, in raw height units. The sea ramp is indexed by
/// d / (d + SeaCompression). 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%.
///
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;
}
/// The hypsometric tint for a height, before any relief shading.
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),
};
/// The land stops of a palette, for drawing a legend.
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;
}
/// The land range the stops cover, for a run header / INDEX.
public static (float lo, float hi) LandAnchors => (AtlasLand[0].h, AtlasLand[AtlasLand.Length - 1].h);
}
}