b451f83174
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>
150 lines
5.3 KiB
C#
150 lines
5.3 KiB
C#
namespace Theriapolis.Core.Entities;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class ActorManager
|
|
{
|
|
private readonly List<Actor> _actors = new();
|
|
private int _nextId = 1;
|
|
|
|
public IReadOnlyList<Actor> All => _actors;
|
|
|
|
public PlayerActor? Player { get; private set; }
|
|
|
|
/// <summary>Creates the player actor. Idempotent on repeat calls — returns the existing player.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Phase 6 M1 — adopt a pre-constructed NpcActor (e.g. a resident built
|
|
/// by <see cref="Rules.Combat.ResidentInstantiator"/> with the resident
|
|
/// template constructor). Assigns an id and registers in the actor list.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Remove an actor by id. Returns true if it was found.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Returns every NPC currently spawned.</summary>
|
|
public IEnumerable<NpcActor> Npcs
|
|
{
|
|
get
|
|
{
|
|
foreach (var a in _actors) if (a is NpcActor npc) yield return npc;
|
|
}
|
|
}
|
|
|
|
/// <summary>Find a live NPC originating from a specific chunk + spawn index.</summary>
|
|
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;
|
|
}
|
|
}
|