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;
}
}