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; /// 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, CurveKnots knots, CurveAnchors anchors, float hMaxSeed, float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs, List notes) { MapSize = mapSize; Seed = seed; Height = height; HeightClassify = heightClassify; CurveOn = curveOn; DetailOn = detailOn; 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); } } }