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