namespace Theriapolis.Core.Tactical;
///
/// One streamed chunk of the tactical world. Always
/// ² tactical tiles.
///
/// Chunks are produced by from the deterministic
/// inputs (worldSeed, ChunkCoord, WorldState) plus an optional delta overlay
/// from the save layer. The chunk itself does not know whether deltas have
/// been applied — that's 's job.
///
public sealed class TacticalChunk
{
public ChunkCoord Coord { get; }
/// Indexed [tx, ty] in chunk-local coordinates (0..CHUNK_SIZE-1).
public TacticalTile[,] Tiles { get; } = new TacticalTile[C.TACTICAL_CHUNK_SIZE, C.TACTICAL_CHUNK_SIZE];
/// Phase-4 spawn list. Stored, not yet acted on (Phase 5 reads it).
public List Spawns { get; } = new();
///
/// Phase 5 M5 difficulty tier (0..C.DANGER_ZONE_MAX). Drives which template
/// each instantiates as a live NPC. Set
/// in 's spawn pass; folded into the hash
/// so determinism tests catch zone-formula drift.
///
public byte DangerZone { get; set; }
/// Set true by ChunkStreamer when a delta is applied; flushed back on eviction.
public bool Dirty { get; set; }
/// True if any field has been modified relative to the deterministic baseline.
public bool HasDelta { get; set; }
public TacticalChunk(ChunkCoord coord) { Coord = coord; }
/// Top-left tactical tile coordinate of this chunk in world-pixel space.
public int OriginX => Coord.X * C.TACTICAL_CHUNK_SIZE;
public int OriginY => Coord.Y * C.TACTICAL_CHUNK_SIZE;
/// Returns the tile at chunk-local (lx, ly) — bounds-checked.
public ref TacticalTile TileAt(int lx, int ly)
{
if ((uint)lx >= C.TACTICAL_CHUNK_SIZE || (uint)ly >= C.TACTICAL_CHUNK_SIZE)
throw new ArgumentOutOfRangeException($"({lx},{ly}) outside chunk");
return ref Tiles[lx, ly];
}
/// FNV-1a hash over every tile. Used by determinism tests.
public ulong Hash()
{
const ulong FNV_PRIME = 1099511628211UL;
const ulong FNV_OFFSET = 14695981039346656037UL;
ulong h = FNV_OFFSET;
for (int y = 0; y < C.TACTICAL_CHUNK_SIZE; y++)
for (int x = 0; x < C.TACTICAL_CHUNK_SIZE; x++)
{
ref var t = ref Tiles[x, y];
h = (h ^ (byte)t.Surface) * FNV_PRIME;
h = (h ^ (byte)t.Deco) * FNV_PRIME;
h = (h ^ t.Variant) * FNV_PRIME;
h = (h ^ t.Flags) * FNV_PRIME;
}
// Fold spawn list into the hash so any non-determinism there shows up.
foreach (var s in Spawns)
{
h = (h ^ (byte)s.Kind) * FNV_PRIME;
h = (h ^ (uint)s.LocalX) * FNV_PRIME;
h = (h ^ (uint)s.LocalY) * FNV_PRIME;
}
// Fold the DangerZone in so changes to the zone formula register as
// a chunk-hash change in the determinism tests.
h = (h ^ DangerZone) * FNV_PRIME;
return h;
}
}
public enum SpawnKind : byte
{
None = 0,
WildAnimal,
Brigand,
Merchant,
Patrol,
PoiGuard,
///
/// Phase 6 M0 — emitted by
/// for each occupied building role. Phase 6 M1 instantiates these as
/// friendly s with role-specific dialogue.
///
Resident = 16,
}
/// Single spawn record in chunk-local coordinates. Phase 5 acts on these.
public readonly struct TacticalSpawn
{
public readonly SpawnKind Kind;
public readonly int LocalX;
public readonly int LocalY;
public TacticalSpawn(SpawnKind kind, int lx, int ly) { Kind = kind; LocalX = lx; LocalY = ly; }
}