Files
TheriapolisV3/Theriapolis.Core/Tactical/TacticalChunk.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

103 lines
4.0 KiB
C#

namespace Theriapolis.Core.Tactical;
/// <summary>
/// One streamed chunk of the tactical world. Always
/// <see cref="C.TACTICAL_CHUNK_SIZE"/>² tactical tiles.
///
/// Chunks are produced by <see cref="TacticalChunkGen"/> 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 <see cref="ChunkStreamer"/>'s job.
/// </summary>
public sealed class TacticalChunk
{
public ChunkCoord Coord { get; }
/// <summary>Indexed [tx, ty] in chunk-local coordinates (0..CHUNK_SIZE-1).</summary>
public TacticalTile[,] Tiles { get; } = new TacticalTile[C.TACTICAL_CHUNK_SIZE, C.TACTICAL_CHUNK_SIZE];
/// <summary>Phase-4 spawn list. Stored, not yet acted on (Phase 5 reads it).</summary>
public List<TacticalSpawn> Spawns { get; } = new();
/// <summary>
/// Phase 5 M5 difficulty tier (0..C.DANGER_ZONE_MAX). Drives which template
/// each <see cref="TacticalSpawn.Kind"/> instantiates as a live NPC. Set
/// in <see cref="TacticalChunkGen"/>'s spawn pass; folded into the hash
/// so determinism tests catch zone-formula drift.
/// </summary>
public byte DangerZone { get; set; }
/// <summary>Set true by ChunkStreamer when a delta is applied; flushed back on eviction.</summary>
public bool Dirty { get; set; }
/// <summary>True if any field has been modified relative to the deterministic baseline.</summary>
public bool HasDelta { get; set; }
public TacticalChunk(ChunkCoord coord) { Coord = coord; }
/// <summary>Top-left tactical tile coordinate of this chunk in world-pixel space.</summary>
public int OriginX => Coord.X * C.TACTICAL_CHUNK_SIZE;
public int OriginY => Coord.Y * C.TACTICAL_CHUNK_SIZE;
/// <summary>Returns the tile at chunk-local (lx, ly) — bounds-checked.</summary>
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];
}
/// <summary>FNV-1a hash over every tile. Used by determinism tests.</summary>
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,
/// <summary>
/// Phase 6 M0 — emitted by <see cref="World.Settlements.SettlementStamper"/>
/// for each occupied building role. Phase 6 M1 instantiates these as
/// friendly <see cref="Entities.NpcActor"/>s with role-specific dialogue.
/// </summary>
Resident = 16,
}
/// <summary>Single spawn record in chunk-local coordinates. Phase 5 acts on these.</summary>
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; }
}