using NeonSprawl.Server.Game.Npc;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.World;
namespace NeonSprawl.Server.Game.Combat;
/// Deterministic NPC telegraph-complete damage resolve (NEO-95 / NEO-98).
public static class NpcAttackOperations
{
///
/// Applies catalog ability baseDamage to when the threat row still
/// names that holder for and the holder is within the attack ability's
/// of the NPC anchor.
///
public static bool TryResolveTelegraphComplete(
string npcInstanceId,
string holderPlayerId,
string attackAbilityId,
IAbilityDefinitionRegistry abilityRegistry,
IPositionStateStore positionStore,
IPlayerCombatHealthStore playerHealthStore,
IThreatStateStore threatStore)
{
if (!abilityRegistry.TryGetDefinition(attackAbilityId, out var ability))
{
return false;
}
if (ability.BaseDamage <= 0)
{
return true;
}
if (string.IsNullOrWhiteSpace(holderPlayerId) ||
!threatStore.TryGet(npcInstanceId, out var threat) ||
string.IsNullOrEmpty(threat.AggroHolderPlayerId) ||
!string.Equals(threat.AggroHolderPlayerId, holderPlayerId, StringComparison.Ordinal))
{
return false;
}
if (!IsHolderWithinAbilityMaxRange(npcInstanceId, holderPlayerId, ability.MaxRange, positionStore))
{
return false;
}
if (!playerHealthStore.TryApplyDamage(holderPlayerId, ability.BaseDamage, out var snapshot))
{
return false;
}
if (snapshot.Defeated)
{
// TODO(E9.M1): player_death
}
return true;
}
internal static bool IsHolderWithinAbilityMaxRange(
string npcInstanceId,
string holderPlayerId,
double maxRange,
IPositionStateStore positionStore)
{
if (maxRange <= 0 ||
!PrototypeNpcRegistry.TryGet(npcInstanceId, out var entry) ||
!positionStore.TryGetPosition(holderPlayerId, out var playerSnap))
{
return false;
}
return HorizontalReach.IsWithinHorizontalRadius(
playerSnap.X,
playerSnap.Z,
entry.X,
entry.Z,
maxRange);
}
}