Files
TheriapolisV3/Theriapolis.Core/World/WorldState.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

165 lines
7.5 KiB
C#

using Theriapolis.Core.Data;
using Theriapolis.Core.World.Polylines;
namespace Theriapolis.Core.World;
/// <summary>
/// The runtime world model. Holds all canonical simulation data for the generated continent.
/// Arrays are indexed [x, y] with (0,0) at the top-left (north-west).
/// </summary>
public sealed class WorldState
{
public ulong WorldSeed { get; init; }
// ── Canonical arrays ─────────────────────────────────────────────────────
public WorldTile[,] Tiles { get; } = new WorldTile[C.WORLD_WIDTH_TILES, C.WORLD_HEIGHT_TILES];
// Convenience accessors into the tile array (avoid struct copies in hot paths)
public ref WorldTile TileAt(int x, int y) => ref Tiles[x, y];
// ── Macro grid ────────────────────────────────────────────────────────────
public MacroCell[,]? MacroGrid { get; set; }
// ── Content defs (loaded from JSON, not generated) ────────────────────────
public BiomeDef[]? BiomeDefs { get; set; }
public FactionDef[]? FactionDefs { get; set; }
// ── Phase 2+3: Polylines (source of truth for linear features) ────────────
public List<Polyline> Rivers { get; } = new();
public List<Polyline> Roads { get; } = new();
public List<Polyline> Rails { get; } = new();
// ── Phase 2+3: Settlements ───────────────────────────────────────────────
public List<Settlement> Settlements { get; } = new();
// ── Phase 2+3: Bridges (road/rail crossings over rivers) ────────────────
public List<Bridge> Bridges { get; } = new();
// ── Phase 2+3: Computed maps ─────────────────────────────────────────────
public float[,]? Habitability { get; set; }
public float[,]? EncounterDensity { get; set; }
public FactionInfluenceMap? FactionInfluence { get; set; }
// ── Stage hashes for save integrity ───────────────────────────────────────
// Each stage appends its hash here after completing.
public Dictionary<string, ulong> StageHashes { get; } = new();
// ── Helper: macro cell for a given world tile coordinate ─────────────────
/// <summary>
/// Looks up a macro cell by unwarped grid position. Use this only for
/// pre-ElevationGen stages or places where you explicitly want the raw
/// grid lookup. Most callers should use <see cref="MacroCellForTile"/>.
/// </summary>
public MacroCell MacroCellAt(int tileX, int tileY)
{
if (MacroGrid is null) throw new InvalidOperationException("MacroGrid not loaded yet.");
int mx = Math.Clamp(tileX / (C.WORLD_WIDTH_TILES / C.MACRO_GRID_WIDTH), 0, C.MACRO_GRID_WIDTH - 1);
int my = Math.Clamp(tileY / (C.WORLD_HEIGHT_TILES / C.MACRO_GRID_HEIGHT), 0, C.MACRO_GRID_HEIGHT - 1);
return MacroGrid[mx, my];
}
/// <summary>
/// Returns the macro cell stored on the given tile. <see cref="ElevationGenStage"/>
/// overwrites each tile's <see cref="WorldTile.MacroX"/> and <see cref="WorldTile.MacroY"/>
/// with border-warped coordinates so that macro cell boundaries follow
/// organic wiggly curves instead of grid-aligned lines (Addendum A §1).
/// All post-ElevationGen stages and tests should use this method.
/// </summary>
public MacroCell MacroCellForTile(in WorldTile tile)
{
if (MacroGrid is null) throw new InvalidOperationException("MacroGrid not loaded yet.");
return MacroGrid[tile.MacroX, tile.MacroY];
}
// ── Sea level constant ─────────────────────────────────────────────────────
// Tiles with elevation < SeaLevel are ocean.
public const float SeaLevel = 0.35f;
// ── Fast hash for determinism tests ──────────────────────────────────────
/// <summary>FNV-1a hash over all elevation values (for determinism tests).</summary>
public ulong HashElevation()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong hash = FNV_OFFSET;
for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
{
uint bits = BitConverter.SingleToUInt32Bits(Tiles[x, y].Elevation);
hash = (hash ^ bits) * FNV_PRIME;
}
return hash;
}
public ulong HashMoisture()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong hash = FNV_OFFSET;
for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
{
uint bits = BitConverter.SingleToUInt32Bits(Tiles[x, y].Moisture);
hash = (hash ^ bits) * FNV_PRIME;
}
return hash;
}
public ulong HashTemperature()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong hash = FNV_OFFSET;
for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
{
uint bits = BitConverter.SingleToUInt32Bits(Tiles[x, y].Temperature);
hash = (hash ^ bits) * FNV_PRIME;
}
return hash;
}
public ulong HashBiomes()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong hash = FNV_OFFSET;
for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
hash = (hash ^ (byte)Tiles[x, y].Biome) * FNV_PRIME;
return hash;
}
/// <summary>FNV-1a hash over all settlements (sorted by ID).</summary>
public ulong HashSettlements()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong hash = FNV_OFFSET;
foreach (var s in Settlements.OrderBy(s => s.Id))
{
hash = (hash ^ (ulong)s.Id) * FNV_PRIME;
hash = (hash ^ (ulong)s.Tier) * FNV_PRIME;
hash = (hash ^ (ulong)s.TileX) * FNV_PRIME;
hash = (hash ^ (ulong)s.TileY) * FNV_PRIME;
}
return hash;
}
/// <summary>FNV-1a hash over all polyline points (rivers, then roads, then rails).</summary>
public ulong HashPolylines()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong hash = FNV_OFFSET;
foreach (var polylineList in new[] { Rivers, Roads, Rails })
foreach (var p in polylineList.OrderBy(p => p.Id))
foreach (var pt in p.Points)
{
hash = (hash ^ BitConverter.SingleToUInt32Bits(pt.X)) * FNV_PRIME;
hash = (hash ^ BitConverter.SingleToUInt32Bits(pt.Y)) * FNV_PRIME;
}
return hash;
}
}