islaApocalypse-v2/Core/Scripts/MaterialRegistry.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

117 lines
6.3 KiB
C#

using System;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
/// <summary>
/// The two material schemas, as APPEND-ONLY registries. → `Design - Data - Material Schema.md`, D-054.
///
/// ⭐ NO MATERIAL LOGIC IS HARDCODED ANYWHERE — everything reads from here.
/// This is the specific discipline that makes deferring the full material list SAFE, unlike
/// biomes, which were hardcoded and coupled. Understand the shape now; fill the rows forever.
///
/// ⚠ THE SEED ROWS BELOW ARE NOT THE MATERIAL LIST. They are the few that are certain
/// (limestone is in — karst substrate). Do NOT enumerate the full list now; design materials
/// incrementally as building proceeds. Adding one is one new row, appended, nothing else.
///
/// ⚠ REGISTRATION ORDER IS NOT MEANINGFUL. It sets <c>RuntimeIndex</c>, which is never
/// serialized. Reordering these lines must not change a single persisted byte, now or ever.
///
/// Phase 0: rows are seeded in code. Loading them from a data file is a later phase — the shape
/// here (identity + properties, no logic) is what makes that a loader change and nothing more.
/// </summary>
public static class MaterialRegistry
{
// ---- terrain --------------------------------------------------------
private static readonly Dictionary<MaterialKey, TerrainMaterial> _terrain = new();
private static readonly List<TerrainMaterial> _terrainByIndex = new();
// ---- building -------------------------------------------------------
private static readonly Dictionary<MaterialKey, BuildingMaterial> _building = new();
private static readonly List<BuildingMaterial> _buildingByIndex = new();
/// <summary>
/// AIR. The one key the column model leans on structurally: everything above the surface is
/// the top AIR run, and a cave is an air run mid-column. It is a material row like any
/// other — there is no "is air" branch anywhere. → `Design - Data - Column Model.md`.
/// </summary>
public static readonly MaterialKey Air = new("core.air");
static MaterialRegistry()
{
// ===== TERRAIN — seed rows, NOT final =====
// key display hardness yield per-voxel support
RegisterTerrain("core.air", "Air", 0.0f, MaterialKey.None, 0, 0.0f);
RegisterTerrain("core.sand", "Sand", 0.2f, "core.sand", 1, 0.1f);
RegisterTerrain("core.dirt", "Dirt", 0.3f, "core.dirt", 1, 0.2f);
RegisterTerrain("core.clay", "Clay", 0.4f, "core.clay", 1, 0.3f);
RegisterTerrain("core.limestone", "Limestone", 0.7f, "core.limestone", 1, 0.7f); // karst substrate
RegisterTerrain("core.stone", "Stone", 0.9f, "core.stone", 1, 0.9f);
RegisterTerrain("core.bedrock", "Bedrock", 1.0f, MaterialKey.None, 0, 1.0f); // unmineable floor
// ===== BUILDING — seed rows, NOT final =====
// key display reach decay weight durability
RegisterBuilding("build.thatch", "Thatch/Palm", 1.5f, 0.60f, 0.2f, 0.2f);
RegisterBuilding("build.wood", "Wood Plank", 4.0f, 0.30f, 0.5f, 0.5f);
RegisterBuilding("build.stone", "Worked Stone", 6.0f, 0.20f, 1.0f, 0.8f);
RegisterBuilding("build.concrete", "Concrete", 9.0f, 0.12f, 1.1f, 0.9f);
RegisterBuilding("build.steel", "Steel", 14.0f, 0.06f, 0.9f, 1.0f);
}
// ---- registration (append-only; no removal, by design) --------------
private static void RegisterTerrain(string key, string name, float hardness, MaterialKey yieldKey, int yieldPer, float support)
=> RegisterTerrain(new TerrainMaterial(new MaterialKey(key), name, hardness, yieldKey, yieldPer, support));
private static void RegisterTerrain(string key, string name, float hardness, string yieldKey, int yieldPer, float support)
=> RegisterTerrain(key, name, hardness, new MaterialKey(yieldKey), yieldPer, support);
private static void RegisterBuilding(string key, string name, float reach, float decay, float weight, float durability)
=> RegisterBuilding(new BuildingMaterial(new MaterialKey(key), name, reach, decay, weight, durability));
/// <summary>Append a terrain material. Public so mods and data loaders add rows the same way seeds do.</summary>
public static void RegisterTerrain(TerrainMaterial material)
{
if (material == null) throw new ArgumentNullException(nameof(material));
if (_terrain.ContainsKey(material.Key))
throw new InvalidOperationException($"Terrain material '{material.Key}' is already registered. The registry is append-only; keys are unique.");
material.RuntimeIndex = _terrainByIndex.Count;
_terrainByIndex.Add(material);
_terrain.Add(material.Key, material);
}
/// <summary>Append a building material.</summary>
public static void RegisterBuilding(BuildingMaterial material)
{
if (material == null) throw new ArgumentNullException(nameof(material));
if (_building.ContainsKey(material.Key))
throw new InvalidOperationException($"Building material '{material.Key}' is already registered. The registry is append-only; keys are unique.");
material.RuntimeIndex = _buildingByIndex.Count;
_buildingByIndex.Add(material);
_building.Add(material.Key, material);
}
// ---- lookup ---------------------------------------------------------
public static TerrainMaterial Terrain(MaterialKey key)
=> _terrain.TryGetValue(key, out var m)
? m
: throw new KeyNotFoundException($"No terrain material registered for key '{key}'.");
public static BuildingMaterial Building(MaterialKey key)
=> _building.TryGetValue(key, out var m)
? m
: throw new KeyNotFoundException($"No building material registered for key '{key}'.");
public static bool TryTerrain(MaterialKey key, out TerrainMaterial material) => _terrain.TryGetValue(key, out material);
public static bool TryBuilding(MaterialKey key, out BuildingMaterial material) => _building.TryGetValue(key, out material);
public static bool IsRegisteredTerrain(MaterialKey key) => _terrain.ContainsKey(key);
public static IReadOnlyList<TerrainMaterial> AllTerrain => _terrainByIndex;
public static IReadOnlyList<BuildingMaterial> AllBuilding => _buildingByIndex;
public static int TerrainCount => _terrainByIndex.Count;
public static int BuildingCount => _buildingByIndex.Count;
}
}