using System.Collections.Concurrent; using NeonSprawl.Server.Game.Npc; namespace NeonSprawl.Server.Game.Combat; /// Thread-safe in-memory HP for prototype NPC combat instances; empty at startup — lazy rows only (NEO-80, NEO-91). public sealed class InMemoryCombatEntityHealthStore(INpcBehaviorDefinitionRegistry behaviorRegistry) : ICombatEntityHealthStore { private readonly ConcurrentDictionary currentHpById = new(StringComparer.Ordinal); private readonly ConcurrentDictionary idLocks = new(StringComparer.Ordinal); /// public bool TryGet(string? targetId, out CombatEntityHealthSnapshot snapshot) { var key = NormalizeTargetId(targetId); if (key.Length == 0 || !TryResolveMaxHp(key, out var maxHp)) { snapshot = default; return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { EnsureRowInitialized(key, maxHp); snapshot = CreateSnapshot(key, maxHp, currentHpById[key]); return true; } } /// public bool TryApplyDamage(string? targetId, int damage, out CombatEntityHealthSnapshot snapshot) { snapshot = default; if (damage < 0) { return false; } var key = NormalizeTargetId(targetId); if (key.Length == 0 || !TryResolveMaxHp(key, out var maxHp)) { return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { EnsureRowInitialized(key, maxHp); var current = currentHpById[key]; var next = Math.Max(0, current - damage); currentHpById[key] = next; snapshot = CreateSnapshot(key, maxHp, next); return true; } } /// public bool TryResetToFull(string? targetId, out CombatEntityHealthSnapshot snapshot) { var key = NormalizeTargetId(targetId); if (key.Length == 0 || !TryResolveMaxHp(key, out var maxHp)) { snapshot = default; return false; } lock (idLocks.GetOrAdd(key, _ => new object())) { currentHpById[key] = maxHp; snapshot = CreateSnapshot(key, maxHp, maxHp); return true; } } private bool TryResolveMaxHp(string normalizedId, out int maxHp) { maxHp = 0; if (!PrototypeNpcRegistry.TryGet(normalizedId, out var entry)) { return false; } if (!behaviorRegistry.TryGetDefinition(entry.BehaviorDefId, out var definition)) { return false; } maxHp = definition.MaxHp; return true; } private void EnsureRowInitialized(string normalizedId, int maxHp) { if (!currentHpById.ContainsKey(normalizedId)) { currentHpById[normalizedId] = maxHp; } } private static CombatEntityHealthSnapshot CreateSnapshot(string targetId, int maxHp, int currentHp) => new( targetId, maxHp, currentHp, currentHp <= 0); private static string NormalizeTargetId(string? targetId) => targetId?.Trim().ToLowerInvariant() ?? string.Empty; }