islaApocalypse-v2/Core/Scripts/Column.cs
beezm 9107b0822b Phase 0: clean project skeleton for the v2 rewrite
Stands up the CODE_REPO the rewrite is written into (D-049, D-058). Godot 4.7.2 /
.NET 8 / Godot.NET.Sdk 4.7.2. Nothing is generated, meshed, or ported — this is the
shell and the two data contracts.

Four layers, each with its boundary stated in a directory README:
  Core/    math + data only, and engine-free — depends on nothing above it
  Server/  authoritative logic                (empty this phase)
  Client/  rendering                          (empty this phase)
  Tools/   the offline generator — may NOT reference Client   (Phase 1 fills it)

Contracts (compiling stubs, no algorithms):
  - Column model (D-053): a 2D grid of columns, each a stack of (material, thickness)
    runs, any material at any depth. Air is the TOP RUN — there is no air-vs-solid
    height branch, and no surface-height accessor exists to reintroduce one. Water is
    not a band; it stays an overlay.
  - Material schemas (D-054): terrain and building as two append-only registries,
    bridged by a recipe seam that is deliberately EMPTY. Identity is a registry key,
    never a serialized ordinal. Mesh style is per-ORIGIN and is not a material field.

Rails, so Phase 1 inherits them rather than rediscovering them:
  - GenerationScale: MapSize is a parameter, everything derives from it, nothing uses
    a raw pixel number. Tightening over the reference — decorrelation offsets are
    declared in map widths, not pixels (chat1/00 §4.2).
  - ToolingPaths: config/blueprint/output paths all env-overridable, resolved in one
    place, so a batch cannot touch the developer's live files.
  - FileSafety: the permanent no-deletion rules as throws rather than sentences.

user:// isolation: project name and use_custom_user_dir both pin the runtime dir to
~/.local/share/islaApocalypse-v2/, away from the old prototype's preserved seeds and
batches. Tools/Scenes/UserDirProbe.tscn confirms it rather than assuming it.
2026-08-19 20:21:12 -04:00

170 lines
7.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
/// <summary>
/// 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.
/// </summary>
public enum RunOrigin : byte
{
/// <summary>Produced by world generation — stratigraphy, or a 3D feature pass.</summary>
Natural = 0,
/// <summary>Placed by a player.</summary>
PlayerPlaced = 1,
}
/// <summary>
/// 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
/// <see cref="MaterialRegistry"/> by lookup on <see cref="Material"/>. 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`.
///
/// <see cref="Origin"/> is not an exception to that rule: it is not a property OF the material,
/// it is a property of THIS RUN.
/// </summary>
public readonly struct MaterialRun
{
/// <summary>The material's registry key. → <see cref="MaterialKey"/>.</summary>
public readonly MaterialKey Material;
/// <summary>Thickness in voxels, bottom to top. Strictly positive.</summary>
public readonly int Thickness;
/// <summary>Provenance. Decides mesh style; see <see cref="RunOrigin"/>.</summary>
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})";
}
/// <summary>
/// ★★ 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.
///
/// <code>
/// bedrock 050
/// limestone 5060
/// air 6070 ← a cave: an air run mid-column
/// limestone 7084
/// iron 8486 ← an ore vein: a material run inside limestone
/// limestone 8690
/// clay 9098
/// topsoil 98100
/// air 100+ ← everything above the surface is simply the top air run
/// </code>
///
/// ═══ 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 (<see cref="MaterialRegistry.Air"/>).
///
/// 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.
/// </summary>
public sealed class Column
{
private readonly List<MaterialRun> _runs;
public Column(int expectedRuns = 8)
{
_runs = new List<MaterialRun>(expectedRuns);
}
/// <summary>The runs, bottom to top. Read-only: mutation goes through <see cref="Append"/>.</summary>
public IReadOnlyList<MaterialRun> Runs => _runs;
public int RunCount => _runs.Count;
/// <summary>
/// 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.
/// </summary>
public void Append(MaterialRun run)
{
_runs.Add(run);
}
/// <summary>Append a run by its parts. Convenience over <see cref="Append(MaterialRun)"/>.</summary>
public void Append(MaterialKey material, int thickness, RunOrigin origin = RunOrigin.Natural)
=> Append(new MaterialRun(material, thickness, origin));
/// <summary>
/// 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.
/// </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)";
}
}