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