109 lines
3.5 KiB
C#
109 lines
3.5 KiB
C#
using NeonSprawl.Server.Game.PositionState;
|
|
using NeonSprawl.Server.Game.World;
|
|
|
|
namespace NeonSprawl.Server.Game.Npc;
|
|
|
|
/// <summary>Deterministic prototype aggro acquire and leash-clear rules (NEO-92).</summary>
|
|
public static class AggroOperations
|
|
{
|
|
/// <summary>
|
|
/// Sets aggro holder to <paramref name="playerId"/> when the NPC row is empty.
|
|
/// Returns <c>true</c> only when holder was empty and is now set.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears aggro on every prototype NPC instance where <paramref name="playerId"/> is holder
|
|
/// and horizontal distance from the NPC anchor exceeds catalog <c>leashRadius</c>.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Clears aggro for one NPC when the holder is outside that NPC's leash radius.</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Clears aggro holders on all prototype NPC instances (dev fixture).</summary>
|
|
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;
|
|
}
|