using System;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
///
/// Where a run came FROM. Not a material property — a fact about provenance.
///
/// ⭐ THIS, AND ONLY THIS, DECIDES MESH STYLE: natural runs mesh smooth (SN/DC), player-placed
/// runs mesh as cubes, and the seam-blend handles the join. The same material can be both —
/// which is exactly why mesh style is NOT a field on the material row.
/// → D-054 ("material does not override mesh style"), `Design - Rendering - Mesher.md`, D-052.
///
/// No mesher exists this phase. The flag exists so the mesher has something true to read.
///
public enum RunOrigin : byte
{
/// Produced by world generation — stratigraphy, or a 3D feature pass.
Natural = 0,
/// Placed by a player.
PlayerPlaced = 1,
}
///
/// One run-length band in a column: a material, a thickness, and where it came from.
/// → `Design - Data - Column Model.md`, D-053.
///
/// ⚠ A RUN STORES A MATERIAL IDENTITY AND NOTHING ELSE ABOUT THE MATERIAL.
/// Hardness, yield, support strength — every property — comes from
/// by lookup on . Nothing is stored
/// per-run. That is properties-from-type, and it is what makes materials, support and
/// mod-ability clean. → `Design - Data - Material Schema.md`.
///
/// is not an exception to that rule: it is not a property OF the material,
/// it is a property of THIS RUN.
///
public readonly struct MaterialRun
{
/// The material's registry key. → .
public readonly MaterialKey Material;
/// Thickness in voxels, bottom to top. Strictly positive.
public readonly int Thickness;
/// Provenance. Decides mesh style; see .
public readonly RunOrigin Origin;
public MaterialRun(MaterialKey material, int thickness, RunOrigin origin = RunOrigin.Natural)
{
if (material.IsNone)
throw new ArgumentException("A run must name a material. Air is a material (core.air), not an absence.", nameof(material));
if (thickness <= 0)
throw new ArgumentOutOfRangeException(nameof(thickness), thickness, "A run's thickness must be strictly positive. A zero-thickness run is not a thing; omit it.");
Material = material;
Thickness = thickness;
Origin = origin;
}
public override string ToString() => $"{Material} x{Thickness} ({Origin})";
}
///
/// ★★ THE LOAD-BEARING CONTRACT — one horizontal cell of the world, as a vertical stack of
/// run-length bands, bottom to top. → `Design - Data - Column Model.md`, D-053.
///
///
/// bedrock 0–50
/// limestone 50–60
/// air 60–70 ← a cave: an air run mid-column
/// limestone 70–84
/// iron 84–86 ← an ore vein: a material run inside limestone
/// limestone 86–90
/// clay 90–98
/// topsoil 98–100
/// air 100+ ← everything above the surface is simply the top air run
///
///
/// ═══ TWO GENERALIZATIONS THIS SHAPE BUYS ═══
///
/// 1. ANY MATERIAL AT ANY DEPTH — not "one material per depth zone". A run can be any material
/// at any position, which is what lets 3D feature passes write into columns: a cave is an
/// AIR run mid-stack, an ore vein is an ore run inside rock. No cave or ore code exists this
/// phase; the SHAPE that makes them trivial does.
///
/// 2. SURFACE MATERIAL IS CLIMATE-DRIVEN, not a fixed top layer. The topmost solid run's
/// material is f(elevation, temperature, precipitation) from the 2D maps, so snow caps and
/// bare peaks fall out for free. ⭐ Snow is a surface MATERIAL, not a biome — biomes are a
/// separate classification layer. No stratigraphy exists this phase.
///
/// ═══ ⚠⚠ THE BEHAVIOURAL RULE — THE ONE THIS TYPE EXISTS TO ENFORCE ═══
///
/// NEVER BRANCH ON "AIR VS SOLID" FROM HEIGHT AS A SPECIAL CASE.
///
/// The prototype's mistake began exactly there — panicking about "is this air or solid, based
/// on the heightmap". In the band model there is NO height-triggered branching: everything
/// above the surface is simply the top AIR run. Air is not a special height rule; it is just
/// the topmost band, a registry row like any other ().
///
/// This type deliberately exposes NO surface-height, ground-level, or is-solid-at accessor.
/// Every one of those is the old mistake wearing a helper's clothes. If you find yourself
/// wanting one, you are about to reintroduce the bug this model was built to make impossible.
///
/// ═══ ⚠ WHAT IS NOT HERE, AND MUST NOT BE ═══
///
/// • WATER IS NOT A BAND. Water is a separate OVERLAY over the columns — the levels-not-cells
/// model (sea, lakes, rivers). It is never stored as a run. Nothing about water belongs in
/// this type. → `Design - Water - Runtime Water.md`, `Design - Water - Sea Level Model.md`.
///
/// • NO PER-RUN PROPERTIES. Runs store identity; the registry answers everything else.
///
/// • NO ALGORITHMS. Phase 0 is the shape only. Run splitting on dig, merging, stability
/// storage, chunking for streaming, and the mesher are all later phases. The minimal API
/// below — construct, append, iterate — is the whole surface on purpose.
///
/// ⚠ A LIVE INVARIANT constrains how the shaping/hydrology/water passes use this model (D-046):
/// classify-height and render-height must stay consistent. Its home is
/// `Design - Pipeline - Generation Order.md` § LIVE INVARIANT — read it there before writing
/// anything that reads or writes columns from both height views.
///
public sealed class Column
{
private readonly List _runs;
public Column(int expectedRuns = 8)
{
_runs = new List(expectedRuns);
}
/// The runs, bottom to top. Read-only: mutation goes through .
public IReadOnlyList Runs => _runs;
public int RunCount => _runs.Count;
///
/// Append a run ON TOP of the stack. Runs are built bottom-up, in order.
///
/// No merging of like-material neighbours, no splitting, no insertion mid-stack. Those are
/// later-phase operations with their own correctness questions; this phase builds the shape.
///
public void Append(MaterialRun run)
{
_runs.Add(run);
}
/// Append a run by its parts. Convenience over .
public void Append(MaterialKey material, int thickness, RunOrigin origin = RunOrigin.Natural)
=> Append(new MaterialRun(material, thickness, origin));
///
/// Total stacked thickness of every run, including air.
///
/// ⚠ THIS IS NOT A SURFACE HEIGHT AND MUST NOT BE USED AS ONE. It is the top of the column
/// including its air run. There is deliberately no "height of the solid part" here — see
/// the behavioural rule in this type's summary.
///
public int TotalThickness
{
get
{
int t = 0;
for (int i = 0; i < _runs.Count; i++) t += _runs[i].Thickness;
return t;
}
}
public override string ToString() => $"Column({_runs.Count} runs, {TotalThickness} thick)";
}
}