using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐ PASS 2b — HYDRAULIC EROSION, THE CALLER (chat2/11). Runs on
/// the RENDER field of a shaped result — after the curve + detail (pass 2a), before the crater
/// carve (which does not exist yet) — exactly the reference's slot (MapGenerator.cs:737-805).
///
/// ═══ THE THREE THINGS THE CALLER OWNS (ported from the reference caller) ═══
///
/// 1. THE SPLIT. Only the render field is eroded; the classify field is finalized before the pass
/// and never sees it — biomes / water / region labeling classify pre-erosion (the oracle).
/// If the two fields are aliased (curve off), the render field is COPIED first, as the
/// reference allocated a separate classify array when erosion was on.
/// 2. THE FLOOD GUARD. Render-map water pixels are counted BEFORE and AFTER; any change throws
/// "EROSION FLOOD-GUARD VIOLATION". With the pass's sea clamp (below-sea cells read-only in
/// both directions; carve floor at sea + margin) this is the active proof that no coastline
/// moved — mainland and every island alike.
/// 3. THE CRATER EXCLUSION, INERT. The weight is passed whole (core / feather radii, mode) but
/// with no crater in v2 the radius is 0 ⇒ weight 1 everywhere. It activates when the crater
/// carve lands; the reference's "CraterErosionCore < carve factor" warning is DORMANT until
/// then (it concerns the erosion↔carve interaction, which does not exist yet).
///
/// The governors are clamped as the reference's ConfigManager clamped them (count [0, 50 M],
/// lifetime [1, 4096], carve cap [0, 60], deposit cap [0, 60], sea margin [0, 5], inertia
/// [0, 0.99], evaporation [0, 0.5]) and a clamp is reported, not silent.
///
public static class ErosionPass
{
public sealed class Result
{
public Pass2Result Shaped; // the result with the eroded RENDER field (classify untouched)
public HydraulicErosion.Stats Stats;
public HydraulicErosion.Params Params;
public long WetBefore, WetAfter;
public ulong Ms;
public List Notes = new();
}
/// Count render-map water pixels (height below the flat sea) — the flood guard's instrument.
public static long CountWaterPixels(float[,] height, int mapSize, float sea)
{
long wet = 0;
for (int x = 0; x < mapSize; x++)
for (int y = 0; y < mapSize; y++)
if (height[x, y] < sea) wet++;
return wet;
}
///
/// Erode 's render field (a copy if aliased to classify) and return the
/// result. Throws on a governor-cap violation (from the pass) or a flood-guard violation.
///
public static Result Apply(Pass2Result p2, TerrainGenConfig cfg)
{
ulong t0 = Time.GetTicksMsec();
var r = new Result();
int n = p2.MapSize;
float sea = cfg.SeaLevel;
// 1. THE SPLIT — never erode an array the classify field shares.
float[,] render = p2.Height;
if (p2.FieldsAreAliased)
{
render = (float[,])p2.Height.Clone();
r.Notes.Add("[Erosion] render and classify were aliased (curve off) — the render field was copied before eroding, as the reference allocated a separate classify array when erosion was on.");
}
// The governors, clamped as the reference's ConfigManager clamped them — reported, not silent.
int count = Math.Clamp(cfg.ErosionDropletCount, 0, 50_000_000);
int life = Math.Clamp(cfg.ErosionDropletLifetime, 1, 4096);
float carve = Math.Clamp(cfg.ErosionCarveCapM, 0f, 60f);
float deposit = Math.Clamp(cfg.ErosionDepositCapM, 0f, 60f);
float margin = Math.Clamp(cfg.ErosionSeaMarginM, 0f, 5f);
float inertia = Math.Clamp(cfg.ErosionInertia, 0f, 0.99f);
float evap = Math.Clamp(cfg.ErosionEvaporation, 0f, 0.5f);
if (count != cfg.ErosionDropletCount || life != cfg.ErosionDropletLifetime || carve != cfg.ErosionCarveCapM || deposit != cfg.ErosionDepositCapM
|| margin != cfg.ErosionSeaMarginM || inertia != cfg.ErosionInertia || evap != cfg.ErosionEvaporation)
r.Notes.Add($"[Erosion] ⚠ governor out of bounds — clamped: count {cfg.ErosionDropletCount}→{count}, lifetime {cfg.ErosionDropletLifetime}→{life}, carve {cfg.ErosionCarveCapM}→{carve} m, deposit {cfg.ErosionDepositCapM}→{deposit} m, margin {cfg.ErosionSeaMarginM}→{margin} m, inertia {cfg.ErosionInertia}→{inertia}, evaporation {cfg.ErosionEvaporation}→{evap}.");
// 3. THE CRATER EXCLUSION — inert: no crater ⇒ radius 0 ⇒ weight 1 everywhere.
float craterRadius = cfg.CraterRadius; // 0 in v2 until the crater task
float coreR = craterRadius * cfg.CraterErosionCore;
float featherR = craterRadius * cfg.CraterErosionFeather;
const float CRATER_CARVE_FACTOR = 0.80f; // the reference's carve radius factor
if (craterRadius > 0f && cfg.CraterErosionCore < CRATER_CARVE_FACTOR)
r.Notes.Add($"[Erosion] ⚠ CraterErosionCore {cfg.CraterErosionCore:F2} is inside the carve radius ({CRATER_CARVE_FACTOR:F2} × CraterRadius) — erosion will modify carve-authored terrain and the carve will amplify those deltas across the waterline (the reference's warning; live only once a crater exists).");
// 2. THE FLOOD GUARD — before.
r.WetBefore = CountWaterPixels(render, n, sea);
r.Params = new HydraulicErosion.Params
{
DropletCount = count, Lifetime = life, CarveCapM = carve, DepositCapM = deposit, SeaMarginM = margin,
BrushRadius = Math.Max(0, cfg.ErosionBrushRadius), Inertia = inertia, CapacityFactor = cfg.ErosionCapacity,
MinSlopeM = cfg.ErosionMinSlopeM, ErodeRate = cfg.ErosionErodeRate, DepositRate = cfg.ErosionDepositRate,
Evaporation = evap, Gravity = cfg.ErosionGravity,
CraterMode = cfg.CraterErosionFeatherMode ? HydraulicErosion.CRATER_MODE_FEATHER : HydraulicErosion.CRATER_MODE_FULL,
Seed = cfg.Seed + HydraulicErosion.SEED_OFFSET,
};
r.Stats = HydraulicErosion.Apply(render, n, null, sea, cfg.CraterCenterX, cfg.CraterCenterY, coreR, featherR, r.Params);
// 2. THE FLOOD GUARD — after. Any change refuses the generation.
r.WetAfter = CountWaterPixels(render, n, sea);
if (r.WetAfter != r.WetBefore)
throw new InvalidOperationException(
$"[ErosionPass] EROSION FLOOD-GUARD VIOLATION: render-map water pixels {r.WetBefore} -> {r.WetAfter}. Refusing to generate.");
r.Ms = Time.GetTicksMsec() - t0;
var st = r.Stats;
r.Notes.Add($"[Erosion] crater exclusion: {(craterRadius > 0f ? $"core {coreR:F0} px → feather {featherR:F0} px" : "INERT (no crater; weight 1 everywhere)")}.");
r.Notes.Add($"[Erosion] v1: {st.Spawned} droplets ({st.SkippedNoLand} skipped), {st.Steps:N0} steps, {r.Ms / 1000.0:F1}s wall. " +
$"Eroded {st.ErodedVolumeM3:F0} m³ over {st.ModifiedCells:N0} touched cells (max cell carve {st.MaxCellErosionM:F2} m vs cap {carve:F2} m), " +
$"deposited {st.DepositedVolumeM3:F0} m³ (max cell deposit {st.MaxCellDepositM:F2} m vs cap {deposit:F2} m). " +
$"Deaths: {st.DiedSea} sea / {st.DiedEdge} edge / {st.DiedDry} dry / {st.DiedLifetime} lifetime. " +
$"Water pixels {r.WetBefore:N0} -> {r.WetAfter:N0} (flood guard holds).");
r.Shaped = p2.WithHeight(render, r.Notes, r.Ms);
return r;
}
}
}