feat: curve v2 — per-seed spike normalization, u4 wall, 420 m ceiling (terrain-water task 06)
Per the task-05 hillshade-gate verdict: lowlands frozen (identity/toe/ rise/shoulder/plateau byte-unchanged); the spike now maps [t4, hMaxSeed] — each seed's own raw pre-curve maximum, computed in a new pass-1 over GenerateTopography — onto the peak band, so every island reaches the ceiling (v1's pooled-max domain left mid-range seeds at 110–175 m). Spike stiffened to 0.1u + 0.9u^4; peak cap raised to 420 m above sea (1.82869). Degenerate near-flat guard: spike domain floored at t4 + 0.01. The curve is now SEED-DEPENDENT: hMaxSeed is a pure parameter (D-035), recorded in TCRV (field renamed HMaxCal -> SpikeMax, same byte layout; v1 semantics = pooled max, v2 = per-seed), and the monotonicity assertion runs per generation against the effective curve. TerrainCurve gate: "off"|"v2" (default v2); "v1" retired with a loud config error (task-05 blueprints are regenerable). Classify path untouched — the biome/water oracle must hold unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
46a991f352
commit
ac67c27952
7 changed files with 116 additions and 62 deletions
|
|
@ -128,7 +128,7 @@ namespace IslaApocalypse.Core
|
||||||
{
|
{
|
||||||
writer.Write(c.Version);
|
writer.Write(c.Version);
|
||||||
writer.Write(c.T1); writer.Write(c.T2); writer.Write(c.T3); writer.Write(c.T4);
|
writer.Write(c.T1); writer.Write(c.T2); writer.Write(c.T3); writer.Write(c.T4);
|
||||||
writer.Write(c.HMaxCal);
|
writer.Write(c.SpikeMax);
|
||||||
writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil);
|
writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil);
|
||||||
writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap);
|
writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap);
|
||||||
writer.Write(c.TailSlope);
|
writer.Write(c.TailSlope);
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,12 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
public static string SeaLevelModel = "flat";
|
public static string SeaLevelModel = "flat";
|
||||||
public static float SeaLevelValue = 0.15f;
|
public static float SeaLevelValue = 0.15f;
|
||||||
|
|
||||||
// Height-redistribution curve (task 05, graduation M-7): "v1" applies the
|
// Height-redistribution curve (tasks 05/06, graduation M-7): "v2" applies the
|
||||||
// calibrated storm-ladder curve to above-sea terrain (see HeightCurve.cs);
|
// calibrated storm-ladder curve with the per-seed peak spike (HeightCurve.cs);
|
||||||
// "off" is the raw legacy profile. Biome classification is curve-invariant
|
// "off" is the raw legacy profile. "v1" was dropped with the v2 recalibration
|
||||||
// by construction either way. Default: v1.
|
// (task-05 blueprints are regenerable). Biome classification is curve-invariant
|
||||||
public static string TerrainCurve = "v1";
|
// by construction either way. Default: v2.
|
||||||
|
public static string TerrainCurve = "v2";
|
||||||
|
|
||||||
public static void LoadConfig()
|
public static void LoadConfig()
|
||||||
{
|
{
|
||||||
|
|
@ -105,8 +106,10 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
|
||||||
if (data.ContainsKey("TerrainCurve"))
|
if (data.ContainsKey("TerrainCurve"))
|
||||||
{
|
{
|
||||||
string curve = (string)data["TerrainCurve"];
|
string curve = (string)data["TerrainCurve"];
|
||||||
if (curve == "off" || curve == "v1")
|
if (curve == "off" || curve == "v2")
|
||||||
TerrainCurve = curve;
|
TerrainCurve = curve;
|
||||||
|
else if (curve == "v1")
|
||||||
|
GD.PrintErr($"[ConfigManager] TerrainCurve 'v1' was retired by the v2 recalibration (task 06). Keeping '{TerrainCurve}' — use \"v2\" or \"off\".");
|
||||||
else
|
else
|
||||||
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
|
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,11 @@ namespace IslaApocalypse.Core
|
||||||
public class TerrainCurveInfo
|
public class TerrainCurveInfo
|
||||||
{
|
{
|
||||||
public ushort Version;
|
public ushort Version;
|
||||||
public float T1, T2, T3, T4, HMaxCal; // input knots
|
// Input knots. SpikeMax is the spike domain's top: under curve v1 it was the
|
||||||
|
// pooled calibration max (identical every seed); from v2 it is the SEED'S own
|
||||||
|
// effective raw maximum — blueprints are no longer reproducible from curve
|
||||||
|
// constants alone, which is exactly why it is recorded here.
|
||||||
|
public float T1, T2, T3, T4, SpikeMax;
|
||||||
public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands
|
public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,7 +394,7 @@ namespace IslaApocalypse.Core
|
||||||
c.Version = reader.ReadUInt16();
|
c.Version = reader.ReadUInt16();
|
||||||
c.T1 = reader.ReadSingle(); c.T2 = reader.ReadSingle();
|
c.T1 = reader.ReadSingle(); c.T2 = reader.ReadSingle();
|
||||||
c.T3 = reader.ReadSingle(); c.T4 = reader.ReadSingle();
|
c.T3 = reader.ReadSingle(); c.T4 = reader.ReadSingle();
|
||||||
c.HMaxCal = reader.ReadSingle();
|
c.SpikeMax = reader.ReadSingle();
|
||||||
c.Sea = reader.ReadSingle(); c.OrangeCeil = reader.ReadSingle();
|
c.Sea = reader.ReadSingle(); c.OrangeCeil = reader.ReadSingle();
|
||||||
c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle();
|
c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle();
|
||||||
c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle();
|
c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle();
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,69 @@
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The height-redistribution curve (terrain-water task 05, graduation M-7) — a pure,
|
/// The height-redistribution curve, v2 (terrain-water task 06) — a pure, static,
|
||||||
/// static, monotonic piecewise map over raw blueprint heights (D-035: numbers in,
|
/// monotonic piecewise map over raw blueprint heights (D-035: numbers in, numbers
|
||||||
/// numbers out, no lifecycle).
|
/// out; the per-seed spike maximum is an explicit PARAMETER, not hidden state).
|
||||||
///
|
///
|
||||||
/// OUTPUT bands are fixed by design (the storm ladder; developer-approved
|
/// v2 changes (developer's task-05 hillshade-gate verdict; everything below the
|
||||||
/// 75 % orange coverage / plateau 50 m above sea / peaks 220 m above sea).
|
/// plateau step is behaviorally byte-identical to v1):
|
||||||
/// INPUT knots were calibrated ONCE from measured data — the pooled CDF of
|
/// - PER-SEED SPIKE NORMALIZATION: the spike's input domain runs from t4 to the
|
||||||
/// above-sea land heights across batch 04's ten flat-sea seeds (340,618,126
|
/// current seed's own raw pre-curve maximum (hMaxSeed), so every island's
|
||||||
/// samples, 2026-08-07): P75 / P90 / P93 / P96 / max. The same knots apply to
|
/// tallest pixel reaches the ceiling — v1 mapped against the pooled
|
||||||
/// every seed; per-seed band proportions vary a few points by design.
|
/// calibration max and mid-range seeds topped out at 110–175 m.
|
||||||
|
/// - STIFFER SPIKE: ease-in 0.1u + 0.9·u⁴ (was 0.2u + 0.8·u³) — a wall, not a ramp.
|
||||||
|
/// - PEAK CEILING 420 m above sea (was 220 m).
|
||||||
///
|
///
|
||||||
/// Shape, monotonic by construction (every segment's normalized slope is bounded
|
/// Lower knots/bands are v1's, calibrated 2026-08-07 from batch 04's ten flat-sea
|
||||||
/// below by a positive constant) and asserted numerically at startup:
|
/// heightmaps (pooled above-sea land CDF, 340,618,126 samples): P75/P90/P93/P96.
|
||||||
|
///
|
||||||
|
/// Shape (strictly monotonic; every segment's normalized slope bounded below by a
|
||||||
|
/// positive constant; asserted numerically per generation against the EFFECTIVE
|
||||||
|
/// per-seed curve once hMaxSeed is known):
|
||||||
/// h ≤ sea (0.15) identity — water and the below-sea world untouched
|
/// h ≤ sea (0.15) identity — water and the below-sea world untouched
|
||||||
/// sea → t1 smooth toe, ease-out blend (gentle rolling, never flat)
|
/// sea → t1 smooth toe, ease-out blend (gentle rolling, never flat)
|
||||||
/// t1 → t2 linear rise into the red band
|
/// t1 → t2 linear rise into the red band
|
||||||
/// t2 → t3 smooth shoulder up to the plateau shelf
|
/// t2 → t3 smooth shoulder up to the plateau shelf
|
||||||
/// t3 → t4 near-flat plateau step (small positive slope)
|
/// t3 → t4 near-flat plateau step (small positive slope)
|
||||||
/// t4 → hmaxCal accelerating spike to the peak cap
|
/// t4 → spikeMax accelerating u⁴ spike to the 420 m peak cap (per-seed domain)
|
||||||
/// h > hmaxCal linear tail (keeps strict monotonicity, no clamp)
|
/// h > spikeMax linear tail (strict monotonicity, no clamp; reachable only
|
||||||
|
/// in the degenerate near-flat guard case)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HeightCurve
|
public static class HeightCurve
|
||||||
{
|
{
|
||||||
public const ushort VERSION = 1;
|
public const ushort VERSION = 2;
|
||||||
|
|
||||||
// Input knots — calibrated from batch 04 B-flat pooled land CDF (see report).
|
// Input knots — v1 calibration, unchanged (see class header).
|
||||||
public const float T1 = 0.628736f; // P75 — orange coverage boundary
|
public const float T1 = 0.628736f; // P75 — orange coverage boundary
|
||||||
public const float T2 = 0.819152f; // P90
|
public const float T2 = 0.819152f; // P90
|
||||||
public const float T3 = 0.879340f; // P93
|
public const float T3 = 0.879340f; // P93
|
||||||
public const float T4 = 0.962922f; // P96
|
public const float T4 = 0.962922f; // P96
|
||||||
public const float HMAX_CAL = 1.452219f; // pooled max
|
|
||||||
|
|
||||||
// Output bands — the storm ladder.
|
// Output bands — the storm ladder. Lower anchors unchanged from v1.
|
||||||
public const float SEA = 0.15f;
|
public const float SEA = 0.15f;
|
||||||
public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling
|
public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling
|
||||||
public const float RED_CEIL = 0.27f; // biblical ceiling
|
public const float RED_CEIL = 0.27f; // biblical ceiling
|
||||||
public const float PLATEAU_LO = SEA + 50f / 251f; // ≈ 0.34924 (50 m above sea)
|
public const float PLATEAU_LO = SEA + 50f / 251f; // ≈ 0.34924 (50 m above sea)
|
||||||
public const float PLATEAU_HI = PLATEAU_LO + 0.02f; // ≈ 0.36924 (~5 m step relief)
|
public const float PLATEAU_HI = PLATEAU_LO + 0.02f; // ≈ 0.36924 (~5 m step relief)
|
||||||
public const float PEAK_CAP = SEA + 220f / 251f; // ≈ 1.02649 (220 m above sea)
|
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 (420 m above sea; v1: 220 m)
|
||||||
public const float TAIL_SLOPE = 0.25f; // above HMAX_CAL
|
public const float TAIL_SLOPE = 0.25f; // above spikeMax (degenerate guard only)
|
||||||
|
|
||||||
public static float Apply(float h)
|
// Degenerate/near-flat guard: the spike domain is [T4, max(hMaxSeed, T4 + SPIKE_MIN_SPAN)],
|
||||||
|
// so a pathological seed whose raw max sits at or below t4 still yields a positive,
|
||||||
|
// monotonic domain (its cap is then simply never reached; heights above spikeMax — none in
|
||||||
|
// practice — would ride the tail).
|
||||||
|
public const float SPIKE_MIN_SPAN = 0.01f;
|
||||||
|
|
||||||
|
/// <summary>The effective spike-domain top for a seed's raw maximum, guard applied.</summary>
|
||||||
|
public static float EffectiveSpikeMax(float hMaxSeed)
|
||||||
|
{
|
||||||
|
return Mathf.Max(hMaxSeed, T4 + SPIKE_MIN_SPAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="h">Raw pre-curve height.</param>
|
||||||
|
/// <param name="hMaxSeed">The seed's raw pre-curve maximum (post noise/falloff/Trench/spine,
|
||||||
|
/// pre-carve) — the same field the curve consumes. Makes the map seed-dependent (v2).</param>
|
||||||
|
public static float Apply(float h, float hMaxSeed)
|
||||||
{
|
{
|
||||||
if (h <= SEA) return h;
|
if (h <= SEA) return h;
|
||||||
|
|
||||||
|
|
@ -69,44 +90,46 @@ public static class HeightCurve
|
||||||
u = (h - T3) / (T4 - T3);
|
u = (h - T3) / (T4 - T3);
|
||||||
return PLATEAU_LO + u * (PLATEAU_HI - PLATEAU_LO); // near-flat, small positive slope
|
return PLATEAU_LO + u * (PLATEAU_HI - PLATEAU_LO); // near-flat, small positive slope
|
||||||
}
|
}
|
||||||
if (h < HMAX_CAL)
|
float spikeMax = EffectiveSpikeMax(hMaxSeed);
|
||||||
|
if (h < spikeMax)
|
||||||
{
|
{
|
||||||
u = (h - T4) / (HMAX_CAL - T4);
|
u = (h - T4) / (spikeMax - T4);
|
||||||
s = 0.2f * u + 0.8f * (u * u * u); // ease-in spike, slope ≥ 0.2
|
s = 0.1f * u + 0.9f * (u * u * u * u); // ease-in u⁴ wall, slope ≥ 0.1
|
||||||
return PLATEAU_HI + s * (PEAK_CAP - PLATEAU_HI);
|
return PLATEAU_HI + s * (PEAK_CAP - PLATEAU_HI);
|
||||||
}
|
}
|
||||||
return PEAK_CAP + (h - HMAX_CAL) * TAIL_SLOPE;
|
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Numeric strict-monotonicity check across the whole plausible domain.
|
/// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once
|
||||||
/// Cheap (runs once at generator start); a violation is a build bug, not a
|
/// per generation after hMaxSeed is known, before the curve pass. A violation is
|
||||||
/// data condition — fail loudly and refuse to generate.
|
/// a build bug, not a data condition — fail loudly and refuse to generate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void AssertMonotonic()
|
public static void AssertMonotonic(float hMaxSeed)
|
||||||
{
|
{
|
||||||
float prevH = -7f;
|
float prevH = -7f;
|
||||||
float prev = Apply(prevH);
|
float prev = Apply(prevH, hMaxSeed);
|
||||||
|
|
||||||
// Successive double samples can round to the SAME float32 (the two loops
|
// Successive double samples can round to the SAME float32 — only strictly
|
||||||
// meeting at 0.10 did exactly that and tripped the strict check against
|
// increasing float samples are compared (task-05 incident fix, kept).
|
||||||
// itself) — so only strictly increasing float samples are compared.
|
|
||||||
void Check(double hd)
|
void Check(double hd)
|
||||||
{
|
{
|
||||||
float h = (float)hd;
|
float h = (float)hd;
|
||||||
if (h <= prevH) return;
|
if (h <= prevH) return;
|
||||||
float v = Apply(h);
|
float v = Apply(h, hMaxSeed);
|
||||||
if (v <= prev)
|
if (v <= prev)
|
||||||
throw new System.InvalidOperationException(
|
throw new System.InvalidOperationException(
|
||||||
$"[HeightCurve] MONOTONICITY VIOLATION at h={h}: {v} <= {prev}. Refusing to generate.");
|
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}): {v} <= {prev}. Refusing to generate.");
|
||||||
prev = v;
|
prev = v;
|
||||||
prevH = h;
|
prevH = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coarse below the identity region, fine through every knot, out past the tail.
|
// Coarse below the identity region, fine through every knot, out past the
|
||||||
|
// per-seed spike top and the tail.
|
||||||
|
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
|
||||||
for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h);
|
for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h);
|
||||||
for (double h = 0.10; h <= 2.0; h += 0.0001) Check(h);
|
for (double h = 0.10; h <= top; h += 0.0001) Check(h);
|
||||||
for (double h = 2.05; h <= 8.0; h += 0.05) Check(h);
|
for (double h = top + 0.05; h <= top + 6.0; h += 0.05) Check(h);
|
||||||
GD.Print("[HeightCurve] Monotonicity assertion passed (v" + VERSION + ").");
|
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
Tools/Scripts/HeightCurve.cs.uid
Normal file
1
Tools/Scripts/HeightCurve.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ljv7x1fwm3os
|
||||||
|
|
@ -37,6 +37,11 @@ public partial class MapGenerator : TextureRect
|
||||||
private float[,] _heightMapClassify;
|
private float[,] _heightMapClassify;
|
||||||
private bool _curveOn;
|
private bool _curveOn;
|
||||||
|
|
||||||
|
// The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine,
|
||||||
|
// pre-carve) — the v2 curve's per-seed spike normalizer. Computed in
|
||||||
|
// GenerateTopography pass 1; recorded in TCRV (effective, guard applied).
|
||||||
|
private float _hMaxSeed = float.MinValue;
|
||||||
|
|
||||||
private float[,] _tempMap;
|
private float[,] _tempMap;
|
||||||
private Biome[,] _biomeMap;
|
private Biome[,] _biomeMap;
|
||||||
private bool[,] _isTrueOcean;
|
private bool[,] _isTrueOcean;
|
||||||
|
|
@ -70,8 +75,9 @@ public partial class MapGenerator : TextureRect
|
||||||
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
|
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
|
||||||
|
|
||||||
_heightMap = new float[MapSize, MapSize];
|
_heightMap = new float[MapSize, MapSize];
|
||||||
_curveOn = ConfigManager.TerrainCurve == "v1";
|
_curveOn = ConfigManager.TerrainCurve == "v2";
|
||||||
if (_curveOn) HeightCurve.AssertMonotonic();
|
// (The monotonicity assertion now runs inside GenerateTopography, against the
|
||||||
|
// effective per-seed curve, once hMaxSeed is known.)
|
||||||
_heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
|
_heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
|
||||||
_tempMap = new float[MapSize, MapSize];
|
_tempMap = new float[MapSize, MapSize];
|
||||||
_biomeMap = new Biome[MapSize, MapSize];
|
_biomeMap = new Biome[MapSize, MapSize];
|
||||||
|
|
@ -254,7 +260,7 @@ public partial class MapGenerator : TextureRect
|
||||||
{
|
{
|
||||||
Version = HeightCurve.VERSION,
|
Version = HeightCurve.VERSION,
|
||||||
T1 = HeightCurve.T1, T2 = HeightCurve.T2, T3 = HeightCurve.T3, T4 = HeightCurve.T4,
|
T1 = HeightCurve.T1, T2 = HeightCurve.T2, T3 = HeightCurve.T3, T4 = HeightCurve.T4,
|
||||||
HMaxCal = HeightCurve.HMAX_CAL,
|
SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed (v2)
|
||||||
Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
|
Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
|
||||||
PlateauLo = HeightCurve.PLATEAU_LO, PlateauHi = HeightCurve.PLATEAU_HI,
|
PlateauLo = HeightCurve.PLATEAU_LO, PlateauHi = HeightCurve.PLATEAU_HI,
|
||||||
PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE
|
PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE
|
||||||
|
|
@ -401,28 +407,45 @@ public partial class MapGenerator : TextureRect
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. COMBINE HEIGHT ---
|
// --- 4. COMBINE HEIGHT ---
|
||||||
|
// PASS 1 stores the RAW pre-curve height and tracks the seed maximum;
|
||||||
|
// the curve (which is per-seed in v2 — its spike normalizes against
|
||||||
|
// hMaxSeed) and the crater carve are applied in PASS 2 below.
|
||||||
float rawBase = (_noise.GetNoise2D(x, y) + 1.0f) / 2.0f;
|
float rawBase = (_noise.GetNoise2D(x, y) + 1.0f) / 2.0f;
|
||||||
float finalH = rawBase + mountainSpine - (finalFalloff * FalloffStrength);
|
float finalH = rawBase + mountainSpine - (finalFalloff * FalloffStrength);
|
||||||
|
if (finalH > _hMaxSeed) _hMaxSeed = finalH;
|
||||||
|
_heightMap[x, y] = finalH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 4b. THE REDISTRIBUTION CURVE (task 05) ---
|
// The v2 curve is SEED-DEPENDENT: its spike maps [t4, hMaxSeed] onto the peak
|
||||||
// Applied AFTER noise + falloff + Trench, BEFORE the crater carve, so
|
// band, so the monotonicity assertion must run against the EFFECTIVE per-seed
|
||||||
// the carve cuts into curved terrain and the rim/bowl shape is
|
// curve — after hMaxSeed is known, before any pixel is curved.
|
||||||
// untouched by the curve. Identity at and below sea + this ordering
|
if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed);
|
||||||
// preserve the Trench/ocean-border guarantee and the crater by
|
|
||||||
// construction. classifyH stays uncurved — see _heightMapClassify.
|
// --- PASS 2: curve (task 05/06) + crater carve ---
|
||||||
float classifyH = finalH;
|
// Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so
|
||||||
float curvedH = _curveOn ? HeightCurve.Apply(finalH) : finalH;
|
// the carve cuts into curved terrain and the rim/bowl shape is untouched by
|
||||||
|
// the curve. Identity at and below sea + this ordering preserve the
|
||||||
|
// Trench/ocean-border guarantee and the crater by construction. classifyH
|
||||||
|
// stays uncurved — see _heightMapClassify; hMaxSeed never touches it.
|
||||||
|
float physicalCraterRadius = _impactRadius * 0.80f;
|
||||||
|
for (int x = 0; x < MapSize; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < MapSize; y++)
|
||||||
|
{
|
||||||
|
float raw = _heightMap[x, y];
|
||||||
|
float classifyH = raw;
|
||||||
|
float curvedH = _curveOn ? HeightCurve.Apply(raw, _hMaxSeed) : raw;
|
||||||
|
|
||||||
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
|
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
|
||||||
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
|
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
|
||||||
|
|
||||||
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
|
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
|
||||||
float physicalCraterRadius = _impactRadius * 0.80f;
|
|
||||||
if (distToCrater < physicalCraterRadius)
|
if (distToCrater < physicalCraterRadius)
|
||||||
{
|
{
|
||||||
float craterDepth = 1.0f - (distToCrater / physicalCraterRadius);
|
float craterDepth = 1.0f - (distToCrater / physicalCraterRadius);
|
||||||
// Dialed back to -0.15f as per your excellent instinct!
|
// Dialed back to -0.15f as per your excellent instinct!
|
||||||
float carveTarget = GetSeaLevel(temperature) - 0.15f;
|
float carveTarget = GetSeaLevel(_tempMap[x, y]) - 0.15f;
|
||||||
classifyH = Mathf.Lerp(classifyH, carveTarget, craterDepth * 0.9f);
|
classifyH = Mathf.Lerp(classifyH, carveTarget, craterDepth * 0.9f);
|
||||||
curvedH = Mathf.Lerp(curvedH, carveTarget, craterDepth * 0.9f);
|
curvedH = Mathf.Lerp(curvedH, carveTarget, craterDepth * 0.9f);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,8 @@ public partial class RoundTripHarness : Node
|
||||||
}
|
}
|
||||||
var ca = a.TerrainCurve; var cb = b.TerrainCurve;
|
var ca = a.TerrainCurve; var cb = b.TerrainCurve;
|
||||||
bool same = ca.Version == cb.Version;
|
bool same = ca.Version == cb.Version;
|
||||||
float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.HMaxCal, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope };
|
float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope };
|
||||||
float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.HMaxCal, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope };
|
float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope };
|
||||||
for (int i = 0; i < fa.Length; i++)
|
for (int i = 0; i < fa.Length; i++)
|
||||||
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
|
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
|
||||||
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }
|
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue