78 lines
3.1 KiB
C#
78 lines
3.1 KiB
C#
|
|
using Theriapolis.Core.Entities;
|
||
|
|
|
||
|
|
namespace Theriapolis.Core.World.Settlements;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Phase 6 M1 — runtime map between symbolic ids used by quest scripts /
|
||
|
|
/// dialogue conditions and live world entities. Quest scripts never embed
|
||
|
|
/// world coordinates; they reference NPCs by role tag and locations by
|
||
|
|
/// anchor id (per master plan §8.4):
|
||
|
|
///
|
||
|
|
/// <code>
|
||
|
|
/// anchor:millhaven → the live Settlement
|
||
|
|
/// role:millhaven.innkeeper → the live NpcActor for that named role
|
||
|
|
/// </code>
|
||
|
|
///
|
||
|
|
/// Built lazily as chunks stream in: when a settlement's buildings resolve
|
||
|
|
/// (and any named NPC instantiates), the entry registers here. Phase 6 M2
|
||
|
|
/// persists the registry; M1 rebuilds it on every load from the live
|
||
|
|
/// settlement list and active NpcActors.
|
||
|
|
/// </summary>
|
||
|
|
public sealed class AnchorRegistry
|
||
|
|
{
|
||
|
|
private readonly Dictionary<string, int> _anchorToSettlementId = new(StringComparer.OrdinalIgnoreCase);
|
||
|
|
private readonly Dictionary<string, int> _roleToNpcId = new(StringComparer.OrdinalIgnoreCase);
|
||
|
|
|
||
|
|
/// <summary>Register a settlement under its anchor id (e.g. "anchor:millhaven").</summary>
|
||
|
|
public void RegisterAnchor(NarrativeAnchor anchor, int settlementId)
|
||
|
|
{
|
||
|
|
string key = $"anchor:{anchor.ToString().ToLowerInvariant()}";
|
||
|
|
_anchorToSettlementId[key] = settlementId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Register an NpcActor under its named role tag (e.g. "role:millhaven.innkeeper").</summary>
|
||
|
|
public void RegisterRole(string roleTag, int npcId)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(roleTag)) return;
|
||
|
|
if (!roleTag.Contains('.')) return; // generic role; only named (anchor.role) roles register
|
||
|
|
_roleToNpcId[$"role:{roleTag.ToLowerInvariant()}"] = npcId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Forget the role mapping (called on chunk evict / NPC despawn).</summary>
|
||
|
|
public void UnregisterRole(string roleTag)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(roleTag)) return;
|
||
|
|
_roleToNpcId.Remove($"role:{roleTag.ToLowerInvariant()}");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Resolve "anchor:millhaven" → SettlementId (or null when not registered yet).</summary>
|
||
|
|
public int? ResolveAnchor(string id)
|
||
|
|
{
|
||
|
|
return _anchorToSettlementId.TryGetValue(id, out int sid) ? sid : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Resolve "role:millhaven.innkeeper" → NpcId (or null when not loaded / not yet streamed).</summary>
|
||
|
|
public int? ResolveRole(string id)
|
||
|
|
{
|
||
|
|
return _roleToNpcId.TryGetValue(id, out int nid) ? nid : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Bulk re-register every settlement's anchor (e.g. after world load).</summary>
|
||
|
|
public void RegisterAllAnchors(WorldState world)
|
||
|
|
{
|
||
|
|
foreach (var s in world.Settlements)
|
||
|
|
if (s.Anchor is { } a)
|
||
|
|
RegisterAnchor(a, s.Id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>For diagnostics: every (id → entityId) mapping currently held.</summary>
|
||
|
|
public IReadOnlyDictionary<string, int> AllAnchors => _anchorToSettlementId;
|
||
|
|
public IReadOnlyDictionary<string, int> AllRoles => _roleToNpcId;
|
||
|
|
|
||
|
|
public void Clear()
|
||
|
|
{
|
||
|
|
_anchorToSettlementId.Clear();
|
||
|
|
_roleToNpcId.Clear();
|
||
|
|
}
|
||
|
|
}
|