Files
CypherCore/Source/Game/Combat/ThreatManager.cs
T

887 lines
34 KiB
C#

/*
* Copyright (C) 2012-2020 CypherCore <http://github.com/CypherCore>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Framework.Constants;
using Game.Entities;
using Game.Networking.Packets;
using Game.Spells;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Game.Combat
{
public class ThreatManager
{
public static uint CLIENT_THREAT_UPDATE_INTERVAL = 1000u;
public Unit _owner;
bool _ownerCanHaveThreatList;
bool _ownerEngaged;
uint _updateClientTimer;
List<ThreatReference> _sortedThreatList = new();
Dictionary<ObjectGuid, ThreatReference> _myThreatListEntries = new();
ThreatReference _currentVictimRef;
Dictionary<ObjectGuid, ThreatReference> _threatenedByMe = new(); // these refs are entries for myself on other units' threat lists
public float[] _singleSchoolModifiers = new float[(int)SpellSchools.Max]; // most spells are single school - we pre-calculate these and store them
public volatile Dictionary<SpellSchoolMask, float> _multiSchoolModifiers = new(); // these are calculated on demand
public List<Tuple<ObjectGuid, uint>> _redirectInfo = new(); // current redirection targets and percentages (updated from registry in ThreatManager::UpdateRedirectInfo)
public Dictionary<uint, Dictionary<ObjectGuid, uint>> _redirectRegistry = new(); // spellid . (victim . pct); all redirection effects on us (removal individually managed by spell scripts because blizzard is dumb)
public static bool CanHaveThreatList(Unit who)
{
Creature cWho = who.ToCreature();
// only creatures can have threat list
if (!cWho)
return false;
// pets, totems and triggers cannot have threat list
if (cWho.IsPet() || cWho.IsTotem() || cWho.IsTrigger())
return false;
// summons cannot have a threat list, unless they are controlled by a creature
if (cWho.HasUnitTypeMask(UnitTypeMask.Minion | UnitTypeMask.Guardian) && !cWho.GetOwnerGUID().IsCreature())
return false;
return true;
}
public ThreatManager(Unit owner)
{
_owner = owner;
_updateClientTimer = CLIENT_THREAT_UPDATE_INTERVAL;
for (var i = 0; i < (int)SpellSchools.Max; ++i)
_singleSchoolModifiers[i] = 1.0f;
}
public void Initialize()
{
_ownerCanHaveThreatList = CanHaveThreatList(_owner);
}
public void Update(uint tdiff)
{
if (!CanHaveThreatList() || !IsEngaged())
return;
if (_updateClientTimer <= tdiff)
{
_updateClientTimer = CLIENT_THREAT_UPDATE_INTERVAL;
SendThreatListToClients();
}
else
_updateClientTimer -= tdiff;
}
public Unit GetCurrentVictim()
{
if (_currentVictimRef != null)
return _currentVictimRef.GetVictim();
return null;
}
public Unit GetAnyTarget()
{
foreach (ThreatReference refe in _sortedThreatList)
if (!refe.IsOffline())
return refe.GetVictim();
return null;
}
public Unit SelectVictim()
{
if (_sortedThreatList.Empty())
return null;
ThreatReference newVictimRef = ReselectVictim();
if (newVictimRef != _currentVictimRef)
{
if (newVictimRef != null)
SendNewVictimToClients(newVictimRef);
_currentVictimRef = newVictimRef;
}
return newVictimRef != null ? newVictimRef.GetVictim() : null;
}
public bool IsThreatListEmpty(bool includeOffline = false)
{
if (includeOffline)
return _sortedThreatList.Empty();
foreach (ThreatReference refe in _sortedThreatList)
if (refe.IsAvailable())
return false;
return true;
}
public bool IsThreatenedBy(ObjectGuid who, bool includeOffline = false)
{
var refe = _myThreatListEntries.LookupByKey(who);
if (refe == null)
return false;
return (includeOffline || refe.IsAvailable());
}
public bool IsThreatenedBy(Unit who, bool includeOffline = false) { return IsThreatenedBy(who.GetGUID(), includeOffline); }
public float GetThreat(Unit who, bool includeOffline = false)
{
var refe = _myThreatListEntries.LookupByKey(who.GetGUID());
if (refe == null)
return 0.0f;
return (includeOffline || refe.IsAvailable()) ? refe.GetThreat() : 0.0f;
}
public List<ThreatReference> GetModifiableThreatList()
{
return new(_sortedThreatList);
}
public bool IsThreateningAnyone(bool includeOffline = false)
{
if (includeOffline)
return !_threatenedByMe.Empty();
foreach (var pair in _threatenedByMe)
if (pair.Value.IsAvailable())
return true;
return false;
}
public bool IsThreateningTo(ObjectGuid who, bool includeOffline = false)
{
var refe = _threatenedByMe.LookupByKey(who);
if (refe == null)
return false;
return (includeOffline || refe.IsAvailable());
}
public bool IsThreateningTo(Unit who, bool includeOffline = false) { return IsThreateningTo(who.GetGUID(), includeOffline); }
public void UpdateOnlineStates(bool meThreateningOthers, bool othersThreateningMe)
{
if (othersThreateningMe)
foreach (var pair in _myThreatListEntries)
pair.Value.UpdateOnlineState();
if (meThreateningOthers)
foreach (var pair in _threatenedByMe)
pair.Value.UpdateOnlineState();
}
static void SaveCreatureHomePositionIfNeed(Creature c)
{
MovementGeneratorType movetype = c.GetMotionMaster().GetCurrentMovementGeneratorType();
if (movetype == MovementGeneratorType.Waypoint || movetype == MovementGeneratorType.Point || (c.IsAIEnabled && c.GetAI().IsEscorted()))
c.SetHomePosition(c.GetPosition());
}
public void AddThreat(Unit target, float amount, SpellInfo spell = null, bool ignoreModifiers = false, bool ignoreRedirects = false)
{
// step 1: we can shortcut if the spell has one of the NO_THREAT attrs set - nothing will happen
if (spell != null)
{
if (spell.HasAttribute(SpellAttr1.NoThreat))
return;
if (!_owner.IsEngaged() && spell.HasAttribute(SpellAttr3.NoInitialAggro))
return;
}
// while riding a vehicle, all threat goes to the vehicle, not the pilot
Unit vehicle = target.GetVehicleBase();
if (vehicle != null)
{
AddThreat(vehicle, amount, spell, ignoreModifiers, ignoreRedirects);
if (target.HasUnitTypeMask(UnitTypeMask.Accessory)) // accessories are fully treated as components of the parent and cannot have threat
return;
amount = 0.0f;
}
// If victim is personal spawn, redirect all aggro to summoner
if (target.IsPrivateObject() && (!GetOwner().IsPrivateObject() || !GetOwner().CheckPrivateObjectOwnerVisibility(target)))
{
Unit privateObjectOwner = Global.ObjAccessor.GetUnit(GetOwner(), target.GetPrivateObjectOwner());
if (privateObjectOwner != null)
{
AddThreat(privateObjectOwner, amount, spell, ignoreModifiers, ignoreRedirects);
amount = 0.0f;
}
}
// if we cannot actually have a threat list, we instead just set combat state and avoid creating threat refs altogether
if (!CanHaveThreatList())
{
CombatManager combatMgr = _owner.GetCombatManager();
if (!combatMgr.SetInCombatWith(target))
return;
// traverse redirects and put them in combat, too
foreach (var pair in target.GetThreatManager()._redirectInfo)
{
if (!combatMgr.IsInCombatWith(pair.Item1))
{
Unit redirTarget = Global.ObjAccessor.GetUnit(_owner, pair.Item1);
if (redirTarget != null)
combatMgr.SetInCombatWith(redirTarget);
}
}
return;
}
// apply threat modifiers to the amount
if (!ignoreModifiers)
amount = CalculateModifiedThreat(amount, target, spell);
// if we're increasing threat, send some/all of it to redirection targets instead if applicable
if (!ignoreRedirects && amount > 0.0f)
{
var redirInfo = target.GetThreatManager()._redirectInfo;
if (!redirInfo.Empty())
{
float origAmount = amount;
// intentional iteration by index - there's a nested AddThreat call further down that might cause AI calls which might modify redirect info through spells
for (var i = 0; i < redirInfo.Count; ++i)
{
var pair = redirInfo[i]; // (victim,pct)
Unit redirTarget;
var refe = _myThreatListEntries.LookupByKey(pair.Item1); // try to look it up in our threat list first (faster)
if (refe != null)
redirTarget = refe.GetVictim();
else
redirTarget = Global.ObjAccessor.GetUnit(_owner, pair.Item1);
if (redirTarget)
{
float amountRedirected = MathFunctions.CalculatePct(origAmount, pair.Item2);
AddThreat(redirTarget, amountRedirected, spell, true, true);
amount -= amountRedirected;
}
}
}
}
// ensure we're in combat (threat implies combat!)
if (!_owner.GetCombatManager().SetInCombatWith(target)) // if this returns false, we're not actually in combat, and thus cannot have threat!
return; // typical causes: bad scripts trying to add threat to GMs, dead targets etc
// ok, now we actually apply threat
// check if we already have an entry - if we do, just increase threat for that entry and we're done
var targetRefe = _myThreatListEntries.LookupByKey(target.GetGUID());
if (targetRefe != null)
{
targetRefe.AddThreat(amount);
return;
}
// ok, we're now in combat - create the threat list reference and push it to the respective managers
ThreatReference newRefe = new(this, target, amount);
PutThreatListRef(target.GetGUID(), newRefe);
target.GetThreatManager().PutThreatenedByMeRef(_owner.GetGUID(), newRefe);
Creature cOwner = _owner.ToCreature();
Cypher.Assert(cOwner != null); // if we got here the owner can have a threat list, and must be a creature!
if (!_ownerEngaged && (cOwner.HasReactState(ReactStates.Passive) || !newRefe.IsOffline()))
{
_ownerEngaged = true;
SaveCreatureHomePositionIfNeed(cOwner);
if (cOwner.IsAIEnabled)
cOwner.GetAI().JustEngagedWith(target);
}
}
void ScaleThreat(Unit target, float factor)
{
var refe = _myThreatListEntries.LookupByKey(target.GetGUID());
if (refe != null)
refe.ScaleThreat(Math.Max(factor, 0.0f));
}
public void MatchUnitThreatToHighestThreat(Unit target)
{
if (_sortedThreatList.Empty())
return;
int index = 0;
ThreatReference highest = _sortedThreatList[index];
if (!highest.IsOnline())
return;
if (highest.GetTauntState() != 0) // might need to skip this - new max could be one of the preceding elements (heap property) since there is only one taunt element
{
if ((++index) != _sortedThreatList.Count - 1)
{
ThreatReference a = _sortedThreatList[index];
if (a.IsOnline() && a.GetThreat() > highest.GetThreat())
highest = a;
if ((++index) != _sortedThreatList.Count)
{
ThreatReference aa = _sortedThreatList[index];
if (aa.IsOnline() && aa.GetThreat() > highest.GetThreat())
highest = aa;
}
}
}
AddThreat(target, highest.GetThreat() - GetThreat(target, true), null, true, true);
}
public void TauntUpdate()
{
var tauntEffects = _owner.GetAuraEffectsByType(AuraType.ModTaunt);
ThreatReference tauntRef = null;
// Only the last taunt effect applied by something still on our threat list is considered
foreach (var auraEffect in tauntEffects)
{
var refe = _myThreatListEntries.LookupByKey(auraEffect.GetCasterGUID());
if (refe == null)
continue;
if (!refe.IsOnline())
continue;
tauntRef = refe;
break;
}
foreach (var pair in _myThreatListEntries)
pair.Value.UpdateTauntState(pair.Value == tauntRef);
}
public void ResetAllThreat()
{
foreach (var pair in _myThreatListEntries)
pair.Value.SetThreat(0.0f);
}
public void ClearThreat(Unit target)
{
var refe = _myThreatListEntries.LookupByKey(target.GetGUID());
if (refe != null)
refe.ClearThreat();
}
public void ClearAllThreat()
{
_ownerEngaged = false;
if (_myThreatListEntries.Empty())
return;
SendClearAllThreatToClients();
do
_myThreatListEntries.First().Value.ClearThreat(false);
while (!_myThreatListEntries.Empty());
}
ThreatReference ReselectVictim()
{
ThreatReference oldVictimRef = _currentVictimRef;
if (oldVictimRef != null && !oldVictimRef.IsAvailable())
oldVictimRef = null;
// in 99% of cases - we won't need to actually look at anything beyond the first element
ThreatReference highest = _sortedThreatList.Max();
// if the highest reference is offline, the entire list is offline, and we indicate this
if (!highest.IsAvailable())
return null;
// if we have no old victim, or old victim is still highest, then highest is our target and we're done
if (oldVictimRef == null || highest == oldVictimRef)
return highest;
// if highest threat doesn't break 110% of old victim, nothing below it is going to do so either; new victim = old victim and done
if (!CompareReferencesLT(oldVictimRef, highest, 1.1f))
return oldVictimRef;
// if highest threat breaks 130%, it's our new target regardless of range (and we're done)
if (CompareReferencesLT(oldVictimRef, highest, 1.3f))
return highest;
// if it doesn't break 130%, we need to check if it's melee - if yes, it breaks 110% (we checked earlier) and is our new target
if (_owner.IsWithinMeleeRange(highest.GetVictim()))
return highest;
// If we get here, highest threat is ranged, but below 130% of current - there might be a melee that breaks 110% below us somewhere, so now we need to actually look at the next highest element
// luckily, this is a heap, so getting the next highest element is O(log n), and we're just gonna do that repeatedly until we've seen enough targets (or find a target)
foreach (var next in _sortedThreatList)
{
// if we've found current victim, we're done (nothing above is higher, and nothing below can be higher)
if (next == oldVictimRef)
return next;
// if next isn't above 110% threat, then nothing below it can be either - we're done, old victim stays
if (!CompareReferencesLT(oldVictimRef, next, 1.1f))
return oldVictimRef;
// if next is melee, he's above 110% and our new victim
if (_owner.IsWithinMeleeRange(next.GetVictim()))
return next;
// otherwise the next highest target may still be a melee above 110% and we need to look further
}
// we should have found the old victim at some point in the loop above, so execution should never get to this point
Cypher.Assert(false, "Current victim not found in sorted threat list even though it has a reference - manager desync!");
return null;
}
// returns true if a is LOWER on the threat list than b
public static bool CompareReferencesLT(ThreatReference a, ThreatReference b, float aWeight)
{
if (a.GetOnlineState() != b.GetOnlineState()) // online state precedence (ONLINE > SUPPRESSED > OFFLINE)
return a.GetOnlineState() < b.GetOnlineState();
if (a.GetTauntState() != b.GetTauntState()) // taunt state precedence (TAUNT > NONE > DETAUNT)
return a.GetTauntState() < b.GetTauntState();
return (a.GetThreat() * aWeight < b.GetThreat());
}
public static float CalculateModifiedThreat(float threat, Unit victim, SpellInfo spell)
{
// modifiers by spell
if (spell != null)
{
SpellThreatEntry threatEntry = Global.SpellMgr.GetSpellThreatEntry(spell.Id);
if (threatEntry != null)
if (threatEntry.pctMod != 1.0f) // flat/AP modifiers handled in Spell::HandleThreatSpells
threat *= threatEntry.pctMod;
Player modOwner = victim.GetSpellModOwner();
if (modOwner != null)
modOwner.ApplySpellMod(spell, SpellModOp.Hate, ref threat);
}
// modifiers by effect school
ThreatManager victimMgr = victim.GetThreatManager();
SpellSchoolMask mask = spell != null ? spell.GetSchoolMask() : SpellSchoolMask.Normal;
switch (mask)
{
case SpellSchoolMask.Normal:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Normal];
break;
case SpellSchoolMask.Holy:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Holy];
break;
case SpellSchoolMask.Fire:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Fire];
break;
case SpellSchoolMask.Nature:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Nature];
break;
case SpellSchoolMask.Frost:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Frost];
break;
case SpellSchoolMask.Shadow:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Shadow];
break;
case SpellSchoolMask.Arcane:
threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Arcane];
break;
default:
{
if (victimMgr._multiSchoolModifiers.TryGetValue(mask, out float value))
{
threat *= value;
break;
}
float mod = victim.GetTotalAuraMultiplierByMiscMask(AuraType.ModThreat, (uint)mask);
victimMgr._multiSchoolModifiers[mask] = mod;
threat *= mod;
break;
}
}
return threat;
}
void SendClearAllThreatToClients()
{
ThreatClear threatClear = new();
threatClear.UnitGUID = _owner.GetGUID();
_owner.SendMessageToSet(threatClear, false);
}
public void SendThreatListToClients()
{
ThreatUpdate threatUpdate = new();
threatUpdate.UnitGUID = _owner.GetGUID();
foreach (ThreatReference refe in _sortedThreatList)
{
if (!refe.IsAvailable()) // @todo check if suppressed threat should get sent for bubble/iceblock/hop etc
continue;
ThreatInfo threatInfo = new();
threatInfo.UnitGUID = refe.GetVictim().GetGUID();
threatInfo.Threat = (long)(refe.GetThreat() * 100);
threatUpdate.ThreatList.Add(threatInfo);
}
_owner.SendMessageToSet(threatUpdate, false);
}
public void ForwardThreatForAssistingMe(Unit assistant, float baseAmount, SpellInfo spell = null, bool ignoreModifiers = false)
{
if (spell != null && spell.HasAttribute(SpellAttr1.NoThreat)) // shortcut, none of the calls would do anything
return;
foreach (var pair in _threatenedByMe)
pair.Value.GetOwner().GetThreatManager().AddThreat(assistant, baseAmount, spell, ignoreModifiers);
}
public void RemoveMeFromThreatLists()
{
while (!_threatenedByMe.Empty())
_threatenedByMe.First().Value.ClearThreat();
}
public void UpdateMyTempModifiers()
{
int mod = 0;
foreach (AuraEffect eff in _owner.GetAuraEffectsByType(AuraType.ModTotalThreat))
mod += eff.GetAmount();
foreach (var pair in _threatenedByMe)
{
pair.Value._tempModifier = mod;
pair.Value.ListNotifyChanged();
}
}
public void UpdateMySpellSchoolModifiers()
{
for (byte i = 0; i < (int)SpellSchools.Max; ++i)
_singleSchoolModifiers[i] = _owner.GetTotalAuraMultiplierByMiscMask(AuraType.ModThreat, 1u << i);
_multiSchoolModifiers.Clear();
}
public void RegisterRedirectThreat(uint spellId, ObjectGuid victim, uint pct)
{
_redirectRegistry[spellId][victim] = pct;
UpdateRedirectInfo();
}
public void UnregisterRedirectThreat(uint spellId)
{
if (_redirectRegistry.Remove(spellId))
UpdateRedirectInfo();
}
void UnregisterRedirectThreat(uint spellId, ObjectGuid victim)
{
var victimMap = _redirectRegistry.LookupByKey(spellId);
if (victimMap == null)
return;
if (victimMap.Remove(victim))
UpdateRedirectInfo();
}
public void SendRemoveToClients(Unit victim)
{
ThreatRemove threatRemove = new();
threatRemove.UnitGUID = _owner.GetGUID();
threatRemove.AboutGUID = victim.GetGUID();
_owner.SendMessageToSet(threatRemove, false);
}
void SendNewVictimToClients(ThreatReference victimRef)
{
HighestThreatUpdate highestThreatUpdate = new();
highestThreatUpdate.UnitGUID = _owner.GetGUID();
highestThreatUpdate.HighestThreatGUID = victimRef.GetVictim().GetGUID();
foreach (ThreatReference refe in _sortedThreatList)
{
if (!refe.IsAvailable())
continue;
ThreatInfo threatInfo = new();
threatInfo.UnitGUID = refe.GetVictim().GetGUID();
threatInfo.Threat = (long)(refe.GetThreat() * 100);
highestThreatUpdate.ThreatList.Add(threatInfo);
}
_owner.SendMessageToSet(highestThreatUpdate, false);
}
void PutThreatListRef(ObjectGuid guid, ThreatReference refe)
{
Cypher.Assert(!_myThreatListEntries.ContainsKey(guid), $"Duplicate threat reference being inserted on {_owner.GetGUID()} for {guid}!");
_myThreatListEntries[guid] = refe;
_sortedThreatList.Add(refe);
_sortedThreatList.Sort();
}
public void PurgeThreatListRef(ObjectGuid guid, bool sendRemove)
{
var refe = _myThreatListEntries.LookupByKey(guid);
if (refe == null)
return;
_myThreatListEntries.Remove(guid);
if (_currentVictimRef == refe)
_currentVictimRef = null;
_sortedThreatList.Remove(refe);
_sortedThreatList.Sort();
if (sendRemove && refe.IsOnline())
SendRemoveToClients(refe.GetVictim());
}
void PutThreatenedByMeRef(ObjectGuid guid, ThreatReference refe)
{
Cypher.Assert(!_threatenedByMe.ContainsKey(guid), $"Duplicate threatened-by-me reference being inserted on {_owner.GetGUID()} for {guid}!");
_threatenedByMe[guid] = refe;
}
public void PurgeThreatenedByMeRef(ObjectGuid guid)
{
_threatenedByMe.Remove(guid);
}
void UpdateRedirectInfo()
{
_redirectInfo.Clear();
uint totalPct = 0;
foreach (var pair in _redirectRegistry) // (spellid, victim . pct)
{
foreach (var victimPair in pair.Value) // (victim,pct)
{
uint thisPct = Math.Min(100 - totalPct, victimPair.Value);
if (thisPct > 0)
{
_redirectInfo.Add(Tuple.Create(victimPair.Key, thisPct));
totalPct += thisPct;
Cypher.Assert(totalPct <= 100);
if (totalPct == 100)
return;
}
}
}
}
// never nullptr
Unit GetOwner() { return _owner; }
// can our owner have a threat list?
// identical to ThreatManager::CanHaveThreatList(GetOwner())
public bool CanHaveThreatList() { return _ownerCanHaveThreatList; }
public bool IsEngaged() { return _ownerEngaged; }
public int GetThreatListSize() { return _sortedThreatList.Count; }
// fastest of the three threat list getters - gets the threat list in "arbitrary" order
public List<ThreatReference> GetSortedThreatList() { return _sortedThreatList; }
public void ListNotifyChanged()
{
_sortedThreatList.Sort();
}
public Dictionary<ObjectGuid, ThreatReference> GetThreatenedByMeList() { return _threatenedByMe; }
// Modify target's threat by +percent%
public void ModifyThreatByPercent(Unit target, int percent)
{
if (percent != 0)
ScaleThreat(target, 0.01f * (100f + percent));
}
// Resets the specified unit's threat to zero
public void ResetThreat(Unit target) { ScaleThreat(target, 0.0f); }
}
public enum TauntState
{
Detaunt = -1,
None = 0,
Taunt = 1
}
public enum OnlineState
{
Online = 2,
Suppressed = 1,
Offline = 0
}
public class ThreatReference : IComparable<ThreatReference>
{
Unit _owner;
ThreatManager _mgr;
Unit _victim;
float _baseAmount;
public int _tempModifier; // Temporary effects (auras with SPELL_AURA_MOD_TOTAL_THREAT) - set from victim's threatmanager in ThreatManager::UpdateMyTempModifiers
OnlineState _online;
TauntState _taunted;
public ThreatReference(ThreatManager mgr, Unit victim, float amount)
{
_owner = mgr._owner;
_mgr = mgr;
_victim = victim;
_baseAmount = amount;
_online = SelectOnlineState();
}
public void AddThreat(float amount)
{
if (amount == 0.0f)
return;
_baseAmount = Math.Max(_baseAmount + amount, 0.0f);
ListNotifyChanged();
}
public void ScaleThreat(float factor)
{
if (factor == 1.0f)
return;
_baseAmount *= factor;
ListNotifyChanged();
}
public void UpdateOnlineState()
{
OnlineState onlineState = SelectOnlineState();
if (onlineState == _online)
return;
_online = onlineState;
ListNotifyChanged();
if (!IsAvailable())
_owner.GetThreatManager().SendRemoveToClients(_victim);
}
public static bool FlagsAllowFighting(Unit a, Unit b)
{
if (a.IsCreature() && a.ToCreature().IsTrigger())
return false;
if (a.HasUnitFlag(UnitFlags.PvpAttackable))
{
if (b.HasUnitFlag(UnitFlags.ImmuneToPc))
return false;
}
else
{
if (b.HasUnitFlag(UnitFlags.ImmuneToNpc))
return false;
}
return true;
}
OnlineState SelectOnlineState()
{
// first, check all offline conditions
if (!_owner.CanSeeOrDetect(_victim)) // not in map/phase, or stealth/invis
return OnlineState.Offline;
if (_victim.HasUnitState(UnitState.Died)) // feign death
return OnlineState.Offline;
if (!FlagsAllowFighting(_owner, _victim) || !FlagsAllowFighting(_victim, _owner))
return OnlineState.Offline;
if (_owner.IsAIEnabled && !_owner.GetAI().CanAIAttack(_victim))
return OnlineState.Offline;
// next, check suppression (immunity to chosen melee attack school)
if (_victim.IsImmunedToDamage(_owner.GetMeleeDamageSchoolMask()))
return OnlineState.Suppressed;
// or any form of CC that will break on damage - disorient, polymorph, blind etc
if (_victim.HasBreakableByDamageCrowdControlAura())
return OnlineState.Suppressed;
// no suppression - we're online
return OnlineState.Online;
}
public void UpdateTauntState(bool victimIsTaunting)
{
if (victimIsTaunting)
{
_taunted = TauntState.Taunt;
ListNotifyChanged();
return;
}
// Check for SPELL_AURA_MOD_DETAUNT (applied from owner to victim)
foreach (AuraEffect eff in _victim.GetAuraEffectsByType(AuraType.ModDetaunt))
{
if (eff.GetCasterGUID() == _owner.GetGUID())
{
_taunted = TauntState.Detaunt;
ListNotifyChanged();
return;
}
}
_taunted = TauntState.None;
ListNotifyChanged();
}
public void ClearThreat(bool sendRemove = true)
{
_owner.GetThreatManager().PurgeThreatListRef(_victim.GetGUID(), sendRemove);
_victim.GetThreatManager().PurgeThreatenedByMeRef(_owner.GetGUID());
}
public Unit GetOwner() { return _owner; }
public Unit GetVictim() { return _victim; }
public float GetThreat() { return Math.Max(_baseAmount + (float)_tempModifier, 0.0f); }
public OnlineState GetOnlineState() { return _online; }
public bool IsOnline() { return (_online >= OnlineState.Online); }
public bool IsAvailable() { return (_online > OnlineState.Offline); }
public bool IsOffline() { return (_online <= OnlineState.Offline); }
public TauntState GetTauntState() { return _taunted; }
public bool IsTaunting() { return _taunted == TauntState.Taunt; }
public bool IsDetaunted() { return _taunted == TauntState.Detaunt; }
public void SetThreat(float amount)
{
_baseAmount = amount;
ListNotifyChanged();
}
public void ModifyThreatByPercent(int percent)
{
if (percent != 0)
ScaleThreat(0.01f * (100f + percent)); }
public void ListNotifyChanged() { _mgr.ListNotifyChanged(); }
public int CompareTo(ThreatReference other)
{
return ThreatManager.CompareReferencesLT(this, other, 1.0f) ? 1 : -1;
}
}
}