using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.World; namespace NeonSprawl.Server.Game.Npc; /// Deterministic prototype aggro acquire and leash-clear rules (NEO-92). public static class AggroOperations { /// /// Sets aggro holder to when the NPC row is empty. /// Returns true only when holder was empty and is now set. /// public static bool TryAcquire(string npcInstanceId, string playerId, IThreatStateStore threatStore) { var npcKey = NormalizeId(npcInstanceId); var playerKey = NormalizeId(playerId); if (npcKey.Length == 0 || playerKey.Length == 0 || !PrototypeNpcRegistry.TryGet(npcKey, out _)) { return false; } if (!threatStore.TryGet(npcKey, out var current)) { return false; } if (!string.IsNullOrEmpty(current.AggroHolderPlayerId)) { return false; } return threatStore.TrySetHolder(npcKey, playerKey); } /// /// Clears aggro on every prototype NPC instance where is holder /// and horizontal distance from the NPC anchor exceeds catalog leashRadius. /// public static void TryClearOnLeashForPlayer( string playerId, IPositionStateStore positions, INpcBehaviorDefinitionRegistry behaviorRegistry, IThreatStateStore threatStore) { var playerKey = NormalizeId(playerId); if (playerKey.Length == 0 || !positions.TryGetPosition(playerKey, out var playerSnap)) { return; } foreach (var npcInstanceId in PrototypeNpcRegistry.GetInstanceIdsInOrder()) { TryClearOnLeashForNpc( npcInstanceId, playerKey, in playerSnap, behaviorRegistry, threatStore); } } /// Clears aggro for one NPC when the holder is outside that NPC's leash radius. public static void TryClearOnLeashForNpc( string npcInstanceId, string playerIdLowercase, in PositionSnapshot playerSnap, INpcBehaviorDefinitionRegistry behaviorRegistry, IThreatStateStore threatStore) { var npcKey = NormalizeId(npcInstanceId); if (npcKey.Length == 0 || !PrototypeNpcRegistry.TryGet(npcKey, out var entry) || !threatStore.TryGet(npcKey, out var threat) || string.IsNullOrEmpty(threat.AggroHolderPlayerId) || !string.Equals(threat.AggroHolderPlayerId, playerIdLowercase, StringComparison.Ordinal)) { return; } if (!behaviorRegistry.TryGetDefinition(entry.BehaviorDefId, out var behavior)) { return; } var distance = HorizontalReach.HorizontalDistance( playerSnap.X, playerSnap.Z, entry.X, entry.Z); if (distance > behavior.LeashRadius) { threatStore.TryClearHolder(npcKey); } } /// Clears aggro holders on all prototype NPC instances (dev fixture). public static void ClearAllPrototypeHolders(IThreatStateStore threatStore) { foreach (var npcInstanceId in PrototypeNpcRegistry.GetInstanceIdsInOrder()) { threatStore.TryClearHolder(npcInstanceId); } } private static string NormalizeId(string? raw) => raw?.Trim().ToLowerInvariant() ?? string.Empty; }