using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// Everything pass 2 produces: THE TWO HEIGHT FIELDS. → .
///
/// ═══ ⭐⭐ THE TWO-FIELD SPLIT (D-046) — THE DISCIPLINE THIS CLASS EXISTS TO HOLD ═══
///
/// RAW. Uncurved, un-detailed. The ORACLE.
/// RENDER. Curved, detailed, and later eroded and carved.
///
/// Everything that CLASSIFIES the world — biomes, water bodies, the ocean flood fill — reads the
/// classify field. Everything that DRAWS or MESHES it reads the render field. The reference's
/// hardest-won lesson is that this split is what made five rounds of taste-iteration safe: the
/// biome and water maps stayed md5-identical across every shaping change, so correctness was
/// never being judged by eye. → `Design - Tooling - Iteration and Batching.md`,
/// "build the oracle before the taste-iteration".
///
/// ⚠ NOTHING CONSUMES THE CLASSIFY FIELD YET. No water, no biomes exist in this phase. The split
/// is established HERE, at the curve, because the curve is where the second field is BORN — and
/// retrofitting a classify path after three passes already ran on one array is how the two
/// silently diverge. The field is produced and asserted now so that when water lands it has
/// something correct to read.
///
/// ═══ ⚠ WHEN THE TWO FIELDS ARE THE SAME ARRAY ═══
///
/// With the curve OFF there is nothing to separate, so both properties reference ONE array —
/// exactly as the reference did (_heightMapClassify = (_curveOn || _erosionOn) ? new float[…]
/// : _heightMap). says so out loud, because a later pass that
/// writes through one reference while reading the other MUST know: the reference's crater carve
/// reads both into locals before writing either for precisely this reason, and that is the trap
/// this flag is here to keep visible until the carve lands.
///
public sealed class Pass2Result
{
/// Map side in columns.
public readonly int MapSize;
/// The resolved seed. Same seed, same two fields.
public readonly int Seed;
///
/// ⭐ THE RENDER FIELD, [x, y] — curved and detailed. What gets drawn, dumped and
/// (later) eroded, carved and meshed.
///
public readonly float[,] Height;
///
/// ⭐ THE CLASSIFY FIELD, [x, y] — bit-for-bit the raw pre-curve pass-1 height.
///
/// ⚠ Nothing may write to this after pass 2 except the crater carve, which is the one pass
/// that legitimately moves both fields. Erosion, rivers and detail are render-only.
///
public readonly float[,] HeightClassify;
/// Was the curve applied? The primary A/B gate.
public readonly bool CurveOn;
///
/// Which curve shaped the render field: "off", "staircase", "continuous" or "lifted_WRONG".
/// A label, not logic — the oracle and the INDEX read it so a result can never be mistaken
/// for the wrong mode's.
///
public readonly string CurveModeLabel;
///
/// The batch variant this result belongs to, carried straight from
/// .
///
/// ⚠ CARRIED, NOT INFERRED. The first cut of the chat2/02 tool reconstructed this from the
/// knob values ("ceiling > 31 ⇒ hold_higher"), which works only for exactly today's
/// variant set: add a second variant sharing a knob value and two different runs silently
/// write to one folder. The label is an identity, so it travels with the result.
///
public readonly string VariantLabel;
///
/// The per-seed continuous spline, when is "continuous" — the
/// oracle samples its slopes (check e) and the INDEX prints its control points. Null for
/// every other mode.
///
public readonly ContinuousCurve Continuous;
// ═══ ⭐ THE OFFSHORE TAG, CARRIED (chat2/05) ═══
//
// Pass 1 sets it; pass 2 carries it UNCHANGED beside the two height fields, because this is
// where the shaped-terrain result flows and where a downstream consumer would pick it up.
// The curve is identity at sea and monotone above, so a cell that was offshore-island LAND
// in pass 1 is still land in the render field — the tag stays valid for both fields without
// being recomputed (oracle: "classify/render coastline consistent").
//
// ⚠ NO LOGIC READS IT THIS PHASE. It is a data layer. A biome/fertility/placement pass reads
// it from here, checks for null (offshore off), and never re-derives island-land from
// geometry.
/// → , the same array. Null when offshore is off.
public readonly bool[,] IsOffshoreIsland;
/// → , the same array. Null when offshore is off.
public readonly byte[,] IslandHemisphere;
/// Was shelf detail applied? Requires — it warps the curve's knots.
public readonly bool DetailOn;
/// The knot set used. Null when the curve is off.
public readonly CurveKnots Knots;
/// The output anchors used. Null when the curve is off.
public readonly CurveAnchors Anchors;
/// The per-seed spike input, carried through from pass 1.
public readonly float HMaxSeed;
/// The edge-warp amplitude actually APPLIED, raw units (post-clamp). Zero when detail is off.
public readonly float EdgeAmpRaw;
/// The knot set's safe warp bound, raw units — what was clamped to.
public readonly float MaxEdgeShiftRaw;
/// Render-field extremes after shaping. For the ramp and the report.
public readonly float HMin, HMax;
/// Wall-clock milliseconds pass 2 took.
public readonly ulong ElapsedMs;
///
/// Lines worth printing: the monotonicity confirmation, any loud clamp. Collected rather than
/// printed inside the pass so the shaping code stays a pure function of its inputs and the
/// tool owns the console.
///
public readonly List Notes;
public Pass2Result(int mapSize, int seed, float[,] height, float[,] heightClassify,
bool curveOn, bool detailOn, string curveModeLabel, string variantLabel,
ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
List notes, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null)
{
IsOffshoreIsland = isOffshoreIsland;
IslandHemisphere = islandHemisphere;
MapSize = mapSize;
Seed = seed;
Height = height;
HeightClassify = heightClassify;
CurveOn = curveOn;
DetailOn = detailOn;
CurveModeLabel = curveModeLabel;
VariantLabel = variantLabel;
Continuous = continuous;
Knots = knots;
Anchors = anchors;
HMaxSeed = hMaxSeed;
EdgeAmpRaw = edgeAmpRaw;
MaxEdgeShiftRaw = maxEdgeShiftRaw;
HMin = hMin;
HMax = hMax;
ElapsedMs = elapsedMs;
Notes = notes;
}
///
/// ⚠ True when the two fields ARE the same array (curve off). Any pass that writes one while
/// reading the other must read both into locals first. See the type header.
///
public bool FieldsAreAliased => ReferenceEquals(Height, HeightClassify);
/// Fraction of the RENDER field at or above the sea threshold.
public float LandFraction(float seaLevel)
{
long land = 0;
for (int x = 0; x < MapSize; x++)
for (int y = 0; y < MapSize; y++)
if (Height[x, y] >= seaLevel) land++;
return land / (float)((long)MapSize * MapSize);
}
}
}