diff --git a/Source/Framework/Constants/Spells/SpellConst.cs b/Source/Framework/Constants/Spells/SpellConst.cs index 6b56fc929..262f19a33 100644 --- a/Source/Framework/Constants/Spells/SpellConst.cs +++ b/Source/Framework/Constants/Spells/SpellConst.cs @@ -1764,8 +1764,8 @@ namespace Framework.Constants TriggersChanneling = 0x10, // Triggers Channeling LimitN = 0x20, // Limit N Description Remove Previous Application To Another Unit If Applied IgnoreAreaEffectPvpCheck = 0x40, // Ignore Area Effect Pvp Check - NotOnPlayer = 0x80, /*Nyi*/ // Not On Player - NotOnPlayerControlledNpc = 0x100, /*Nyi*/ // Not On Player Controlled Npc + NotOnPlayer = 0x80, // Not On Player + NotOnPlayerControlledNpc = 0x100, // Not On Player Controlled Npc ExtraInitialPeriod = 0x200, // Extra Initial Period Description Immediately Do Periodic Tick On Apply DoNotDisplayDuration = 0x400, // Do Not Display Duration ImpliedTargeting = 0x800, // Implied Targeting (Client Only) diff --git a/Source/Game/AI/CoreAI/UnitAI.cs b/Source/Game/AI/CoreAI/UnitAI.cs index 2ef91c64d..dbbcc43ef 100644 --- a/Source/Game/AI/CoreAI/UnitAI.cs +++ b/Source/Game/AI/CoreAI/UnitAI.cs @@ -124,11 +124,17 @@ namespace Game.AI return SelectTarget(targetType, offset, new DefaultTargetSelector(me, dist, playerOnly, withTank, aura)); } + public Unit SelectTarget(SelectTargetMethod targetType, uint offset, ICheck selector) + { + return SelectTarget(targetType, offset, selector.Invoke); + } + /// /// Select the best target (in order) satisfying from the threat list. /// If is nonzero, the first entries in order (or MAXTHREAT order, if is RANDOM) are skipped. /// - public Unit SelectTarget(SelectTargetMethod targetType, uint offset, ICheck selector) + public delegate bool SelectTargetDelegate(Unit unit); + public Unit SelectTarget(SelectTargetMethod targetType, uint offset, SelectTargetDelegate selector) { ThreatManager mgr = GetThreatManager(); // shortcut: if we ignore the first elements, and there are at most elements, then we ignore ALL elements @@ -162,14 +168,14 @@ namespace Game.AI /// public List SelectTargetList(uint num, SelectTargetMethod targetType, uint offset = 0, float dist = 0f, bool playerOnly = false, bool withTank = true, int aura = 0) { - return SelectTargetList(num, targetType, offset, new DefaultTargetSelector(me, dist, playerOnly, withTank, aura)); + return SelectTargetList(num, targetType, offset, new DefaultTargetSelector(me, dist, playerOnly, withTank, aura).Invoke); } /// /// Select the best (up to) targets (in order) satisfying from the threat list and stores them in (which is cleared first). /// If is nonzero, the first entries in order (or MAXTHREAT order, if is RANDOM) are skipped. /// - public List SelectTargetList(uint num, SelectTargetMethod targetType, uint offset, ICheck selector) + public List SelectTargetList(uint num, SelectTargetMethod targetType, uint offset, SelectTargetDelegate selector) { var targetList = new List(); @@ -228,7 +234,7 @@ namespace Game.AI } // then finally filter by predicate - targetList.RemoveAll(unit => !selector.Invoke(unit)); + targetList.RemoveAll(unit => !selector(unit)); if (targetList.Count <= num) return targetList; @@ -264,8 +270,23 @@ namespace Game.AI var spellInfo = Global.SpellMgr.GetSpellInfo(spellId, me.GetMap().GetDifficultyID()); if (spellInfo != null) { - bool playerOnly = spellInfo.HasAttribute(SpellAttr3.OnlyOnPlayer); - target = SelectTarget(SelectTargetMethod.Random, 0, spellInfo.GetMaxRange(false), playerOnly); + DefaultTargetSelector targetSelectorInner = new(me, spellInfo.GetMaxRange(false), false, true, 0); + bool targetSelector(Unit candidate) + { + if (!candidate.IsPlayer()) + { + if (spellInfo.HasAttribute(SpellAttr3.OnlyOnPlayer)) + return false; + + if (spellInfo.HasAttribute(SpellAttr5.NotOnPlayerControlledNpc) && candidate.IsControlledByPlayer()) + return false; + } + else if (spellInfo.HasAttribute(SpellAttr5.NotOnPlayer)) + return false; + + return targetSelectorInner.Invoke(candidate); + }; + target = SelectTarget(SelectTargetMethod.Random, 0, targetSelector); } break; } @@ -278,12 +299,25 @@ namespace Game.AI var spellInfo = Global.SpellMgr.GetSpellInfo(spellId, me.GetMap().GetDifficultyID()); if (spellInfo != null) { - bool playerOnly = spellInfo.HasAttribute(SpellAttr3.OnlyOnPlayer); float range = spellInfo.GetMaxRange(false); - DefaultTargetSelector targetSelector = new(me, range, playerOnly, true, -(int)spellId); - if (!spellInfo.HasAuraInterruptFlag(SpellAuraInterruptFlags.NotVictim) - && targetSelector.Invoke(me.GetVictim())) + DefaultTargetSelector targetSelectorInner = new(me, range, false, true, -(int)spellId); + bool targetSelector(Unit candidate) + { + if (!candidate.IsPlayer()) + { + if (spellInfo.HasAttribute(SpellAttr3.OnlyOnPlayer)) + return false; + + if (spellInfo.HasAttribute(SpellAttr5.NotOnPlayerControlledNpc) && candidate.IsControlledByPlayer()) + return false; + } + else if (spellInfo.HasAttribute(SpellAttr5.NotOnPlayer)) + return false; + + return targetSelectorInner.Invoke(candidate); + }; + if (!spellInfo.HasAuraInterruptFlag(SpellAuraInterruptFlags.NotVictim) && targetSelector(me.GetVictim())) target = me.GetVictim(); else target = SelectTarget(SelectTargetMethod.Random, 0, targetSelector); diff --git a/Source/Game/Maps/GridNotifiers.cs b/Source/Game/Maps/GridNotifiers.cs index 1ddfe13ee..94372c827 100644 --- a/Source/Game/Maps/GridNotifiers.cs +++ b/Source/Game/Maps/GridNotifiers.cs @@ -2173,8 +2173,19 @@ namespace Game.Maps if (u.IsTypeId(TypeId.Unit) && u.IsTotem()) return false; - if (_spellInfo != null && _spellInfo.HasAttribute(SpellAttr3.OnlyOnPlayer) && !u.IsPlayer()) - return false; + if (_spellInfo != null) + { + if (!u.IsPlayer()) + { + if (_spellInfo.HasAttribute(SpellAttr3.OnlyOnPlayer)) + return false; + + if (_spellInfo.HasAttribute(SpellAttr5.NotOnPlayerControlledNpc) && u.IsControlledByPlayer()) + return false; + } + else if (_spellInfo.HasAttribute(SpellAttr5.NotOnPlayer)) + return false; + } if (!i_funit.IsValidAttackTarget(u, _spellInfo)) return false; diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 11314bf6c..1486eda2f 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -1594,6 +1594,8 @@ namespace Game.Spells retMask &= GridMapTypeMask.Corpse | GridMapTypeMask.Player; if (m_spellInfo.HasAttribute(SpellAttr3.OnlyOnGhosts)) retMask &= GridMapTypeMask.Player; + if (m_spellInfo.HasAttribute(SpellAttr5.NotOnPlayer)) + retMask &= ~GridMapTypeMask.Player; if (condList != null) retMask &= Global.ConditionMgr.GetSearcherTypeMaskForConditionList(condList); diff --git a/Source/Game/Spells/SpellInfo.cs b/Source/Game/Spells/SpellInfo.cs index e4233e9ad..686f793a6 100644 --- a/Source/Game/Spells/SpellInfo.cs +++ b/Source/Game/Spells/SpellInfo.cs @@ -1080,8 +1080,16 @@ namespace Game.Spells return SpellCastResult.SpellCastOk; // corpseOwner and unit specific target checks - if (HasAttribute(SpellAttr3.OnlyOnPlayer) && !unitTarget.IsTypeId(TypeId.Player)) - return SpellCastResult.TargetNotPlayer; + if (!unitTarget.IsPlayer()) + { + if (HasAttribute(SpellAttr3.OnlyOnPlayer)) + return SpellCastResult.TargetNotPlayer; + + if (HasAttribute(SpellAttr5.NotOnPlayerControlledNpc) && unitTarget.IsControlledByPlayer()) + return SpellCastResult.TargetIsPlayerControlled; + } + else if (HasAttribute(SpellAttr5.NotOnPlayer)) + return SpellCastResult.TargetIsPlayer; if (!IsAllowingDeadTarget() && !unitTarget.IsAlive()) return SpellCastResult.TargetsDead;