using Theriapolis.Core.Data; using Theriapolis.Core.World.Polylines; namespace Theriapolis.Core.World; /// /// 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). /// 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 Rivers { get; } = new(); public List Roads { get; } = new(); public List Rails { get; } = new(); // ── Phase 2+3: Settlements ─────────────────────────────────────────────── public List Settlements { get; } = new(); // ── Phase 2+3: Bridges (road/rail crossings over rivers) ──────────────── public List 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 StageHashes { get; } = new(); // ── Helper: macro cell for a given world tile coordinate ───────────────── /// /// 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 . /// 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]; } /// /// Returns the macro cell stored on the given tile. /// overwrites each tile's and /// 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. /// 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 ────────────────────────────────────── /// FNV-1a hash over all elevation values (for determinism tests). 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; } /// FNV-1a hash over all settlements (sorted by ID). 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; } /// FNV-1a hash over all polyline points (rivers, then roads, then rails). 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; } }