namespace Theriapolis.Core.Entities; /// /// Owns live actors. Phase 4 only ever holds the single player actor; this /// class exists so Phase 5/6 can add NPCs without further architectural change. /// public sealed class ActorManager { private readonly List _actors = new(); private int _nextId = 1; public IReadOnlyList All => _actors; public PlayerActor? Player { get; private set; } /// Creates the player actor. Idempotent on repeat calls — returns the existing player. public PlayerActor SpawnPlayer(Util.Vec2 worldPixelPos) { if (Player is not null) return Player; var p = new PlayerActor { Id = _nextId++, Position = worldPixelPos, Allegiance = Rules.Character.Allegiance.Player, }; _actors.Add(p); Player = p; return p; } /// /// Phase 5 overload: spawn the player and attach a freshly-built character /// in one step. The character carries the player's name into the actor. /// public PlayerActor SpawnPlayer(Util.Vec2 worldPixelPos, Rules.Character.Character character) { var p = SpawnPlayer(worldPixelPos); p.Character = character; // The character record's identity (name) wins over the default actor name. // Other fields (HP, abilities, inventory) live exclusively on the Character. return p; } /// /// Restore a player actor from a save. Does not allocate a new id — the /// saved id is restored as-is so cross-save references stay stable. /// public PlayerActor RestorePlayer(PlayerActorState state) { var p = new PlayerActor { Id = state.Id }; p.RestoreState(state); _actors.Add(p); Player = p; if (state.Id >= _nextId) _nextId = state.Id + 1; return p; } /// /// Phase 5 M5: spawn an NPC at the given world-pixel position. The caller /// chooses the template (including any DangerZone-driven per-zone selection). /// Returns the spawned actor with a freshly-allocated id. /// public NpcActor SpawnNpc( Data.NpcTemplateDef template, Util.Vec2 worldPixelPos, Tactical.ChunkCoord? sourceChunk = null, int? sourceSpawnIndex = null) { var npc = new NpcActor(template) { Id = _nextId++, Position = worldPixelPos, SourceChunk = sourceChunk, SourceSpawnIndex = sourceSpawnIndex, }; _actors.Add(npc); return npc; } /// /// Phase 6 M1 — adopt a pre-constructed NpcActor (e.g. a resident built /// by with the resident /// template constructor). Assigns an id and registers in the actor list. /// public NpcActor SpawnNpc(NpcActor pre) { if (pre.Id <= 0) { // Use reflection-free init via a fresh actor copy. NpcActor.Id is // init-only, but the only place we hit this path is the // resident-instantiator which constructs `pre` with Id = -1 // expecting us to assign. Build the real one here. NpcActor adopted = pre.Resident is not null ? new NpcActor(pre.Resident) { Id = _nextId++, Position = pre.Position, SourceChunk = pre.SourceChunk, SourceSpawnIndex = pre.SourceSpawnIndex, RoleTag = pre.RoleTag, HomeSettlementId = pre.HomeSettlementId, } : new NpcActor(pre.Template!) { Id = _nextId++, Position = pre.Position, SourceChunk = pre.SourceChunk, SourceSpawnIndex = pre.SourceSpawnIndex, RoleTag = pre.RoleTag, HomeSettlementId = pre.HomeSettlementId, }; _actors.Add(adopted); return adopted; } _actors.Add(pre); if (pre.Id >= _nextId) _nextId = pre.Id + 1; return pre; } /// Remove an actor by id. Returns true if it was found. public bool RemoveActor(int id) { for (int i = 0; i < _actors.Count; i++) { if (_actors[i].Id == id) { _actors.RemoveAt(i); return true; } } return false; } /// Returns every NPC currently spawned. public IEnumerable Npcs { get { foreach (var a in _actors) if (a is NpcActor npc) yield return npc; } } /// Find a live NPC originating from a specific chunk + spawn index. public NpcActor? FindNpcBySource(Tactical.ChunkCoord chunk, int spawnIndex) { foreach (var a in _actors) if (a is NpcActor npc && npc.SourceChunk.HasValue && npc.SourceChunk.Value.Equals(chunk) && npc.SourceSpawnIndex == spawnIndex) return npc; return null; } }