Presentation only. No generation file is touched — Topography, TerrainNoise, IslandFalloff, Pass1Result, TerrainGenConfig and GenerationScale are all unchanged. Batch naming, fixed in code. The prefix is the AUTHORING TASK number, not a running counter: 04_review means "the batch task 04 authored", not "the fifth batch". It had already drifted — tasks 02 and 03 produced 00_smoke through 05_trophy_10240 across two tasks, so no folder name said which task made what. Now the task number is an explicit argument to ToolingPaths.BatchRoot(taskNumber, descriptor), which composes the prefix itself and REFUSES a descriptor that carries its own. All three tools take it as ISLA_TASK. Verified: ISLA_BATCH=05_foo is refused with a stated reason and exit 2, and creates no folder. Also fixed: an exception out of _Ready does not stop Godot — it logs and the process sits there with no main loop, so a misconfigured run HUNG rather than failing. A hang looks like slow work, which is worse than a crash. The batch tools now catch, print what was refused, and exit non-zero. Grayscale mode: normalize a field to its own [min,max]. This is the one place per-image normalization is correct — everywhere else anchors are fixed so images compare, but here the point is to see one field at full contrast. The range is printed and indexed so a shade reads back to a height. You cannot judge noise through a palette: a ramp bends the distribution, a hillshade adds shape the data does not have. The wide Costa-Rica palette, and the reframing the developer asked for: the pretty map is the WIDE GRADIENT RENDERED FLAT, and relief is no longer the hero. On raw pass-1 noise a hillshade has nothing coherent to shade, so it renders fine fractal bumpiness as fuzz that actively hides the elevation the colour is showing. Strong hillshade is retained as diagnostic_relief and labelled a dev view — its bumpiness is exaggerated slope, not extra terrain. Subtle relief is kept for comparison, with the honest note that it still fuzzes until Phase 2 carves coherent landforms. Legend: a colour bar with ticks drawn from the palette's own ramp, so it cannot drift from the map beside it. Ticks are RELATIVE height and the image says NOT METRES — the conversion is Phase 2's, and labelling it "m" would invent a fact in the artefact a reader most trusts. Text comes from a 5x7 bitmap font written for this, specifically so a legend does not drag in the SubViewport capture path.
198 lines
9.4 KiB
C#
198 lines
9.4 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.</summary>
|
||
public enum Kind { Atlas, Dusk, CostaRica }
|
||
|
||
/// <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
|
||
};
|
||
|
||
/// <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),
|
||
_ => (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);
|
||
}
|
||
}
|