From 9851142796b1dbc84974d092b160e8b1f6c7b78f Mon Sep 17 00:00:00 2001 From: hondacrx Date: Tue, 18 May 2021 12:25:40 -0400 Subject: [PATCH] Core: Combat/threat system rewrite Port From (https://github.com/TrinityCore/TrinityCore/commit/34c7810fe507eca1b8b9389630db5d5d26d92e77) --- Source/Game/AI/CoreAI/CreatureAI.cs | 74 +- Source/Game/AI/CoreAI/PetAI.cs | 13 +- Source/Game/AI/CoreAI/UnitAI.cs | 8 +- Source/Game/AI/ScriptedAI/ScriptedAI.cs | 8 +- Source/Game/AI/SmartScripts/SmartScript.cs | 15 +- Source/Game/BattleGrounds/BattleGround.cs | 1 - Source/Game/Chat/Commands/DebugCommands.cs | 78 +- Source/Game/Chat/Commands/MiscCommands.cs | 1 - Source/Game/Combat/CombatManager.cs | 415 ++++ Source/Game/Combat/Events.cs | 135 -- Source/Game/Combat/HostileRegManager.cs | 398 --- Source/Game/Combat/ThreatManager.cs | 1259 ++++++---- Source/Game/Entities/Creature/Creature.cs | 259 +- Source/Game/Entities/Object/WorldObject.cs | 5 + Source/Game/Entities/Pet.cs | 2 + Source/Game/Entities/Player/Player.Combat.cs | 14 + Source/Game/Entities/Player/Player.PvP.cs | 2 +- Source/Game/Entities/Player/Player.cs | 26 +- Source/Game/Entities/Unit/Unit.Combat.cs | 2126 ++--------------- Source/Game/Entities/Unit/Unit.Fields.cs | 28 +- Source/Game/Entities/Unit/Unit.Movement.cs | 5 +- Source/Game/Entities/Unit/Unit.Pets.cs | 31 +- Source/Game/Entities/Unit/Unit.Spells.cs | 9 +- Source/Game/Entities/Unit/Unit.cs | 1633 ++++++++++++- Source/Game/Handlers/PetHandler.cs | 3 - Source/Game/Maps/Map.cs | 21 +- Source/Game/Maps/ObjectGridLoader.cs | 2 +- .../Generators/FlightPathMovementGenerator.cs | 3 +- Source/Game/Server/WorldSession.cs | 2 +- Source/Game/Spells/Auras/AuraEffect.cs | 84 +- Source/Game/Spells/Spell.cs | 9 +- Source/Game/Spells/SpellEffects.cs | 54 +- Source/Game/Spells/SpellInfo.cs | 4 +- Source/Scripts/Pets/Mage.cs | 45 +- Source/Scripts/Spells/Generic.cs | 2 +- Source/Scripts/Spells/Hunter.cs | 11 +- Source/Scripts/World/NpcSpecial.cs | 14 +- 37 files changed, 3454 insertions(+), 3345 deletions(-) create mode 100644 Source/Game/Combat/CombatManager.cs delete mode 100644 Source/Game/Combat/Events.cs delete mode 100644 Source/Game/Combat/HostileRegManager.cs diff --git a/Source/Game/AI/CoreAI/CreatureAI.cs b/Source/Game/AI/CoreAI/CreatureAI.cs index 19f08cfbe..44d1a57fd 100644 --- a/Source/Game/AI/CoreAI/CreatureAI.cs +++ b/Source/Game/AI/CoreAI/CreatureAI.cs @@ -62,41 +62,42 @@ namespace Game.AI if (!creature) creature = me; - if (!creature.CanHaveThreatList()) - return; - Map map = creature.GetMap(); - if (!map.IsDungeon()) //use IsDungeon instead of Instanceable, in case Battlegrounds will be instantiated + if (creature.CanHaveThreatList()) { - Log.outError(LogFilter.Server, "DoZoneInCombat call for map that isn't an instance (creature entry = {0})", creature.IsTypeId(TypeId.Unit) ? creature.ToCreature().GetEntry() : 0); - return; - } - - if (!creature.HasReactState(ReactStates.Passive) && creature.GetVictim() == null) - { - Unit nearTarget = creature.SelectNearestTarget(maxRangeToNearestTarget); - if (nearTarget != null) - creature.GetAI().AttackStart(nearTarget); - else if (creature.IsSummon()) + if (!map.IsDungeon()) //use IsDungeon instead of Instanceable, in case Battlegrounds will be instantiated { - Unit summoner = creature.ToTempSummon().GetSummoner(); - if (summoner != null) + Log.outError(LogFilter.Server, "DoZoneInCombat call for map that isn't an instance (creature entry = {0})", creature.IsTypeId(TypeId.Unit) ? creature.ToCreature().GetEntry() : 0); + return; + } + + if (!creature.HasReactState(ReactStates.Passive) && creature.GetVictim() == null) + { + Unit nearTarget = creature.SelectNearestTarget(maxRangeToNearestTarget); + if (nearTarget != null) + creature.GetAI().AttackStart(nearTarget); + else if (creature.IsSummon()) { - Unit target = summoner.GetAttackerForHelper(); - if (target == null && summoner.CanHaveThreatList() && !summoner.GetThreatManager().IsThreatListEmpty()) - target = summoner.GetThreatManager().GetHostilTarget(); - if (target != null && (creature.IsFriendlyTo(summoner) || creature.IsHostileTo(target))) - creature.GetAI().AttackStart(target); + Unit summoner = creature.ToTempSummon().GetSummoner(); + if (summoner != null) + { + if (creature.IsFriendlyTo(summoner)) + { + Unit target = summoner.GetAttackerForHelper(); + if (target != null && creature.IsHostileTo(target)) + creature.GetAI().AttackStart(target); + } + } } } - } - // Intended duplicated check, the code above this should select a victim - // If it can't find a suitable attack target then we should error out. - if (!creature.HasReactState(ReactStates.Passive) && creature.GetVictim() == null) - { - Log.outError(LogFilter.Server, "DoZoneInCombat called for creature that has empty threat list (creature entry = {0})", creature.GetEntry()); - return; + // Intended duplicated check, the code above this should select a victim + // If it can't find a suitable attack target then we should error out. + if (!creature.HasReactState(ReactStates.Passive) && creature.GetVictim() == null) + { + Log.outError(LogFilter.Server, "DoZoneInCombat called for creature that has empty threat list (creature entry = {0})", creature.GetEntry()); + return; + } } var playerList = map.GetPlayers(); @@ -104,17 +105,8 @@ namespace Game.AI return; foreach (var player in playerList) - { - if (player.IsGameMaster()) - continue; - - if (player.IsAlive()) - { - creature.SetInCombatWith(player); - player.SetInCombatWith(creature); - creature.GetThreatManager().AddThreat(player, 0.0f, null, true, true); - } - } + if (player != null && player.IsAlive()) + creature.EngageWithTarget(player); } public virtual void MoveInLineOfSight_Safe(Unit who) @@ -247,11 +239,13 @@ namespace Game.AI return me.GetVictim() != null; } - else if (me.GetThreatManager().IsThreatListEmpty()) + else if (me.GetThreatManager().IsThreatListEmpty(true)) { EnterEvadeMode(EvadeReason.NoHostiles); return false; } + else + me.AttackStop(); return true; } diff --git a/Source/Game/AI/CoreAI/PetAI.cs b/Source/Game/AI/CoreAI/PetAI.cs index 500ad0265..c32fa0d2d 100644 --- a/Source/Game/AI/CoreAI/PetAI.cs +++ b/Source/Game/AI/CoreAI/PetAI.cs @@ -56,8 +56,6 @@ namespace Game.AI me.GetMotionMaster().Clear(); me.GetMotionMaster().MoveIdle(); me.CombatStop(); - me.GetHostileRefManager().DeleteReferences(); - return; } @@ -454,9 +452,9 @@ namespace Game.AI } } - me.ClearInPetCombat(); + me.RemoveUnitFlag(UnitFlags.PetInCombat); // on player pets, this flag indicates that we're actively going after a target - we're returning, so remove it } - + void DoAttack(Unit target, bool chase) { // Handles attack with or without chase and also resets flags @@ -464,12 +462,7 @@ namespace Game.AI if (me.Attack(target, true)) { - // properly fix fake combat after pet is sent to attack - Unit owner = me.GetOwner(); - if (owner != null) - owner.AddUnitFlag(UnitFlags.PetInCombat); - - me.AddUnitFlag(UnitFlags.PetInCombat); + me.AddUnitFlag(UnitFlags.PetInCombat); // on player pets, this flag indicates we're actively going after a target - that's what we're doing, so set it // Play sound to let the player know the pet is attacking something it picked on its own if (me.HasReactState(ReactStates.Aggressive) && !me.GetCharmInfo().IsCommandAttack()) diff --git a/Source/Game/AI/CoreAI/UnitAI.cs b/Source/Game/AI/CoreAI/UnitAI.cs index 22b3303a5..cd78ff941 100644 --- a/Source/Game/AI/CoreAI/UnitAI.cs +++ b/Source/Game/AI/CoreAI/UnitAI.cs @@ -188,12 +188,12 @@ namespace Game.AI if (targetType == SelectAggroTarget.MaxDistance || targetType == SelectAggroTarget.MinDistance) { - foreach (HostileReference refe in mgr.GetThreatList()) + foreach (ThreatReference refe in mgr.GetSortedThreatList()) { if (!refe.IsOnline()) continue; - targetList.Add(refe.GetTarget()); + targetList.Add(refe.GetVictim()); } } else @@ -202,12 +202,12 @@ namespace Game.AI if (currentVictim != null) targetList.Add(currentVictim); - foreach (HostileReference refe in mgr.GetThreatList()) + foreach (ThreatReference refe in mgr.GetSortedThreatList()) { if (!refe.IsOnline()) continue; - Unit thisTarget = refe.GetTarget(); + Unit thisTarget = refe.GetVictim(); if (thisTarget != currentVictim) targetList.Add(thisTarget); } diff --git a/Source/Game/AI/ScriptedAI/ScriptedAI.cs b/Source/Game/AI/ScriptedAI/ScriptedAI.cs index 470f9f7c1..cce585c6d 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedAI.cs @@ -526,12 +526,10 @@ namespace Game.AI float x, y, z; me.GetPosition(out x, out y, out z); - var threatList = me.GetThreatManager().GetThreatList(); - foreach (var refe in threatList) + foreach (var pair in me.GetCombatManager().GetPvECombatRefs()) { - Unit target = refe.GetTarget(); - if (target) - if (target.IsTypeId(TypeId.Player) && !CheckBoundary(target)) + Unit target = pair.Value.GetOther(me); + if (target.IsControlledByPlayer() && !CheckBoundary(target)) target.NearTeleportTo(x, y, z, 0); } } diff --git a/Source/Game/AI/SmartScripts/SmartScript.cs b/Source/Game/AI/SmartScripts/SmartScript.cs index 07a887d5c..333ecbdd9 100644 --- a/Source/Game/AI/SmartScripts/SmartScript.cs +++ b/Source/Game/AI/SmartScripts/SmartScript.cs @@ -370,10 +370,10 @@ namespace Game.AI if (_me == null) break; - foreach (var refe in _me.GetThreatManager().GetThreatList()) + foreach (var refe in _me.GetThreatManager().GetModifiableThreatList()) { - refe.AddThreatPercent(Math.Max(-100, (int)(e.Action.threatPCT.threatINC - e.Action.threatPCT.threatDEC))); - Log.outDebug(LogFilter.ScriptsAi, $"SmartScript.ProcessAction: SMART_ACTION_THREAT_ALL_PCT: Creature {_me.GetGUID()} modify threat for {refe.GetTarget().GetGUID()}, value {e.Action.threatPCT.threatINC - e.Action.threatPCT.threatDEC}"); + refe.ModifyThreatByPercent(Math.Max(-100, (int)(e.Action.threatPCT.threatINC - e.Action.threatPCT.threatDEC))); + Log.outDebug(LogFilter.ScriptsAi, $"SmartScript.ProcessAction: SMART_ACTION_THREAT_ALL_PCT: Creature {_me.GetGUID()} modify threat for {refe.GetVictim().GetGUID()}, value {e.Action.threatPCT.threatINC - e.Action.threatPCT.threatDEC}"); } break; } @@ -2245,6 +2245,9 @@ namespace Game.AI } case SmartActions.AddThreat: { + if (!_me.CanHaveThreatList()) + break; + foreach (var target in targets) if (IsUnit(target)) _me.GetThreatManager().AddThreat(target.ToUnit(), (float)(e.Action.threatPCT.threatINC - (float)e.Action.threatPCT.threatDEC), null, true, true); @@ -2847,9 +2850,9 @@ namespace Game.AI { if (_me != null && _me.CanHaveThreatList()) { - foreach (var refe in _me.GetThreatManager().GetThreatList()) - if (e.Target.hostilRandom.maxDist == 0 || _me.IsWithinCombatRange(refe.GetTarget(), e.Target.hostilRandom.maxDist)) - targets.Add(refe.GetTarget()); + foreach (var refe in _me.GetThreatManager().GetSortedThreatList()) + if (e.Target.hostilRandom.maxDist == 0 || _me.IsWithinCombatRange(refe.GetVictim(), e.Target.hostilRandom.maxDist)) + targets.Add(refe.GetVictim()); } break; } diff --git a/Source/Game/BattleGrounds/BattleGround.cs b/Source/Game/BattleGrounds/BattleGround.cs index f78e917e8..0a0c56345 100644 --- a/Source/Game/BattleGrounds/BattleGround.cs +++ b/Source/Game/BattleGrounds/BattleGround.cs @@ -727,7 +727,6 @@ namespace Game.BattleGrounds { //needed cause else in av some creatures will kill the players at the end player.CombatStop(); - player.GetHostileRefManager().DeleteReferences(); } // remove temporary currency bonus auras before rewarding player diff --git a/Source/Game/Chat/Commands/DebugCommands.cs b/Source/Game/Chat/Commands/DebugCommands.cs index 1987a6e61..c01bc50e1 100644 --- a/Source/Game/Chat/Commands/DebugCommands.cs +++ b/Source/Game/Chat/Commands/DebugCommands.cs @@ -441,20 +441,19 @@ namespace Game.Chat Unit target = handler.GetSelectedUnit(); if (!target) target = handler.GetSession().GetPlayer(); - HostileReference refe = target.GetHostileRefManager().GetFirst(); - uint count = 0; - handler.SendSysMessage("Hostil reference list of {0} (guid {1})", target.GetName(), target.GetGUID().ToString()); - while (refe != null) + + handler.SendSysMessage($"Combat refs: (Combat state: {target.IsInCombat()} | Manager state: {target.GetCombatManager().HasCombat()})"); + foreach (var refe in target.GetCombatManager().GetPvPCombatRefs()) { - Unit unit = refe.GetSource().GetOwner(); - if (unit) - { - ++count; - handler.SendSysMessage(" {0}. {1} ({2}, SpawnId: {3}) - threat {4}", count, unit.GetName(), unit.GetGUID().ToString(), unit.IsTypeId(TypeId.Unit) ? unit.ToCreature().GetSpawnId() : 0, refe.GetThreat()); - } - refe = refe.Next(); + Unit unit = refe.Value.GetOther(target); + handler.SendSysMessage($"[PvP] {unit.GetName()} (SpawnID {(unit.IsCreature() ? unit.ToCreature().GetSpawnId() : 0)})"); } - handler.SendSysMessage("End of hostil reference list."); + foreach (var refe in target.GetCombatManager().GetPvECombatRefs()) + { + Unit unit = refe.Value.GetOther(target); + handler.SendSysMessage($"[PvE] {unit.GetName()} (SpawnID {(unit.IsCreature() ? unit.ToCreature().GetSpawnId() : 0)})"); + } + return true; } @@ -788,22 +787,51 @@ namespace Game.Chat [Command("threat", RBACPermissions.CommandDebugThreat)] static bool HandleDebugThreatListCommand(StringArguments args, CommandHandler handler) { - Creature target = handler.GetSelectedCreature(); - if (!target || target.IsTotem() || target.IsPet()) - return false; + Unit target = handler.GetSelectedUnit(); + if (target == null) + target = handler.GetSession().GetPlayer(); - var threatList = target.GetThreatManager().GetThreatList(); - uint count = 0; - handler.SendSysMessage("Threat list of {0} (guid {1})", target.GetName(), target.GetGUID().ToString()); - foreach (var refe in threatList) + ThreatManager mgr = target.GetThreatManager(); + if (!target.IsAlive()) { - Unit unit = refe.GetTarget(); - if (!unit) - continue; - ++count; - handler.SendSysMessage(" {0}. {1} (guid {2}) - threat {3}", count, unit.GetName(), unit.GetGUID().ToString(), refe.GetThreat()); + handler.SendSysMessage($"{target.GetName()} ({target.GetGUID()}) is not alive."); + return true; } - handler.SendSysMessage("End of threat list."); + if (!target.CanHaveThreatList()) + handler.SendSysMessage($"{target.GetName()} ({target.GetGUID()}) cannot have a threat list."); + + uint count = 0; + var threatenedByMe = target.GetThreatManager().GetThreatenedByMeList(); + if (threatenedByMe.Empty()) + handler.SendSysMessage($"{target.GetName()} ({target.GetGUID()}) does not threaten any units."); + else + { + handler.SendSysMessage($"List of units threatened by {target.GetName()} ({target.GetGUID()})"); + foreach (var pair in threatenedByMe) + { + Unit unit = pair.Value.GetOwner(); + handler.SendSysMessage($" {++count}. {unit.GetName()} ({unit.GetGUID()}, SpawnID {(unit.IsCreature() ? unit.ToCreature().GetSpawnId() : 0)}) - threat {pair.Value.GetThreat()}"); + } + handler.SendSysMessage("End of threatened-by-me list."); + } + + if (!mgr.CanHaveThreatList()) + return true; + + if (mgr.IsEngaged()) + { + count = 0; + handler.SendSysMessage($"Threat list of {target.GetName()} ({target.GetGUID()}, SpawnID {(target.IsCreature() ? target.ToCreature().GetSpawnId() : 0)})"); + foreach (ThreatReference refe in mgr.GetSortedThreatList()) + { + Unit unit = refe.GetVictim(); + handler.SendSysMessage($" {++count}. {unit.GetName()} ({unit.GetGUID()}) - threat {refe.GetThreat()}[{refe.GetTauntState()}][{refe.GetOnlineState()}]"); + } + handler.SendSysMessage("End of threat list."); + } + else + handler.SendSysMessage($"{target.GetName()} ({target.GetGUID()}, SpawnID {(target.IsCreature() ? target.ToCreature().GetSpawnId() : 0)}) is not currently engaged."); + return true; } diff --git a/Source/Game/Chat/Commands/MiscCommands.cs b/Source/Game/Chat/Commands/MiscCommands.cs index 114710123..862bfbf84 100644 --- a/Source/Game/Chat/Commands/MiscCommands.cs +++ b/Source/Game/Chat/Commands/MiscCommands.cs @@ -446,7 +446,6 @@ namespace Game.Chat return false; target.CombatStop(); - target.GetHostileRefManager().DeleteReferences(); return true; } diff --git a/Source/Game/Combat/CombatManager.cs b/Source/Game/Combat/CombatManager.cs new file mode 100644 index 000000000..13d5e7afa --- /dev/null +++ b/Source/Game/Combat/CombatManager.cs @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2012-2020 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 . + */ + +using Framework.Constants; +using Game.Entities; +using System.Collections.Generic; +using System.Linq; + +namespace Game.Combat +{ + public class CombatManager + { + Unit _owner; + Dictionary _pveRefs = new(); + Dictionary _pvpRefs = new(); + + public CombatManager(Unit owner) + { + _owner = owner; + } + + public static bool CanBeginCombat(Unit a, Unit b) + { + // Checks combat validity before initial reference creation. + // For the combat to be valid... + // ...the two units need to be different + if (a == b) + return false; + // ...the two units need to be in the world + if (!a.IsInWorld || !b.IsInWorld) + return false; + // ...the two units need to both be alive + if (!a.IsAlive() || !b.IsAlive()) + return false; + // ...the two units need to be on the same map + if (a.GetMap() != b.GetMap()) + return false; + // ...the two units need to be in the same phase + if (!WorldObject.InSamePhase(a, b)) + return false; + if (a.HasUnitState(UnitState.Evade) || b.HasUnitState(UnitState.Evade)) + return false; + if (a.HasUnitState(UnitState.InFlight) || b.HasUnitState(UnitState.InFlight)) + return false; + if (a.IsControlledByPlayer() || b.IsControlledByPlayer()) + { + // PvSomething, only block friendly fire + if (a.IsFriendlyTo(b) || b.IsFriendlyTo(a)) + return false; + } + else + { + // CvC, need hostile reaction to start a fight + if (!a.IsHostileTo(b) && !b.IsHostileTo(a)) + return false; + } + Player playerA = a.GetCharmerOrOwnerPlayerOrPlayerItself(); + Player playerB = b.GetCharmerOrOwnerPlayerOrPlayerItself(); + // ...neither of the two units must be (owned by) a player with .gm on + if ((playerA && playerA.IsGameMaster()) || (playerB && playerB.IsGameMaster())) + return false; + return true; + } + + public void Update(uint tdiff) + { + foreach(var pair in _pvpRefs.ToList()) + { + PvPCombatReference refe = pair.Value; + if (refe.first == _owner && !refe.Update(tdiff)) // only update if we're the first unit involved (otherwise double decrement) + { + _pvpRefs.Remove(pair.Key); + refe.EndCombat(); // this will remove it from the other side + } + } + } + + public bool HasPvPCombat() + { + foreach (var pair in _pvpRefs) + if (!pair.Value.IsSuppressedFor(_owner)) + return true; + + return false; + } + + Unit GetAnyTarget() + { + if (!_pveRefs.Empty()) + return _pveRefs.First().Value.GetOther(_owner); + + foreach (var pair in _pvpRefs) + if (!pair.Value.IsSuppressedFor(_owner)) + return pair.Value.GetOther(_owner); + + return null; + } + + public bool SetInCombatWith(Unit who) + { + // Are we already in combat? If yes, refresh pvp combat + var pvpRefe = _pvpRefs.LookupByKey(who.GetGUID()); + if (pvpRefe != null) + { + pvpRefe.Refresh(); + return true; + } + else if (_pveRefs.ContainsKey(who.GetGUID())) + return true; + + // Otherwise, check validity... + if (!CombatManager.CanBeginCombat(_owner, who)) + return false; + + // ...then create new reference + CombatReference refe; + if (_owner.IsControlledByPlayer() && who.IsControlledByPlayer()) + refe = new PvPCombatReference(_owner, who); + else + refe = new CombatReference(_owner, who); + + // ...and insert it into both managers + PutReference(who.GetGUID(), refe); + who.GetCombatManager().PutReference(_owner.GetGUID(), refe); + + // now, sequencing is important - first we update the combat state, which will set both units in combat and do non-AI combat start stuff + bool needSelfAI = UpdateOwnerCombatState(); + bool needOtherAI = who.GetCombatManager().UpdateOwnerCombatState(); + + // then, we finally notify the AI (if necessary) and let it safely do whatever it feels like + if (needSelfAI) + NotifyAICombat(_owner, who); + if (needOtherAI) + NotifyAICombat(who, _owner); + return true; + } + + public bool IsInCombatWith(ObjectGuid guid) + { + return _pveRefs.ContainsKey(guid) || _pvpRefs.ContainsKey(guid); + } + + public bool IsInCombatWith(Unit who) + { + return IsInCombatWith(who.GetGUID()); + } + + public void InheritCombatStatesFrom(Unit who) + { + CombatManager mgr = who.GetCombatManager(); + foreach (var refe in mgr._pveRefs) + { + if (!IsInCombatWith(refe.Key)) + { + Unit target = refe.Value.GetOther(who); + if ((_owner.HasUnitFlag(UnitFlags.ImmuneToPc) && target.HasUnitFlag(UnitFlags.PvpAttackable)) || + (_owner.HasUnitFlag(UnitFlags.ImmuneToNpc) && !target.HasUnitFlag(UnitFlags.PvpAttackable))) + continue; + SetInCombatWith(target); + } + } + foreach (var refe in mgr._pvpRefs) + { + if (!IsInCombatWith(refe.Key)) + { + Unit target = refe.Value.GetOther(who); + if ((_owner.HasUnitFlag(UnitFlags.ImmuneToPc) && target.HasUnitFlag(UnitFlags.PvpAttackable)) || + (_owner.HasUnitFlag(UnitFlags.ImmuneToNpc) && !target.HasUnitFlag(UnitFlags.PvpAttackable))) + continue; + SetInCombatWith(target); + } + } + } + + public void EndCombatBeyondRange(float range, bool includingPvP) + { + foreach (var pair in _pveRefs.ToList()) + { + CombatReference refe = pair.Value; + if (!refe.first.IsWithinDistInMap(refe.second, range)) + { + _pveRefs.Remove(pair.Key); + refe.EndCombat(); + } + } + + if (!includingPvP) + return; + + foreach (var pair in _pvpRefs.ToList()) + { + CombatReference refe = pair.Value; + if (!refe.first.IsWithinDistInMap(refe.second, range)) + { + _pvpRefs.Remove(pair.Key); + refe.EndCombat(); + } + } + } + + public void SuppressPvPCombat() + { + foreach (var pair in _pvpRefs) + pair.Value.Suppress(_owner); + + if (UpdateOwnerCombatState()) + if (_owner.IsAIEnabled) + _owner.GetAI().JustExitedCombat(); + } + + public void EndAllPvECombat() + { + // cannot have threat without combat + _owner.GetThreatManager().RemoveMeFromThreatLists(); + _owner.GetThreatManager().ClearAllThreat(); + while (!_pveRefs.Empty()) + _pveRefs.First().Value.EndCombat(); + } + + void EndAllPvPCombat() + { + while (!_pvpRefs.Empty()) + _pvpRefs.First().Value.EndCombat(); + } + + public static void NotifyAICombat(Unit me, Unit other) + { + if (!me.IsAIEnabled) + return; + me.GetAI().JustEnteredCombat(other); + + Creature cMe = me.ToCreature(); + if (cMe != null) + if (!cMe.CanHaveThreatList()) + cMe.GetAI().JustEngagedWith(other); + } + + void PutReference(ObjectGuid guid, CombatReference refe) + { + if (refe._isPvP) + { + Cypher.Assert(!_pvpRefs.ContainsKey(guid), "Duplicate combat state detected!"); + _pvpRefs[guid] = (PvPCombatReference)refe; + } + else + { + Cypher.Assert(!_pveRefs.ContainsKey(guid), "Duplicate combat state detected!"); + _pveRefs[guid] = refe; + } + } + + public void PurgeReference(ObjectGuid guid, bool pvp) + { + if (pvp) + _pvpRefs.Remove(guid); + else + _pveRefs.Remove(guid); + } + + public bool UpdateOwnerCombatState() + { + bool combatState = HasCombat(); + if (combatState == _owner.IsInCombat()) + return false; + + if (combatState) + { + _owner.AddUnitFlag(UnitFlags.InCombat); + _owner.AtEnterCombat(); + } + else + { + _owner.RemoveUnitFlag(UnitFlags.InCombat); + _owner.AtExitCombat(); + } + + Unit master = _owner.GetCharmerOrOwner(); + if (master != null) + master.UpdatePetCombatState(); + + return true; + } + + + Unit GetOwner() { return _owner; } + + public bool HasCombat() { return HasPvECombat() || HasPvPCombat(); } + + public bool HasPvECombat() { return !_pveRefs.Empty(); } + + public Dictionary GetPvECombatRefs() { return _pveRefs; } + + public Dictionary GetPvPCombatRefs() { return _pvpRefs; } + + public void EndAllCombat() + { + EndAllPvECombat(); + EndAllPvPCombat(); + } + } + + public class CombatReference + { + public Unit first; + public Unit second; + public bool _isPvP; + + public CombatReference(Unit a, Unit b, bool pvp = false) + { + first = a; + second = b; + _isPvP = pvp; + } + + public void EndCombat() + { + // sequencing matters here - AI might do nasty stuff, so make sure refs are in a consistent state before you hand off! + + // first, get rid of any threat that still exists... + first.GetThreatManager().ClearThreat(second); + second.GetThreatManager().ClearThreat(first); + + // ...then, remove the references from both managers... + first.GetCombatManager().PurgeReference(second.GetGUID(), _isPvP); + second.GetCombatManager().PurgeReference(first.GetGUID(), _isPvP); + + // ...update the combat state, which will potentially remove IN_COMBAT... + bool needFirstAI = first.GetCombatManager().UpdateOwnerCombatState(); + bool needSecondAI = second.GetCombatManager().UpdateOwnerCombatState(); + + // ...and if that happened, also notify the AI of it... + if (needFirstAI && first.IsAIEnabled) + first.GetAI().JustExitedCombat(); + if (needSecondAI && second.IsAIEnabled) + second.GetAI().JustExitedCombat(); + } + + public Unit GetOther(Unit me) { return (first == me) ? second : first; } + } + + public class PvPCombatReference : CombatReference + { + public static uint PVP_COMBAT_TIMEOUT = 5 * Time.InMilliseconds; + + uint _combatTimer = PVP_COMBAT_TIMEOUT; + bool _suppressFirst = false; + bool _suppressSecond = false; + + public PvPCombatReference(Unit first, Unit second) : base(first, second, true) { } + + public bool Update(uint tdiff) + { + if (_combatTimer <= tdiff) + return false; + _combatTimer -= tdiff; + return true; + } + + public void Refresh() + { + _combatTimer = PVP_COMBAT_TIMEOUT; + + bool needFirstAI = false, needSecondAI = false; + if (_suppressFirst) + { + _suppressFirst = false; + needFirstAI = first.GetCombatManager().UpdateOwnerCombatState(); + } + if (_suppressSecond) + { + _suppressSecond = false; + needSecondAI = second.GetCombatManager().UpdateOwnerCombatState(); + } + + if (needFirstAI) + CombatManager.NotifyAICombat(first, second); + if (needSecondAI) + CombatManager.NotifyAICombat(second, first); + } + + void SuppressFor(Unit who) + { + Suppress(who); + if (who.GetCombatManager().UpdateOwnerCombatState()) + if (who.IsAIEnabled) + who.GetAI().JustExitedCombat(); + } + + // suppressed combat refs do not generate a combat state for one side of the relation + // (used by: vanish, feign death) + public bool IsSuppressedFor(Unit who) { return (who == first) ? _suppressFirst : _suppressSecond; } + + public void Suppress(Unit who) + { + if (who == first) + _suppressFirst = true; + else + _suppressSecond = true; + } + } +} diff --git a/Source/Game/Combat/Events.cs b/Source/Game/Combat/Events.cs deleted file mode 100644 index 87ab85210..000000000 --- a/Source/Game/Combat/Events.cs +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2012-2020 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 . - */ - -using System; - -namespace Game.Combat -{ - public class UnitBaseEvent - { - public UnitBaseEvent(UnitEventTypes pType) - { - iType = pType; - } - public UnitEventTypes GetEventType() - { - return iType; - } - bool MatchesTypeMask(uint pMask) - { - return Convert.ToBoolean((uint)iType & pMask); - } - - void SetEventType(UnitEventTypes pType) - { - iType = pType; - } - - UnitEventTypes iType; - } - - public class ThreatRefStatusChangeEvent : UnitBaseEvent - { - public ThreatRefStatusChangeEvent(UnitEventTypes pType) - : base(pType) - { - iHostileReference = null; - } - public ThreatRefStatusChangeEvent(UnitEventTypes pType, HostileReference pHostileReference) - : base(pType) - { - iHostileReference = pHostileReference; - } - public ThreatRefStatusChangeEvent(UnitEventTypes pType, HostileReference pHostileReference, float pValue) - : base(pType) - { - iHostileReference = pHostileReference; - iFValue = pValue; - } - public ThreatRefStatusChangeEvent(UnitEventTypes pType, HostileReference pHostileReference, bool pValue) - : base(pType) - { - iHostileReference = pHostileReference; - iBValue = pValue; - } - - public float GetFValue() - { - return iFValue; - } - - bool GetBValue() - { - return iBValue; - } - - void SetBValue(bool pValue) - { - iBValue = pValue; - } - - public HostileReference GetReference() - { - return iHostileReference; - } - - public void SetThreatManager(ThreatManager pThreatManager) - { - iThreatManager = pThreatManager; - } - - ThreatManager GetThreatManager() - { - return iThreatManager; - } - - float iFValue; - bool iBValue; - HostileReference iHostileReference; - ThreatManager iThreatManager; - } - - public enum UnitEventTypes - { - // Player/Pet Changed On/Offline Status - ThreatRefOnlineStatus = 1 << 0, - - // Threat For Player/Pet Changed - ThreatRefThreatChange = 1 << 1, - - // Player/Pet Will Be Removed From List (Dead) [For Internal Use] - ThreatRefRemoveFromList = 1 << 2, - - // Player/Pet Entered/Left Water Or Some Other Place Where It Is/Was Not Accessible For The Creature - ThreatRefAccessibleStatus = 1 << 3, - - // Threat List Is Going To Be Sorted (If Dirty Flag Is Set) - ThreatSortList = 1 << 4, - - // New Target Should Be Fetched, Could Tbe The Current Target As Well - ThreatSetNextTarget = 1 << 5, - - // A New Victim (Target) Was Set. Could Be Null - ThreatVictimChanged = 1 << 6 - - // Future Use - //Unit_Killed = 1<<7, - - //Future Use - //Unit_Health_Change = 1<<8, - } -} diff --git a/Source/Game/Combat/HostileRegManager.cs b/Source/Game/Combat/HostileRegManager.cs deleted file mode 100644 index 3dc76df45..000000000 --- a/Source/Game/Combat/HostileRegManager.cs +++ /dev/null @@ -1,398 +0,0 @@ -/* - * Copyright (C) 2012-2020 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 . - */ - -using Framework.Constants; -using Framework.Dynamic; -using Game.Entities; -using Game.Spells; - -namespace Game.Combat -{ - public class HostileRefManager : RefManager - { - Unit Owner; - - public HostileRefManager(Unit owner) - { - Owner = owner; - } - - Unit GetOwner() { return Owner; } - - // send threat to all my haters for the victim - // The victim is then hated by them as well - // use for buffs and healing threat functionality - public void ThreatAssist(Unit victim, float baseThreat, SpellInfo threatSpell = null) - { - float threat = ThreatManager.CalcThreat(victim, Owner, baseThreat, (threatSpell != null ? threatSpell.GetSchoolMask() : SpellSchoolMask.Normal), threatSpell); - threat /= GetSize(); - - HostileReference refe = GetFirst(); - while (refe != null) - { - if (ThreatManager.IsValidProcess(victim, refe.GetSource().GetOwner(), threatSpell)) - refe.GetSource().DoAddThreat(victim, threat); - - refe = refe.Next(); - } - } - - public void AddTempThreat(float threat, bool apply) - { - HostileReference refe = GetFirst(); - while (refe != null) - { - if (apply) - { - if (refe.GetTempThreatModifier() == 0.0f) - refe.AddTempThreat(threat); - } - else - refe.ResetTempThreat(); - - refe = refe.Next(); - } - } - - void AddThreatPercent(int percent) - { - HostileReference refe = GetFirst(); - while (refe != null) - { - refe.AddThreatPercent(percent); - refe = refe.Next(); - } - } - - // The references are not needed anymore - // tell the source to remove them from the list and free the mem - public void DeleteReferences() - { - HostileReference refe = GetFirst(); - while (refe != null) - { - HostileReference nextRef = refe.Next(); - refe.RemoveReference(); - refe = nextRef; - } - } - - // Remove specific faction references - public void DeleteReferencesForFaction(uint faction) - { - HostileReference refe = GetFirst(); - while (refe != null) - { - HostileReference nextRef = refe.Next(); - if (refe.GetSource().GetOwner().GetFactionTemplateEntry().Faction == faction) - { - refe.RemoveReference(); - } - refe = nextRef; - } - } - - // delete all references out of specified range - public void DeleteReferencesOutOfRange(float range) - { - HostileReference refe = GetFirst(); - range = range * range; - while (refe != null) - { - HostileReference nextRef = refe.Next(); - Unit owner = refe.GetSource().GetOwner(); - if (!owner.IsActiveObject() && owner.GetExactDist2dSq(GetOwner()) > range) - { - refe.RemoveReference(); - } - refe = nextRef; - } - } - - public new HostileReference GetFirst() { return (HostileReference)base.GetFirst(); } - - public void UpdateThreatTables() - { - HostileReference refe = GetFirst(); - while (refe != null) - { - refe.UpdateOnlineStatus(); - refe = refe.Next(); - } - } - - public void SetOnlineOfflineState(bool isOnline) - { - HostileReference refe = GetFirst(); - while (refe != null) - { - refe.SetOnlineOfflineState(isOnline); - refe = refe.Next(); - } - } - - // set state for one reference, defined by Unit - public void SetOnlineOfflineState(Unit creature, bool isOnline) - { - HostileReference refe = GetFirst(); - while (refe != null) - { - HostileReference nextRef = refe.Next(); - if (refe.GetSource().GetOwner() == creature) - { - refe.SetOnlineOfflineState(isOnline); - break; - } - refe = nextRef; - } - } - - // delete one reference, defined by Unit - public void DeleteReference(Unit creature) - { - HostileReference refe = GetFirst(); - while (refe != null) - { - HostileReference nextRef = refe.Next(); - if (refe.GetSource().GetOwner() == creature) - { - refe.RemoveReference(); - break; - } - refe = nextRef; - } - } - - public void UpdateVisibility() - { - HostileReference refe = GetFirst(); - while (refe != null) - { - HostileReference nextRef = refe.Next(); - if (!refe.GetSource().GetOwner().CanSeeOrDetect(GetOwner())) - { - nextRef = refe.Next(); - refe.RemoveReference(); - } - refe = nextRef; - } - } - } - - public class HostileReference : Reference - { - public HostileReference(Unit refUnit, ThreatManager threatManager, float threat) - { - iThreat = threat; - iTempThreatModifier = 0.0f; - Link(refUnit, threatManager); - iUnitGuid = refUnit.GetGUID(); - iOnline = true; - iAccessible = true; - } - - public override void TargetObjectBuildLink() - { - GetTarget().AddHatedBy(this); - } - - public override void TargetObjectDestroyLink() - { - GetTarget().RemoveHatedBy(this); - } - - public override void SourceObjectDestroyLink() - { - SetOnlineOfflineState(false); - } - - void FireStatusChanged(ThreatRefStatusChangeEvent threatRefStatusChangeEvent) - { - if (GetSource() != null) - GetSource().ProcessThreatEvent(threatRefStatusChangeEvent); - } - - public void AddThreat(float modThreat) - { - if (modThreat == 0.0f) - return; - - iThreat += modThreat; - - // the threat is changed. Source and target unit have to be available - // if the link was cut before relink it again - if (!IsOnline()) - UpdateOnlineStatus(); - - ThreatRefStatusChangeEvent Event = new(UnitEventTypes.ThreatRefThreatChange, this, modThreat); - FireStatusChanged(Event); - - if (IsValid() && modThreat > 0.0f) - { - Unit victimOwner = GetTarget().GetCharmerOrOwner(); - if (victimOwner != null && victimOwner.IsAlive()) - GetSource().AddThreat(victimOwner, 0.0f); // create a threat to the owner of a pet, if the pet attacks - } - } - - public void AddThreatPercent(int percent) - { - AddThreat(MathFunctions.CalculatePct(iThreat, percent)); - } - - // check, if source can reach target and set the status - public void UpdateOnlineStatus() - { - bool online = false; - bool accessible = false; - - if (!IsValid()) - { - Unit target = Global.ObjAccessor.GetUnit(GetSourceUnit(), GetUnitGuid()); - if (target != null) - Link(target, GetSource()); - } - - // only check for online status if - // ref is valid - // target is no player or not gamemaster - // target is not in flight - if (IsValid() - && (GetTarget().IsTypeId(TypeId.Player) || !GetTarget().ToPlayer().IsGameMaster()) - && !GetTarget().HasUnitState(UnitState.InFlight) - && GetTarget().IsInMap(GetSourceUnit()) - && GetTarget().IsInPhase(GetSourceUnit()) - ) - { - Creature creature = GetSourceUnit().ToCreature(); - online = GetTarget().IsInAccessiblePlaceFor(creature); - if (!online) - { - if (creature.IsWithinCombatRange(GetTarget(), creature.m_CombatDistance)) - online = true; // not accessible but stays online - } - else - accessible = true; - } - SetAccessibleState(accessible); - SetOnlineOfflineState(online); - } - - public void SetOnlineOfflineState(bool isOnline) - { - if (iOnline != isOnline) - { - iOnline = isOnline; - if (!iOnline) - SetAccessibleState(false); // if not online that not accessable as well - - ThreatRefStatusChangeEvent Event = new(UnitEventTypes.ThreatRefOnlineStatus, this); - FireStatusChanged(Event); - } - } - - void SetAccessibleState(bool isAccessible) - { - if (iAccessible != isAccessible) - { - iAccessible = isAccessible; - - ThreatRefStatusChangeEvent Event = new(UnitEventTypes.ThreatRefAccessibleStatus, this); - FireStatusChanged(Event); - } - } - - // reference is not needed anymore. realy delete it ! - public void RemoveReference() - { - Invalidate(); - - ThreatRefStatusChangeEvent Event = new(UnitEventTypes.ThreatRefRemoveFromList, this); - FireStatusChanged(Event); - } - - Unit GetSourceUnit() - { - return GetSource().GetOwner(); - } - - public void SetThreat(float threat) - { - AddThreat(threat - iThreat); - } - - public float GetThreat() - { - return iThreat + iTempThreatModifier; - } - - public bool IsOnline() - { - return iOnline; - } - - // The Unit might be in water and the creature can not enter the water, but has range attack - // in this case online = true, but accessible = false - bool IsAccessible() - { - return iAccessible; - } - - // used for temporary setting a threat and reducing it later again. - // the threat modification is stored - public void SetTempThreat(float threat) - { - AddTempThreat(threat - iTempThreatModifier); - } - - public void AddTempThreat(float threat) - { - if (threat == 0.0f) - return; - - iTempThreatModifier += threat; - - ThreatRefStatusChangeEvent Event = new(UnitEventTypes.ThreatRefThreatChange, this, threat); - FireStatusChanged(Event); - } - - public void ResetTempThreat() - { - AddTempThreat(-iTempThreatModifier); - } - - public float GetTempThreatModifier() - { - return iTempThreatModifier; - } - - public ObjectGuid GetUnitGuid() - { - return iUnitGuid; - } - - public Unit GetVictim() { return GetTarget(); } - - public new HostileReference Next() { return (HostileReference)base.Next(); } - - float iThreat; - float iTempThreatModifier; // used for SPELL_AURA_MOD_TOTAL_THREAT - ObjectGuid iUnitGuid; - bool iOnline; - bool iAccessible; - } -} \ No newline at end of file diff --git a/Source/Game/Combat/ThreatManager.cs b/Source/Game/Combat/ThreatManager.cs index 1208a8a21..dc5a7451a 100644 --- a/Source/Game/Combat/ThreatManager.cs +++ b/Source/Game/Combat/ThreatManager.cs @@ -17,7 +17,9 @@ using Framework.Constants; using Game.Entities; +using Game.Networking.Packets; using Game.Spells; +using System; using System.Collections.Generic; using System.Linq; @@ -25,528 +27,863 @@ namespace Game.Combat { public class ThreatManager { - public ThreatManager(Unit owner) - { - currentVictim = null; - Owner = owner; - updateTimer = ThreatUpdateInternal; - threatContainer = new ThreatContainer(); - threatOfflineContainer = new ThreatContainer(); - } + public static uint CLIENT_THREAT_UPDATE_INTERVAL = 1000u; + static CompareThreatLessThan CompareThreat = new(); - const int ThreatUpdateInternal = 1 * Time.InMilliseconds; - - public void ForwardThreatForAssistingMe(Unit victim, float amount, SpellInfo spell, bool ignoreModifiers = false, bool ignoreRedirection = false) - { - GetOwner().GetHostileRefManager().ThreatAssist(victim, amount, spell); - } - - public void AddThreat(Unit victim, float amount, SpellInfo spell, bool ignoreModifiers = false, bool ignoreRedirection = false) - { - if (!Owner.CanHaveThreatList() || Owner.HasUnitState(UnitState.Evade)) - return; - - if (Owner.IsControlledByPlayer() || victim.IsControlledByPlayer()) - { - if (Owner.IsFriendlyTo(victim) || victim.IsFriendlyTo(Owner)) - return; - } - else if (!Owner.IsHostileTo(victim) && !victim.IsHostileTo(Owner)) - return; - - Owner.SetInCombatWith(victim); - victim.SetInCombatWith(Owner); - AddThreat(victim, amount, spell != null ? spell.GetSchoolMask() : victim.GetMeleeDamageSchoolMask(), spell); - } - - public void ClearAllThreat() - { - if (Owner.CanHaveThreatList(true) && !IsThreatListEmpty()) - Owner.SendClearThreatList(); - ClearReferences(); - } + public Unit _owner; + bool _ownerCanHaveThreatList; + bool _ownerEngaged; - public void ClearReferences() + uint _updateClientTimer; + List _sortedThreatList = new(); + Dictionary _myThreatListEntries = new(); + ThreatReference _currentVictimRef; + + Dictionary _threatenedByMe = new(); // these refs are entries for myself on other units' threat lists + float[] _singleSchoolModifiers = new float[(int)SpellSchools.Max]; // most spells are single school - we pre-calculate these and store them + volatile Dictionary _multiSchoolModifiers = new(); // these are calculated on demand + + List> _redirectInfo = new(); // current redirection targets and percentages (updated from registry in ThreatManager::UpdateRedirectInfo) + Dictionary> _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) { - threatContainer.ClearReferences(); - threatOfflineContainer.ClearReferences(); - currentVictim = null; - updateTimer = ThreatUpdateInternal; - } - - public void AddThreat(Unit victim, float threat, SpellSchoolMask schoolMask = SpellSchoolMask.Normal, SpellInfo threatSpell = null) - { - if (!IsValidProcess(victim, Owner, threatSpell)) - return; - - DoAddThreat(victim, CalcThreat(victim, Owner, threat, schoolMask, threatSpell)); - } - - public void DoAddThreat(Unit victim, float threat) - { - uint redirectThreadPct = victim.GetRedirectThreatPercent(); - Unit redirectTarget = victim.GetRedirectThreatTarget(); - - // If victim is personal spawn, redirect all aggro to summoner - if (victim.IsPrivateObject() && (!GetOwner().IsPrivateObject() || !GetOwner().CheckPrivateObjectOwnerVisibility(victim))) - { - redirectThreadPct = 100; - redirectTarget = Global.ObjAccessor.GetUnit(GetOwner(), victim.GetPrivateObjectOwner()); - } - - // must check > 0.0f, otherwise dead loop - if (threat > 0.0f && redirectThreadPct != 0) - { - if (redirectTarget != null) - { - float redirectThreat = MathFunctions.CalculatePct(threat, redirectThreadPct); - threat -= redirectThreat; - if (IsValidProcess(redirectTarget, GetOwner())) - AddThreat(redirectTarget, redirectThreat); - } - } - - AddThreat(victim, threat); - } - - void AddThreat(Unit victim, float threat) - { - var reff = threatContainer.AddThreat(victim, threat); - // Ref is not in the online refs, search the offline refs next - if (reff == null) - reff = threatOfflineContainer.AddThreat(victim, threat); - - if (reff == null) // there was no ref => create a new one - { - bool isFirst = threatContainer.Empty(); - - // threat has to be 0 here - var hostileRef = new HostileReference(victim, this, 0); - threatContainer.AddReference(hostileRef); - hostileRef.AddThreat(threat); // now we add the real threat - if (victim.IsTypeId(TypeId.Player) && victim.ToPlayer().IsGameMaster()) - hostileRef.SetOnlineOfflineState(false); // GM is always offline - else if (isFirst) - SetCurrentVictim(hostileRef); - } - } - - public void ModifyThreatByPercent(Unit victim, int percent) - { - threatContainer.ModifyThreatByPercent(victim, percent); - } - - public Unit GetHostilTarget() - { - threatContainer.Update(); - HostileReference nextVictim = threatContainer.SelectNextVictim(GetOwner().ToCreature(), getCurrentVictim()); - SetCurrentVictim(nextVictim); - return GetCurrentVictim() != null ? GetCurrentVictim() : null; - } - - public float GetThreat(Unit victim, bool alsoSearchOfflineList = false) - { - float threat = 0.0f; - HostileReference refe = threatContainer.GetReferenceByTarget(victim); - if (refe == null && alsoSearchOfflineList) - refe = threatOfflineContainer.GetReferenceByTarget(victim); - if (refe != null) - threat = refe.GetThreat(); - return threat; - } - - void TauntApply(Unit taunter) - { - HostileReference refe = threatContainer.GetReferenceByTarget(taunter); - if (GetCurrentVictim() != null && refe != null && (refe.GetThreat() < getCurrentVictim().GetThreat())) - { - if (refe.GetTempThreatModifier() == 0.0f) // Ok, temp threat is unused - refe.SetTempThreat(getCurrentVictim().GetThreat()); - } - } - - void TauntFadeOut(Unit taunter) - { - HostileReference refe = threatContainer.GetReferenceByTarget(taunter); - if (refe != null) - refe.ResetTempThreat(); - } - - public void SetCurrentVictim(HostileReference pHostileReference) - { - if (pHostileReference != null && pHostileReference != currentVictim) - { - Owner.SendChangeCurrentVictim(pHostileReference); - } - currentVictim = pHostileReference; - } - - public void ProcessThreatEvent(ThreatRefStatusChangeEvent threatRefStatusChangeEvent) - { - threatRefStatusChangeEvent.SetThreatManager(this); // now we can set the threat manager - - HostileReference hostilRef = threatRefStatusChangeEvent.GetReference(); - - switch (threatRefStatusChangeEvent.GetEventType()) - { - case UnitEventTypes.ThreatRefThreatChange: - if ((getCurrentVictim() == hostilRef && threatRefStatusChangeEvent.GetFValue() < 0.0f) || - (getCurrentVictim() != hostilRef && threatRefStatusChangeEvent.GetFValue() > 0.0f)) - SetDirty(true); // the order in the threat list might have changed - break; - case UnitEventTypes.ThreatRefOnlineStatus: - if (!hostilRef.IsOnline()) - { - if (hostilRef == getCurrentVictim()) - { - SetCurrentVictim(null); - SetDirty(true); - } - Owner.SendRemoveFromThreatList(hostilRef); - threatContainer.Remove(hostilRef); - threatOfflineContainer.AddReference(hostilRef); - } - else - { - if (GetCurrentVictim() != null && hostilRef.GetThreat() > (1.1f * getCurrentVictim().GetThreat())) - SetDirty(true); - threatContainer.AddReference(hostilRef); - threatOfflineContainer.Remove(hostilRef); - } - break; - case UnitEventTypes.ThreatRefRemoveFromList: - if (hostilRef == getCurrentVictim()) - { - SetCurrentVictim(null); - SetDirty(true); - } - Owner.SendRemoveFromThreatList(hostilRef); - if (hostilRef.IsOnline()) - threatContainer.Remove(hostilRef); - else - threatOfflineContainer.Remove(hostilRef); - break; - } - } - - public bool IsNeedUpdateToClient(uint time) - { - if (IsThreatListEmpty()) + Creature cWho = who.ToCreature(); + // only creatures can have threat list + if (!cWho) return false; - if (time >= updateTimer) - { - updateTimer = ThreatUpdateInternal; - return true; - } - updateTimer -= time; - return false; - } - - // Reset all aggro without modifying the threatlist. - void ResetAllAggro() - { - var threatList = threatContainer.threatList; - if (threatList.Empty()) - return; - - foreach (var refe in threatList) - refe.SetThreat(0); - - SetDirty(true); - } - public bool IsThreatListEmpty() - { - return threatContainer.Empty(); - } - public bool IsThreatListsEmpty() - { - return threatContainer.Empty() && threatOfflineContainer.Empty(); - } - - public HostileReference getCurrentVictim() { return currentVictim; } - - public Unit GetOwner() - { - return Owner; - } - - void SetDirty(bool isDirty) - { - threatContainer.SetDirty(isDirty); - } - - public List GetThreatList() { return threatContainer.GetThreatList(); } - public List GetOfflineThreatList() { return threatOfflineContainer.GetThreatList(); } - public ThreatContainer GetOnlineContainer() { return threatContainer; } - - public Unit SelectVictim() { return GetHostilTarget(); } - public Unit GetCurrentVictim() - { - var refe = getCurrentVictim(); - if (refe != null) - return refe.GetTarget(); - else - return null; - } - public bool IsThreatListEmpty(bool includeOffline = false) { return includeOffline ? IsThreatListsEmpty() : IsThreatListEmpty(); } - public bool IsThreatenedBy(Unit who, bool includeOffline = false) { return FindReference(who, includeOffline) != null; } - public int GetThreatListSize() { return threatContainer.threatList.Count; } - public Unit GetAnyTarget() - { - var list = GetThreatList(); - if (!list.Empty()) - return list[0].GetTarget(); - - return null; - } - public void ResetThreat(Unit who) - { - var refe = FindReference(who, true); - if (refe != null) - refe.SetThreat(0.0f); - - } - public void ResetAllThreat() { ResetAllAggro(); } - - HostileReference FindReference(Unit who, bool includeOffline) - { - var refe = threatContainer.GetReferenceByTarget(who); - if (refe != null) - return refe; - - if (includeOffline) - { - var offlineRefe = threatOfflineContainer.GetReferenceByTarget(who); - if (offlineRefe != null) - return offlineRefe; - } - - return null; - } - - // The hatingUnit is not used yet - public static float CalcThreat(Unit hatedUnit, Unit hatingUnit, float threat, SpellSchoolMask schoolMask = SpellSchoolMask.Normal, SpellInfo threatSpell = null) - { - if (threatSpell != null) - { - var threatEntry = Global.SpellMgr.GetSpellThreatEntry(threatSpell.Id); - if (threatEntry != null) - if (threatEntry.pctMod != 1.0f) - threat *= threatEntry.pctMod; - - // Energize is not affected by Mods - foreach (SpellEffectInfo effect in threatSpell.GetEffects()) - if (effect != null && (effect.Effect == SpellEffectName.Energize || effect.ApplyAuraName == AuraType.PeriodicEnergize)) - return threat; - - Player modOwner = hatedUnit.GetSpellModOwner(); - if (modOwner != null) - modOwner.ApplySpellMod(threatSpell, SpellModOp.Hate, ref threat); - } - - return hatedUnit.ApplyTotalThreatModifier(threat, schoolMask); - } - - public static bool IsValidProcess(Unit hatedUnit, Unit hatingUnit, SpellInfo threatSpell = null) - { - //function deals with adding threat and adding players and pets into ThreatList - //mobs, NPCs, guards have ThreatList and HateOfflineList - //players and pets have only InHateListOf - //HateOfflineList is used co contain unattackable victims (in-flight, in-water, GM etc.) - - if (hatedUnit == null || hatingUnit == null) + // pets, totems and triggers cannot have threat list + if (cWho.IsPet() || cWho.IsTotem() || cWho.IsTrigger()) return false; - // not to self - if (hatedUnit == hatingUnit) + // 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; - // not to GM - if (hatedUnit.IsTypeId(TypeId.Player) && hatedUnit.ToPlayer().IsGameMaster()) - return false; - - // not to dead and not for dead - if (!hatedUnit.IsAlive() || !hatingUnit.IsAlive()) - return false; - - // not in same map or phase - if (!hatedUnit.IsInMap(hatingUnit) || !hatedUnit.IsInPhase(hatingUnit)) - return false; - - // spell not causing threat - if (threatSpell != null && threatSpell.HasAttribute(SpellAttr1.NoThreat)) - return false; - - Cypher.Assert(hatingUnit.IsTypeId(TypeId.Unit)); - return true; } - Unit Owner; - HostileReference currentVictim; - uint updateTimer; - ThreatContainer threatContainer; - ThreatContainer threatOfflineContainer; - } - - public class ThreatContainer - { - public ThreatContainer() + public ThreatManager(Unit owner) { - threatList = new List(); - iDirty = false; + _owner = owner; + _updateClientTimer = CLIENT_THREAT_UPDATE_INTERVAL; + + for (var i = 0; i < (int)SpellSchools.Max; ++i) + _singleSchoolModifiers[i] = 1.0f; } - public void ClearReferences() + public void Initialize() { - foreach (var reff in threatList) - { - reff.Unlink(); - } - - threatList.Clear(); + _ownerCanHaveThreatList = CanHaveThreatList(_owner); } - public HostileReference GetReferenceByTarget(Unit victim) + public void Update(uint tdiff) { - if (victim == null) - return null; - - ObjectGuid guid = victim.GetGUID(); - foreach (var reff in threatList) + if (!CanHaveThreatList() || !IsEngaged()) + return; + if (_updateClientTimer <= tdiff) { - if (reff != null && reff.GetUnitGuid() == guid) - return reff; + _updateClientTimer = CLIENT_THREAT_UPDATE_INTERVAL; + SendThreatListToClients(); } + else + _updateClientTimer -= tdiff; + } + + public Unit GetCurrentVictim() + { + if (_currentVictimRef != null) + return _currentVictimRef.GetVictim(); return null; } - public HostileReference AddThreat(Unit victim, float threat) + public Unit GetAnyTarget() { - var reff = GetReferenceByTarget(victim); - if (reff != null) - reff.AddThreat(threat); - return reff; + foreach (ThreatReference refe in _sortedThreatList) + if (!refe.IsOffline()) + return refe.GetVictim(); + + return null; } - public void ModifyThreatByPercent(Unit victim, int percent) + public Unit SelectVictim() { - HostileReference refe = GetReferenceByTarget(victim); - if (refe != null) - refe.AddThreatPercent(percent); - } + if (_sortedThreatList.Empty()) + return null; - public void Update() - { - if (iDirty && threatList.Count > 1) - threatList = threatList.OrderByDescending(p => p.GetThreat()).ToList(); - - iDirty = false; - } - - public HostileReference SelectNextVictim(Creature attacker, HostileReference currentVictim) - { - HostileReference currentRef = null; - bool found = false; - bool noPriorityTargetFound = false; - - for (var i = 0; i < threatList.Count; i++) + ThreatReference newVictimRef = ReselectVictim(); + if (newVictimRef != _currentVictimRef) { - if (found) - break; + if (newVictimRef != null) + SendNewVictimToClients(newVictimRef); - currentRef = threatList[i]; + _currentVictimRef = newVictimRef; + } + return newVictimRef != null ? newVictimRef.GetVictim() : null; + } - Unit target = currentRef.GetTarget(); - Cypher.Assert(target); // if the ref has status online the target must be there ! + public bool IsThreatListEmpty(bool includeOffline = false) + { + if (includeOffline) + return _sortedThreatList.Empty(); - // some units are prefered in comparison to others - if (!noPriorityTargetFound && (target.IsImmunedToDamage(attacker.GetMeleeDamageSchoolMask()) || target.HasNegativeAuraWithInterruptFlag(SpellAuraInterruptFlags.Damage))) + 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 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) { - if (i != threatList.Count - 1) + 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)) { - // current victim is a second choice target, so don't compare threat with it below - if (currentRef == currentVictim) - currentVictim = null; - continue; - } - else - { - // if we reached to this point, everyone in the threatlist is a second choice target. In such a situation the target with the highest threat should be attacked. - noPriorityTargetFound = true; - i = 0; - continue; + Unit redirTarget = Global.ObjAccessor.GetUnit(_owner, pair.Item1); + if (redirTarget != null) + combatMgr.SetInCombatWith(redirTarget); } } + return; + } - if (attacker.CanCreatureAttack(target)) // skip non attackable currently targets + // 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()) { - if (currentVictim != null) // select 1.3/1.1 better target in comparison current target + 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) { - // list sorted and and we check current target, then this is best case - if (currentVictim == currentRef || currentRef.GetThreat() <= 1.1f * currentVictim.GetThreat()) + var pair = redirInfo[i]; // (victim,pct) + Unit redirTarget = null; + + 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) { - if (currentVictim != currentRef && attacker.CanCreatureAttack(currentVictim.GetTarget())) - currentRef = currentVictim; // for second case, if currentvictim is attackable - - found = true; - break; + float amountRedirected = MathFunctions.CalculatePct(origAmount, pair.Item2); + AddThreat(redirTarget, amountRedirected, spell, true, true); + amount -= amountRedirected; } - - if (currentRef.GetThreat() > 1.3f * currentVictim.GetThreat() || - (currentRef.GetThreat() > 1.1f * currentVictim.GetThreat() && - attacker.IsWithinMeleeRange(target))) - { //implement 110% threat rule for targets in melee range - found = true; //and 130% rule for targets in ranged distances - break; //for selecting alive targets - } - } - else // select any - { - found = true; - break; } } } - if (!found) - currentRef = null; - return currentRef; + // 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; + } + + // otherwise, 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, we're now in combat - create the threat list reference and push it to the respective managers + ThreatReference newRefe = new ThreatReference(this, target, amount); + PutThreatListRef(target.GetGUID(), newRefe); + target.GetThreatManager().PutThreatenedByMeRef(_owner.GetGUID(), newRefe); + if (!newRefe.IsOffline() && !_ownerEngaged) + { + _ownerEngaged = true; + + Creature cOwner = _owner.ToCreature(); + Cypher.Assert(cOwner != null); // if we got here the owner can have a threat list, and must be a creature! + SaveCreatureHomePositionIfNeed(cOwner); + if (cOwner.IsAIEnabled) + cOwner.GetAI().JustEngagedWith(target); + } } - public void SetDirty(bool isDirty) + void ScaleThreat(Unit target, float factor) { - iDirty = isDirty; + var refe = _myThreatListEntries.LookupByKey(target.GetGUID()); + if (refe != null) + refe.ScaleThreat(Math.Max(factor, 0.0f)); } - bool IsDirty() + public void MatchUnitThreatToHighestThreat(Unit target) { - return iDirty; + 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 bool Empty() + public void TauntUpdate() { - return threatList.Empty(); - } - public HostileReference GetMostHated() - { - return threatList.Count == 0 ? null : threatList[0]; + 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 Remove(HostileReference hostileRef) + public void ResetAllThreat() { - threatList.Remove(hostileRef); - } - public void AddReference(HostileReference hostileRef) - { - threatList.Add(hostileRef); + foreach (var pair in _myThreatListEntries) + pair.Value.SetThreat(0.0f); } - public List GetThreatList() { return threatList; } + public void ClearThreat(Unit target) + { + var refe = _myThreatListEntries.LookupByKey(target.GetGUID()); + if (refe != null) + refe.ClearThreat(); + } - public List threatList { get; set; } - bool iDirty; + 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.ToString()}!"); + _myThreatListEntries[guid] = refe; + _sortedThreatList.Add(refe); + _sortedThreatList.Sort(CompareThreat); + } + + 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(CompareThreat); + 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 GetSortedThreatList() { return _sortedThreatList; } + + public void ListNotifyChanged() + { + _sortedThreatList.Sort(CompareThreat); + } + + public Dictionary 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 + { + 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; + + bool increase = (onlineState > _online); + _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 class CompareThreatLessThan : IComparer + { + public int Compare(ThreatReference a, ThreatReference b) + { + return ThreatManager.CompareReferencesLT(a, b, 1.0f) ? 1 : -1; + } } } diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 6a8ede463..324aadabc 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -19,6 +19,7 @@ using Framework.Constants; using Framework.Database; using Framework.Dynamic; using Game.AI; +using Game.Combat; using Game.DataStorage; using Game.Groups; using Game.Loots; @@ -420,6 +421,8 @@ namespace Game.Entities UpdateMovementFlags(); LoadCreaturesAddon(); LoadTemplateImmunities(); + + GetThreatManager().UpdateOnlineStates(true, true); return true; } @@ -511,6 +514,8 @@ namespace Game.Entities if (!IsAlive()) break; + GetThreatManager().Update(diff); + if (m_shouldReacquireTarget && !IsFocusing(null, true)) { SetTarget(m_suppressedTarget); @@ -903,10 +908,110 @@ namespace Game.Entities ApplySpellImmune(0, SpellImmunity.Effect, SpellEffectName.KnockBackDest, true); } + GetThreatManager().Initialize(); + return true; } - void InitializeReactState() + public Unit SelectVictim() + { + Unit target = null; + + ThreatManager mgr = GetThreatManager(); + + if (mgr.CanHaveThreatList()) + { + target = mgr.SelectVictim(); + while (!target) + { + Unit newTarget = null; + // nothing found to attack - try to find something we're in combat with (but don't have a threat entry for yet) and start attacking it + foreach (var pair in GetCombatManager().GetPvECombatRefs()) + { + newTarget = pair.Value.GetOther(this); + if (!mgr.IsThreatenedBy(newTarget, true)) + { + mgr.AddThreat(newTarget, 0.0f, null, true, true); + break; + } + else + newTarget = null; + } + if (!newTarget) + break; + target = mgr.SelectVictim(); + } + } + else if (!HasReactState(ReactStates.Passive)) + { + // We're a player pet, probably + target = GetAttackerForHelper(); + if (!target && IsSummon()) + { + Unit owner = ToTempSummon().GetOwner(); + if (owner != null) + { + if (owner.IsInCombat()) + target = owner.GetAttackerForHelper(); + if (!target) + { + foreach (var itr in owner.m_Controlled) + { + if (itr.IsInCombat()) + { + target = itr.GetAttackerForHelper(); + if (target) + break; + } + } + } + } + } + } + else + return null; + + if (target && _IsTargetAcceptable(target) && CanCreatureAttack(target)) + { + if (!IsFocusing(null, true)) + SetInFront(target); + return target; + } + + /// @todo a vehicle may eat some mob, so mob should not evade + if (GetVehicle()) + return null; + + // search nearby enemy before enter evade mode + if (HasReactState(ReactStates.Passive)) + { + target = SelectNearestTargetInAttackDistance(m_CombatDistance != 0 ? m_CombatDistance : SharedConst.AttackDistance); + + if (target && _IsTargetAcceptable(target) && CanCreatureAttack(target)) + return target; + } + + var iAuras = GetAuraEffectsByType(AuraType.ModInvisibility); + if (!iAuras.Empty()) + { + foreach (var itr in iAuras) + { + if (itr.GetBase().IsPermanent()) + { + GetAI().EnterEvadeMode(EvadeReason.Other); + break; + } + } + return null; + } + + // enter in evade mode in other case + GetAI().EnterEvadeMode(EvadeReason.NoHostiles); + + return null; + } + + public void InitializeReactState() { if (IsTotem() || IsTrigger() || IsCritter() || IsSpiritService()) SetReactState(ReactStates.Passive); @@ -994,6 +1099,37 @@ namespace Game.Entities && !GetCreatureTemplate().FlagsExtra.HasAnyFlag(CreatureFlagsExtra.NoXpAtKill); } + public override void AtEnterCombat() + { + base.AtEnterCombat(); + + if (!GetCreatureTemplate().TypeFlags.HasAnyFlag(CreatureTypeFlags.MountedCombatAllowed)) + Dismount(); + + if (IsPet() || IsGuardian()) // update pets' speed for catchup OOC speed + { + UpdateSpeed(UnitMoveType.Run); + UpdateSpeed(UnitMoveType.Swim); + UpdateSpeed(UnitMoveType.Flight); + } + } + + public override void AtExitCombat() + { + base.AtExitCombat(); + + ClearUnitState(UnitState.AttackPlayer); + if (HasDynamicFlag(UnitDynFlags.Tapped)) + SetDynamicFlags((UnitDynFlags)GetCreatureTemplate().DynamicFlags); + + if (IsPet() || IsGuardian()) // update pets' speed for catchup OOC speed + { + UpdateSpeed(UnitMoveType.Run); + UpdateSpeed(UnitMoveType.Swim); + UpdateSpeed(UnitMoveType.Flight); + } + } + public bool IsEscortNPC(bool onlyIfActive = true) { if (!IsAIEnabled) @@ -3147,127 +3283,6 @@ namespace Game.Entities // There's many places not ready for dynamic spawns. This allows them to live on for now. void SetRespawnCompatibilityMode(bool mode = true) { m_respawnCompatibilityMode = mode; } public bool GetRespawnCompatibilityMode() { return m_respawnCompatibilityMode; } - - public Unit SelectVictim() - { - // function provides main threat functionality - // next-victim-selection algorithm and evade mode are called - // threat list sorting etc. - - Unit target = null; - - // First checking if we have some taunt on us - var tauntAuras = GetAuraEffectsByType(AuraType.ModTaunt); - if (!tauntAuras.Empty()) - { - Unit caster = tauntAuras.Last().GetCaster(); - - // The last taunt aura caster is alive an we are happy to attack him - if (caster != null && caster.IsAlive()) - return GetVictim(); - else if (tauntAuras.Count > 1) - { - // We do not have last taunt aura caster but we have more taunt auras, - // so find first available target - - // Auras are pushed_back, last caster will be on the end - for (var i = tauntAuras.Count - 1; i >= 0; i--) - { - caster = tauntAuras[i].GetCaster(); - if (caster != null && CanSeeOrDetect(caster, true) && IsValidAttackTarget(caster) && caster.IsInAccessiblePlaceFor(ToCreature())) - { - target = caster; - break; - } - } - } - else - target = GetVictim(); - } - - if (CanHaveThreatList()) - { - if (target == null && !GetThreatManager().IsThreatListEmpty()) - // No taunt aura or taunt aura caster is dead standard target selection - target = GetThreatManager().GetHostilTarget(); - } - else if (!HasReactState(ReactStates.Passive)) - { - // We have player pet probably - target = GetAttackerForHelper(); - if (target == null && IsSummon()) - { - Unit owner = ToTempSummon().GetOwner(); - if (owner != null) - { - if (owner.IsInCombat()) - target = owner.GetAttackerForHelper(); - if (target == null) - { - foreach (var unit in owner.m_Controlled) - { - if (unit.IsInCombat()) - { - target = unit.GetAttackerForHelper(); - if (target) - break; - } - } - } - } - } - } - else - return null; - - if (target != null && _IsTargetAcceptable(target) && CanCreatureAttack(target)) - { - if (!IsFocusing(null, true)) - SetInFront(target); - return target; - } - - // last case when creature must not go to evade mode: - // it in combat but attacker not make any damage and not enter to aggro radius to have record in threat list - // Note: creature does not have targeted movement generator but has attacker in this case - foreach (var unit in attackerList) - { - if (CanCreatureAttack(unit) && !unit.IsTypeId(TypeId.Player) - && !unit.ToCreature().HasUnitTypeMask(UnitTypeMask.ControlableGuardian)) - return null; - } - - // @todo a vehicle may eat some mob, so mob should not evade - if (GetVehicle() != null) - return null; - - // search nearby enemy before enter evade mode - if (HasReactState(ReactStates.Aggressive)) - { - target = SelectNearestTargetInAttackDistance(m_CombatDistance != 0 ? m_CombatDistance : SharedConst.AttackDistance); - - if (target != null && _IsTargetAcceptable(target) && CanCreatureAttack(target)) - return target; - } - - var iAuras = GetAuraEffectsByType(AuraType.ModInvisibility); - if (!iAuras.Empty()) - { - foreach (var aura in iAuras) - { - if (aura.GetBase().IsPermanent()) - { - GetAI().EnterEvadeMode(); - break; - } - } - return null; - } - - // enter in evade mode in other case - GetAI().EnterEvadeMode(EvadeReason.NoHostiles); - return null; - } } public class VendorItemCount diff --git a/Source/Game/Entities/Object/WorldObject.cs b/Source/Game/Entities/Object/WorldObject.cs index 2d23ab0d1..2b5382c0e 100644 --- a/Source/Game/Entities/Object/WorldObject.cs +++ b/Source/Game/Entities/Object/WorldObject.cs @@ -1604,6 +1604,11 @@ namespace Game.Entities { return GetPhaseShift().CanSee(obj.GetPhaseShift()); } + + public static bool InSamePhase(WorldObject a, WorldObject b) + { + return a != null && b != null && a.IsInPhase(b); + } public virtual float GetCombatReach() { return 0.0f; } // overridden (only) in Unit public PhaseShift GetPhaseShift() { return _phaseShift; } diff --git a/Source/Game/Entities/Pet.cs b/Source/Game/Entities/Pet.cs index e5386ef5b..f3b6e7539 100644 --- a/Source/Game/Entities/Pet.cs +++ b/Source/Game/Entities/Pet.cs @@ -1343,6 +1343,8 @@ namespace Game.Entities AddUnitFlag2(UnitFlags2.RegeneratePower); SetSheath(SheathState.Melee); + GetThreatManager().Initialize(); + return true; } diff --git a/Source/Game/Entities/Player/Player.Combat.cs b/Source/Game/Entities/Player/Player.Combat.cs index 024cab9c3..900aa33bf 100644 --- a/Source/Game/Entities/Player/Player.Combat.cs +++ b/Source/Game/Entities/Player/Player.Combat.cs @@ -342,6 +342,20 @@ namespace Game.Entities UpdateDamagePhysical(attType); } + public override void AtEnterCombat() + { + base.AtEnterCombat(); + if (GetCombatManager().HasPvPCombat()) + EnablePvpRules(true); + } + + public override void AtExitCombat() + { + base.AtExitCombat(); + UpdatePotionCooldown(); + m_combatExitTime = Time.GetMSTime(); + } + public override float GetBlockPercent(uint attackerLevel) { float blockArmor = (float)m_activePlayerData.ShieldBlock; diff --git a/Source/Game/Entities/Player/Player.PvP.cs b/Source/Game/Entities/Player/Player.PvP.cs index e320227bc..cd3fd8284 100644 --- a/Source/Game/Entities/Player/Player.PvP.cs +++ b/Source/Game/Entities/Player/Player.PvP.cs @@ -475,7 +475,7 @@ namespace Game.Entities if (IsInAreaThatActivatesPvpTalents()) return; - if (GetCombatTimer() == 0) + if (!GetCombatManager().HasPvPCombat()) { RemoveAurasDueToSpell(PlayerConst.SpellPvpRulesEnabled); UpdateItemLevelAreaBasedScaling(); diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index abf7d8584..a037bed07 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -316,6 +316,8 @@ namespace Game.Entities SetPrimarySpecialization(defaultSpec.Id); } + GetThreatManager().Initialize(); + return true; } public override void Update(uint diff) @@ -358,7 +360,7 @@ namespace Game.Entities UpdateAfkReport(now); - if (GetCombatTimer() != 0) // Only set when in pvp combat + if (GetCombatManager().HasPvPCombat()) // Only set when in pvp combat { Aura aura = GetAura(PlayerConst.SpellPvpRulesEnabled); if (aura != null) @@ -615,7 +617,7 @@ namespace Game.Entities { m_hostileReferenceCheckTimer = 15 * Time.InMilliseconds; if (!GetMap().IsDungeon()) - GetHostileRefManager().DeleteReferencesOutOfRange(GetVisibilityRange()); + GetCombatManager().EndCombatBeyondRange(GetVisibilityRange(), true); } else m_hostileReferenceCheckTimer -= diff; @@ -1908,9 +1910,6 @@ namespace Game.Entities // Call base base.SetInWater(inWater); - - // Update threat tables - GetHostileRefManager().UpdateThreatTables(); } public void ValidateMovementInfo(MovementInfo mi) { @@ -2151,15 +2150,11 @@ namespace Game.Entities Pet pet = GetPet(); if (pet != null) - { pet.SetFaction(35); - pet.GetHostileRefManager().SetOnlineOfflineState(false); - } RemovePvpFlag(UnitPVPStateFlags.FFAPvp); ResetContestedPvP(); - GetHostileRefManager().SetOnlineOfflineState(false); CombatStopWithPets(); PhasingHandler.SetAlwaysVisible(this, true, false); @@ -2176,10 +2171,7 @@ namespace Game.Entities Pet pet = GetPet(); if (pet != null) - { pet.SetFaction(GetFaction()); - pet.GetHostileRefManager().SetOnlineOfflineState(true); - } // restore FFA PvP Server state if (Global.WorldMgr.IsFFAPvPRealm()) @@ -2188,7 +2180,6 @@ namespace Game.Entities // restore FFA PvP area state, remove not allowed for GM mounts UpdateArea(m_areaUpdateId); - GetHostileRefManager().SetOnlineOfflineState(true); m_serverSideVisibilityDetect.SetValue(ServerSideVisibilityType.GM, AccountTypes.Player); } @@ -3918,14 +3909,6 @@ namespace Game.Entities public static bool IsValidClass(Class _class) { return Convert.ToBoolean((1 << ((int)_class - 1)) & (int)Class.ClassMaskAllPlayable); } public static bool IsValidRace(Race _race) { return Convert.ToBoolean((ulong)SharedConst.GetMaskForRace(_race) & SharedConst.RaceMaskAllPlayable); } - public override void OnCombatExit() - { - base.OnCombatExit(); - - UpdatePotionCooldown(); - m_combatExitTime = Time.GetMSTime(); - } - void LeaveLFGChannel() { foreach (var i in m_channels) @@ -6887,7 +6870,6 @@ namespace Game.Entities m_taxi.ClearTaxiDestinations(); // not destinations, clear source node Dismount(); RemoveUnitFlag(UnitFlags.RemoveClientControl | UnitFlags.TaxiFlight); - GetHostileRefManager().SetOnlineOfflineState(true); } public void ContinueTaxiFlight() diff --git a/Source/Game/Entities/Unit/Unit.Combat.cs b/Source/Game/Entities/Unit/Unit.Combat.cs index 5c57ccc54..dee22b778 100644 --- a/Source/Game/Entities/Unit/Unit.Combat.cs +++ b/Source/Game/Entities/Unit/Unit.Combat.cs @@ -20,7 +20,6 @@ using Game.BattleFields; using Game.BattleGrounds; using Game.Combat; using Game.DataStorage; -using Game.Groups; using Game.Loots; using Game.Maps; using Game.Networking.Packets; @@ -34,50 +33,168 @@ namespace Game.Entities { public partial class Unit { - // Check if unit in combat with specific unit - public bool IsInCombatWith(Unit who) + public virtual void AtEnterCombat() { - // Check target exists - if (!who) - return false; + foreach (var pair in GetAppliedAuras()) + pair.Value.GetBase().CallScriptEnterLeaveCombatHandlers(pair.Value, true); - // Search in threat list - ObjectGuid guid = who.GetGUID(); - foreach (var refe in GetThreatManager().GetThreatList()) - { - // Return true if the unit matches - if (refe != null && refe.GetUnitGuid() == guid) - return true; - } + Spell spell = GetCurrentSpell(CurrentSpellTypes.Generic); + if (spell != null) + if (spell.GetState() == SpellState.Preparing + && spell.m_spellInfo.HasAttribute(SpellAttr0.CantUsedInCombat) + && spell.m_spellInfo.InterruptFlags.HasFlag(SpellInterruptFlags.Combat)) + InterruptNonMeleeSpells(false); - // Nothing found, false. - return false; + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.EnteringCombat); + ProcSkillsAndAuras(null, ProcFlags.EnterCombat, ProcFlags.None, ProcFlagsSpellType.MaskAll, ProcFlagsSpellPhase.None, ProcFlagsHit.None, null, null, null); } - public ThreatManager GetThreatManager() { return threatManager; } - - public bool CanDualWield() { return m_canDualWield; } - - public void SendChangeCurrentVictim(HostileReference pHostileReference) + public virtual void AtExitCombat() { - if (!GetThreatManager().IsThreatListEmpty()) - { - HighestThreatUpdate packet = new(); - packet.UnitGUID = GetGUID(); - packet.HighestThreatGUID = pHostileReference.GetUnitGuid(); + foreach (var pair in GetAppliedAuras()) + pair.Value.GetBase().CallScriptEnterLeaveCombatHandlers(pair.Value, false); - var refeList = GetThreatManager().GetThreatList(); - foreach (var refe in refeList) - { - ThreatInfo info = new(); - info.UnitGUID = refe.GetUnitGuid(); - info.Threat = (long)refe.GetThreat() * 100; - packet.ThreatList.Add(info); - } - SendMessageToSet(packet, false); + RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.LeavingCombat); + } + + public void CombatStop(bool includingCast = false, bool mutualPvP = true) + { + if (includingCast && IsNonMeleeSpellCast(false)) + InterruptNonMeleeSpells(false); + + AttackStop(); + RemoveAllAttackers(); + if (IsTypeId(TypeId.Player)) + ToPlayer().SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel + + if (mutualPvP) + ClearInCombat(); + else + { // vanish and brethren are weird + m_combatManager.EndAllPvECombat(); + m_combatManager.SuppressPvPCombat(); } } + public void CombatStopWithPets(bool includingCast = false) + { + CombatStop(includingCast); + + foreach (var minion in m_Controlled) + minion.CombatStop(includingCast); + } + + public bool IsInCombat() { return HasUnitFlag(UnitFlags.InCombat); } + + public bool IsInCombatWith(Unit who) { return who != null && m_combatManager.IsInCombatWith(who); } + + public bool IsPetInCombat() { return HasUnitFlag(UnitFlags.PetInCombat); } + + public void SetInCombatWith(Unit enemy) + { + if (enemy != null) + m_combatManager.SetInCombatWith(enemy); + } + + public void EngageWithTarget(Unit enemy) + { + if (enemy == null) + return; + + if (IsEngagedBy(enemy)) + return; + + if (CanHaveThreatList()) + m_threatManager.AddThreat(enemy, 0.0f, null, true, true); + else + SetInCombatWith(enemy); + + Creature creature = ToCreature(); + if (creature != null) + { + CreatureGroup formation = creature.GetFormation(); + if (formation != null) + formation.MemberEngagingTarget(creature, enemy); + } + } + + public void ClearInCombat() { m_combatManager.EndAllCombat(); } + + public void ClearInPetCombat() + { + RemoveUnitFlag(UnitFlags.PetInCombat); + Unit owner = GetOwner(); + if (owner != null) + owner.RemoveUnitFlag(UnitFlags.PetInCombat); + } + + public void RemoveAllAttackers() + { + while (!attackerList.Empty()) + { + var iter = attackerList.First(); + if (!iter.AttackStop()) + { + Log.outError(LogFilter.Unit, "WORLD: Unit has an attacker that isn't attacking it!"); + attackerList.Remove(iter); + } + } + } + + public virtual void OnCombatExit() + { + foreach (var pair in GetAppliedAuras()) + { + AuraApplication aurApp = pair.Value; + aurApp.GetBase().CallScriptEnterLeaveCombatHandlers(aurApp, false); + } + } + + public bool CanHaveThreatList() { return m_threatManager.CanHaveThreatList(); } + + void SendThreatListUpdate() { m_threatManager.SendThreatListToClients(); } + + // For NPCs with threat list: Whether there are any enemies on our threat list + // For other units: Whether we're in combat + // This value is different from IsInCombat when a projectile spell is midair (combat on launch - threat+aggro on impact) + public bool IsEngaged() { return CanHaveThreatList() ? m_threatManager.IsEngaged() : IsInCombat(); } + + public bool IsEngagedBy(Unit who) { return CanHaveThreatList() ? IsThreatenedBy(who) : IsInCombatWith(who); } + + public bool IsThreatenedBy(Unit who) { return who != null && m_threatManager.IsThreatenedBy(who, true); } + + public bool IsTargetableForAttack(bool checkFakeDeath = true) + { + if (!IsAlive()) + return false; + + if (HasUnitFlag(UnitFlags.NonAttackable | UnitFlags.NotSelectable)) + return false; + + if (IsTypeId(TypeId.Player) && ToPlayer().IsGameMaster()) + return false; + + return !HasUnitState(UnitState.Unattackable) && (!checkFakeDeath || !HasUnitState(UnitState.Died)); + } + + public void ValidateAttackersAndOwnTarget() + { + // iterate attackers + List toRemove = new(); + foreach (Unit attacker in GetAttackers()) + if (!attacker.IsValidAttackTarget(this)) + toRemove.Add(attacker); + + foreach (Unit attacker in toRemove) + attacker.AttackStop(); + + // remove our own victim + Unit victim = GetVictim(); + if (victim != null) + if (!IsValidAttackTarget(victim)) + AttackStop(); + } + public void StopAttackFaction(uint factionId) { Unit victim = GetVictim(); @@ -108,12 +225,19 @@ namespace Game.Entities ++i; } - GetHostileRefManager().DeleteReferencesForFaction(factionId); + List refsToEnd = new(); + foreach (var pair in m_combatManager.GetPvECombatRefs()) + if (pair.Value.GetOther(this).GetFactionTemplateEntry().Faction == factionId) + refsToEnd.Add(pair.Value); - foreach (var control in m_Controlled) - control.StopAttackFaction(factionId); + foreach (CombatReference refe in refsToEnd) + refe.EndCombat(); + + foreach (var minion in m_Controlled) + minion.StopAttackFaction(factionId); } + public void HandleProcExtraAttackFor(Unit victim) { while (ExtraAttacks != 0) @@ -123,253 +247,6 @@ namespace Game.Entities } } - public virtual void SetCanDualWield(bool value) { m_canDualWield = value; } - - public void SendClearThreatList() - { - ThreatClear packet = new(); - packet.UnitGUID = GetGUID(); - SendMessageToSet(packet, false); - } - - public void SendRemoveFromThreatList(HostileReference pHostileReference) - { - ThreatRemove packet = new(); - packet.UnitGUID = GetGUID(); - packet.AboutGUID = pHostileReference.GetUnitGuid(); - SendMessageToSet(packet, false); - } - - void SendThreatListUpdate() - { - if (!GetThreatManager().IsThreatListEmpty()) - { - ThreatUpdate packet = new(); - packet.UnitGUID = GetGUID(); - var tlist = GetThreatManager().GetThreatList(); - foreach (var refe in tlist) - { - ThreatInfo info = new(); - info.UnitGUID = refe.GetUnitGuid(); - info.Threat = (long)refe.GetThreat() * 100; - packet.ThreatList.Add(info); - } - SendMessageToSet(packet, false); - } - } - - public void TauntApply(Unit taunter) - { - Cypher.Assert(IsTypeId(TypeId.Unit)); - - if (!taunter || (taunter.IsTypeId(TypeId.Player) && taunter.ToPlayer().IsGameMaster())) - return; - - if (!CanHaveThreatList()) - return; - - Creature creature = ToCreature(); - - if (creature.HasReactState(ReactStates.Passive)) - return; - - Unit target = GetVictim(); - if (target && target == taunter) - return; - - if (!IsFocusing(null, true)) - SetInFront(taunter); - - if (creature.IsAIEnabled) - creature.GetAI().AttackStart(taunter); - } - - public void TauntFadeOut(Unit taunter) - { - Cypher.Assert(IsTypeId(TypeId.Unit)); - - if (!taunter || (taunter.IsTypeId(TypeId.Player) && taunter.ToPlayer().IsGameMaster())) - return; - - if (!CanHaveThreatList()) - return; - - Creature creature = ToCreature(); - - if (creature.HasReactState(ReactStates.Passive)) - return; - - Unit target = GetVictim(); - if (!target || target != taunter) - return; - - if (threatManager.IsThreatListEmpty()) - { - if (creature.IsAIEnabled) - creature.GetAI().EnterEvadeMode(EvadeReason.NoHostiles); - return; - } - - target = creature.SelectVictim(); // might have more taunt auras remaining - - if (target && target != taunter) - { - if (!IsFocusing(null, true)) - SetInFront(target); - if (creature.IsAIEnabled) - creature.GetAI().AttackStart(target); - } - } - - public void ValidateAttackersAndOwnTarget() - { - // iterate attackers - List toRemove = new(); - foreach (Unit attacker in GetAttackers()) - if (!attacker.IsValidAttackTarget(this)) - toRemove.Add(attacker); - - foreach (Unit attacker in toRemove) - attacker.AttackStop(); - - // remove our own victim - Unit victim = GetVictim(); - if (victim != null) - if (!IsValidAttackTarget(victim)) - AttackStop(); - } - - public void CombatStop(bool includingCast = false) - { - if (includingCast && IsNonMeleeSpellCast(false)) - InterruptNonMeleeSpells(false); - - AttackStop(); - RemoveAllAttackers(); - if (IsTypeId(TypeId.Player)) - ToPlayer().SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel - ClearInCombat(); - - // just in case - if (IsPetInCombat() && GetTypeId() != TypeId.Player) - ClearInPetCombat(); - } - public void CombatStopWithPets(bool includingCast = false) - { - CombatStop(includingCast); - - foreach (var control in m_Controlled) - control.CombatStop(includingCast); - } - public void ClearInCombat() - { - combatTimer = 0; - RemoveUnitFlag(UnitFlags.InCombat); - - // Player's state will be cleared in Player.UpdateContestedPvP - Creature creature = ToCreature(); - if (creature != null) - { - ClearUnitState(UnitState.AttackPlayer); - if (HasDynamicFlag(UnitDynFlags.Tapped)) - SetDynamicFlags((UnitDynFlags)creature.GetCreatureTemplate().DynamicFlags); - - if (creature.IsPet() || creature.IsGuardian()) - { - Unit owner = GetOwner(); - if (owner) - for (UnitMoveType i = 0; i < UnitMoveType.Max; ++i) - if (owner.GetSpeedRate(i) > GetSpeedRate(i)) - SetSpeedRate(i, owner.GetSpeedRate(i)); - } - else if (!IsCharmed()) - return; - } - - OnCombatExit(); - - RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.LeavingCombat); - } - - public void ClearInPetCombat() - { - RemoveUnitFlag(UnitFlags.PetInCombat); - Unit owner = GetOwner(); - if (owner != null) - owner.RemoveUnitFlag(UnitFlags.PetInCombat); - } - - public void RemoveAllAttackers() - { - while (!attackerList.Empty()) - { - var iter = attackerList.First(); - if (!iter.AttackStop()) - { - Log.outError(LogFilter.Unit, "WORLD: Unit has an attacker that isn't attacking it!"); - attackerList.Remove(iter); - } - } - } - - public void AddHatedBy(HostileReference pHostileReference) - { - hostileRefManager.InsertFirst(pHostileReference); - } - public void RemoveHatedBy(HostileReference pHostileReference) { } //nothing to do yet - - public float ApplyTotalThreatModifier(float fThreat, SpellSchoolMask schoolMask = SpellSchoolMask.Normal) - { - if (!HasAuraType(AuraType.ModThreat) || fThreat < 0) - return fThreat; - - SpellSchools school = Global.SpellMgr.GetFirstSchoolInMask(schoolMask); - - return fThreat * m_threatModifier[(int)school]; - } - - public virtual void OnCombatExit() - { - foreach (var pair in GetAppliedAuras()) - { - AuraApplication aurApp = pair.Value; - aurApp.GetBase().CallScriptEnterLeaveCombatHandlers(aurApp, false); - } - } - - public bool IsTargetableForAttack(bool checkFakeDeath = true) - { - if (!IsAlive()) - return false; - - if (HasUnitFlag(UnitFlags.NonAttackable | UnitFlags.NotSelectable)) - return false; - - if (IsTypeId(TypeId.Player) && ToPlayer().IsGameMaster()) - return false; - - return !HasUnitState(UnitState.Unattackable) && (!checkFakeDeath || !HasUnitState(UnitState.Died)); - } - - public DeathState GetDeathState() - { - return m_deathState; - } - - public bool IsEngaged() { return IsInCombat(); } - public bool IsEngagedBy(Unit who) { return IsInCombatWith(who); } - public void EngageWithTarget(Unit who) - { - SetInCombatWith(who); - who.SetInCombatWith(this); - GetThreatManager().AddThreat(who, 0.0f); - } - public bool IsThreatened() { return CanHaveThreatList() && !GetThreatManager().IsThreatListEmpty(); } - public bool IsThreatenedBy(Unit who) { return who != null && CanHaveThreatList() && GetThreatManager().IsThreatenedBy(who); } - - public bool IsInCombat() { return HasUnitFlag(UnitFlags.InCombat); } - public bool IsPetInCombat() { return HasUnitFlag(UnitFlags.PetInCombat); } - public bool Attack(Unit victim, bool meleeAttack) { if (victim == null || victim.GetGUID() == GetGUID()) @@ -453,21 +330,7 @@ namespace Game.Entities if (creature != null && !IsPet()) { - // should not let player enter combat by right clicking target - doesn't helps - GetThreatManager().AddThreat(victim, 0.0f); - SetInCombatWith(victim); - - if (victim.IsTypeId(TypeId.Player)) - victim.SetInCombatWith(this); - - Unit owner = victim.GetOwner(); - if (owner != null) - { - GetThreatManager().AddThreat(owner, 0.0f); - SetInCombatWith(owner); - if (owner.GetTypeId() == TypeId.Player) - owner.SetInCombatWith(this); - } + EngageWithTarget(victim); // ensure that anything we're attacking has threat creature.SendAIReaction(AiReaction.Hostile); creature.CallAssistance(); @@ -497,6 +360,7 @@ namespace Game.Entities } return true; } + public void SendMeleeAttackStart(Unit victim) { AttackStart packet = new(); @@ -504,6 +368,7 @@ namespace Game.Entities packet.Victim = victim.GetGUID(); SendMessageToSet(packet, true); } + public void SendMeleeAttackStop(Unit victim = null) { SendMessageToSet(new SAttackStop(this, victim), true); @@ -514,8 +379,11 @@ namespace Game.Entities else Log.outInfo(LogFilter.Unit, "{0} {1} stopped attacking", (IsTypeId(TypeId.Player) ? "Player" : "Creature"), GetGUID().ToString()); } + public ObjectGuid GetTarget() { return m_unitData.Target; } + public virtual void SetTarget(ObjectGuid guid) { } + public bool AttackStop() { if (attacking == null) @@ -549,18 +417,22 @@ namespace Game.Entities SendMeleeAttackStop(victim); return true; } + void _addAttacker(Unit pAttacker) { attackerList.Add(pAttacker); } + void _removeAttacker(Unit pAttacker) { attackerList.Remove(pAttacker); } + public Unit GetVictim() { return attacking; } + public Unit GetAttackerForHelper() { if (!IsEngaged()) @@ -568,65 +440,65 @@ namespace Game.Entities Unit victim = GetVictim(); if (victim != null) - if ((!IsPet() && GetPlayerMovingMe() == null) || IsInCombatWith(victim) || victim.IsInCombatWith(this)) + if ((!IsPet() && GetPlayerMovingMe() == null) || IsInCombatWith(victim)) return victim; - if (!attackerList.Empty()) - return attackerList[0]; + CombatManager mgr = GetCombatManager(); + // pick arbitrary targets; our pvp combat > owner's pvp combat > our pve combat > owner's pve combat + Unit owner = GetCharmerOrOwner(); + if (mgr.HasPvPCombat()) + return mgr.GetPvPCombatRefs().First().Value.GetOther(this); - Player owner = GetCharmerOrOwnerPlayerOrPlayerItself(); - if (owner != null) - { - HostileReference refe = owner.GetHostileRefManager().GetFirst(); - while (refe != null) - { - Unit hostile = refe.GetSource().GetOwner(); - if (hostile != null) - return hostile; + if (owner && (owner.GetCombatManager().HasPvPCombat())) + return owner.GetCombatManager().GetPvPCombatRefs().First().Value.GetOther(owner); - refe = refe.Next(); - } - } + if (mgr.HasPvECombat()) + return mgr.GetPvECombatRefs().First().Value.GetOther(this); + + if (owner && (owner.GetCombatManager().HasPvECombat())) + return owner.GetCombatManager().GetPvECombatRefs().First().Value.GetOther(owner); return null; } + public List GetAttackers() { return attackerList; } public override float GetCombatReach() { return m_unitData.CombatReach; } + public void SetCombatReach(float combatReach) { SetUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.CombatReach), combatReach); } + public float GetBoundingRadius() { return m_unitData.BoundingRadius; } + public void SetBoundingRadius(float boundingRadius) { SetUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.BoundingRadius), boundingRadius); } - public bool HaveOffhandWeapon() - { - if (IsTypeId(TypeId.Player)) - return ToPlayer().GetWeaponForAttack(WeaponAttackType.OffAttack, true) != null; - else - return m_canDualWield; - } public void ResetAttackTimer(WeaponAttackType type = WeaponAttackType.BaseAttack) { m_attackTimer[(int)type] = (uint)(GetBaseAttackTime(type) * m_modAttackSpeedPct[(int)type]); } + public void SetAttackTimer(WeaponAttackType type, uint time) { m_attackTimer[(int)type] = time; } + public uint GetAttackTimer(WeaponAttackType type) { return m_attackTimer[(int)type]; } + public bool IsAttackReady(WeaponAttackType type = WeaponAttackType.BaseAttack) { return m_attackTimer[(int)type] == 0; } + public uint GetBaseAttackTime(WeaponAttackType att) { return m_baseAttackSpeed[(int)att]; } + public void AttackerStateUpdate(Unit victim, WeaponAttackType attType = WeaponAttackType.BaseAttack, bool extra = false) { if (HasUnitState(UnitState.CannotAutoattack) || HasUnitFlag(UnitFlags.Pacified)) @@ -638,7 +510,7 @@ namespace Game.Entities if ((attType == WeaponAttackType.BaseAttack || attType == WeaponAttackType.OffAttack) && !IsWithinLOSInMap(victim)) return; - CombatStart(victim); + AttackedTarget(victim, true); RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Attacking); // ignore ranged case @@ -722,7 +594,7 @@ namespace Game.Entities if ((attType == WeaponAttackType.BaseAttack || attType == WeaponAttackType.OffAttack) && !IsWithinLOSInMap(victim)) return; - CombatStart(victim); + AttackedTarget(victim, true); RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Attacking); if (attType != WeaponAttackType.BaseAttack && attType != WeaponAttackType.OffAttack) @@ -757,8 +629,6 @@ namespace Game.Entities public void SetBaseWeaponDamage(WeaponAttackType attType, WeaponDamageRange damageRange, float value) { m_weaponDamage[(int)attType][(int)damageRange] = value; } - void StartReactiveTimer(ReactiveType reactive) { m_reactiveTimer[reactive] = 4000; } - public Unit GetMagicHitRedirectTarget(Unit victim, SpellInfo spellInfo) { // Patch 1.2 notes: Spell Reflection no longer reflects abilities @@ -795,6 +665,7 @@ namespace Game.Entities } return victim; } + public Unit GetMeleeHitRedirectTarget(Unit victim, SpellInfo spellInfo = null) { var interceptAuras = victim.GetAuraEffectsByType(AuraType.InterceptMeleeRangedAttacks); @@ -812,584 +683,12 @@ namespace Game.Entities } return victim; } + public bool IsValidAttackTarget(Unit target) { return _IsValidAttackTarget(target, null); } - void DealDamageMods(Unit victim, ref uint damage) - { - if (victim == null || !victim.IsAlive() || victim.HasUnitState(UnitState.InFlight) - || (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsInEvadeMode())) - { - damage = 0; - } - } - public void DealDamageMods(Unit victim, ref uint damage, ref uint absorb) - { - if (victim == null || !victim.IsAlive() || victim.HasUnitState(UnitState.InFlight) - || (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsEvadingAttacks())) - { - absorb += damage; - damage = 0; - return; - } - - damage = (uint)(damage * GetDamageMultiplierForTarget(victim)); - } - void DealMeleeDamage(CalcDamageInfo damageInfo, bool durabilityLoss) - { - Unit victim = damageInfo.target; - - if (!victim.IsAlive() || victim.HasUnitState(UnitState.InFlight) || (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsEvadingAttacks())) - return; - - // Hmmmm dont like this emotes client must by self do all animations - if (damageInfo.HitInfo.HasAnyFlag(HitInfo.CriticalHit)) - victim.HandleEmoteCommand(Emote.OneshotWoundCritical); - if (damageInfo.blocked_amount != 0 && damageInfo.TargetState != VictimState.Blocks) - victim.HandleEmoteCommand(Emote.OneshotParryShield); - - if (damageInfo.TargetState == VictimState.Parry && - (!IsTypeId(TypeId.Unit) || !ToCreature().GetCreatureTemplate().FlagsExtra.HasAnyFlag(CreatureFlagsExtra.NoParryHasten))) - { - // Get attack timers - float offtime = victim.GetAttackTimer(WeaponAttackType.OffAttack); - float basetime = victim.GetAttackTimer(WeaponAttackType.BaseAttack); - // Reduce attack time - if (victim.HaveOffhandWeapon() && offtime < basetime) - { - float percent20 = victim.GetBaseAttackTime(WeaponAttackType.OffAttack) * 0.20f; - float percent60 = 3.0f * percent20; - if (offtime > percent20 && offtime <= percent60) - victim.SetAttackTimer(WeaponAttackType.OffAttack, (uint)percent20); - else if (offtime > percent60) - { - offtime -= 2.0f * percent20; - victim.SetAttackTimer(WeaponAttackType.OffAttack, (uint)offtime); - } - } - else - { - float percent20 = victim.GetBaseAttackTime(WeaponAttackType.BaseAttack) * 0.20f; - float percent60 = 3.0f * percent20; - if (basetime > percent20 && basetime <= percent60) - victim.SetAttackTimer(WeaponAttackType.BaseAttack, (uint)percent20); - else if (basetime > percent60) - { - basetime -= 2.0f * percent20; - victim.SetAttackTimer(WeaponAttackType.BaseAttack, (uint)basetime); - } - } - } - - // Call default DealDamage - CleanDamage cleanDamage = new(damageInfo.cleanDamage, damageInfo.absorb, damageInfo.attackType, damageInfo.hitOutCome); - DealDamage(victim, damageInfo.damage, cleanDamage, DamageEffectType.Direct, (SpellSchoolMask)damageInfo.damageSchoolMask, null, durabilityLoss); - - // If this is a creature and it attacks from behind it has a probability to daze it's victim - if ((damageInfo.hitOutCome == MeleeHitOutcome.Crit || damageInfo.hitOutCome == MeleeHitOutcome.Crushing || damageInfo.hitOutCome == MeleeHitOutcome.Normal || damageInfo.hitOutCome == MeleeHitOutcome.Glancing) && - !IsTypeId(TypeId.Player) && !ToCreature().IsControlledByPlayer() && !victim.HasInArc(MathFunctions.PI, this) - && (victim.IsTypeId(TypeId.Player) || !victim.ToCreature().IsWorldBoss()) && !victim.IsVehicle()) - { - // 20% base chance - float chance = 20.0f; - - // there is a newbie protection, at level 10 just 7% base chance; assuming linear function - if (victim.GetLevel() < 30) - chance = 0.65f * victim.GetLevelForTarget(this) + 0.5f; - - uint victimDefense = victim.GetMaxSkillValueForLevel(this); - uint attackerMeleeSkill = GetMaxSkillValueForLevel(); - - chance *= attackerMeleeSkill / (float)victimDefense * 0.16f; - - // -probability is between 0% and 40% - MathFunctions.RoundToInterval(ref chance, 0.0f, 40.0f); - - if (RandomHelper.randChance(chance)) - CastSpell(victim, 1604, true); - } - - if (IsTypeId(TypeId.Player)) - { - DamageInfo dmgInfo = new(damageInfo); - ToPlayer().CastItemCombatSpell(dmgInfo); - } - - // Do effect if any damage done to target - if (damageInfo.damage != 0) - { - // We're going to call functions which can modify content of the list during iteration over it's elements - // Let's copy the list so we can prevent iterator invalidation - var vDamageShieldsCopy = victim.GetAuraEffectsByType(AuraType.DamageShield); - foreach (var dmgShield in vDamageShieldsCopy) - { - SpellInfo spellInfo = dmgShield.GetSpellInfo(); - - // Damage shield can be resisted... - var missInfo = victim.SpellHitResult(this, spellInfo, false); - if (missInfo != SpellMissInfo.None) - { - victim.SendSpellMiss(this, spellInfo.Id, missInfo); - continue; - } - - // ...or immuned - if (IsImmunedToDamage(spellInfo)) - { - victim.SendSpellDamageImmune(this, spellInfo.Id, false); - continue; - } - - uint damage = (uint)dmgShield.GetAmount(); - Unit caster = dmgShield.GetCaster(); - if (caster) - { - damage = caster.SpellDamageBonusDone(this, spellInfo, damage, DamageEffectType.SpellDirect, dmgShield.GetSpellEffectInfo()); - damage = SpellDamageBonusTaken(caster, spellInfo, damage, DamageEffectType.SpellDirect, dmgShield.GetSpellEffectInfo()); - } - - DamageInfo damageInfo1 = new(this, victim, damage, spellInfo, spellInfo.GetSchoolMask(), DamageEffectType.SpellDirect, WeaponAttackType.BaseAttack); - victim.CalcAbsorbResist(damageInfo1); - damage = damageInfo1.GetDamage(); - - victim.DealDamageMods(this, ref damage); - - SpellDamageShield damageShield = new(); - damageShield.Attacker = victim.GetGUID(); - damageShield.Defender = GetGUID(); - damageShield.SpellID = spellInfo.Id; - damageShield.TotalDamage = damage; - damageShield.OriginalDamage = (int)damageInfo.originalDamage; - damageShield.OverKill = (uint)Math.Max(damage - GetHealth(), 0); - damageShield.SchoolMask = (uint)spellInfo.SchoolMask; - damageShield.LogAbsorbed = damageInfo1.GetAbsorb(); - - victim.DealDamage(this, damage, null, DamageEffectType.SpellDirect, spellInfo.GetSchoolMask(), spellInfo, true); - damageShield.LogData.Initialize(this); - - victim.SendCombatLogMessage(damageShield); - } - } - } - public uint DealDamage(Unit victim, uint damage, CleanDamage cleanDamage = null, DamageEffectType damagetype = DamageEffectType.Direct, SpellSchoolMask damageSchoolMask = SpellSchoolMask.Normal, SpellInfo spellProto = null, bool durabilityLoss = true) - { - if (victim.IsAIEnabled) - victim.GetAI().DamageTaken(this, ref damage); - - if (IsAIEnabled) - GetAI().DamageDealt(victim, ref damage, damagetype); - - // Hook for OnDamage Event - Global.ScriptMgr.OnDamage(this, victim, ref damage); - - if (victim.IsTypeId(TypeId.Player) && this != victim) - { - // Signal to pets that their owner was attacked - except when DOT. - if (damagetype != DamageEffectType.DOT) - { - foreach (Unit controlled in victim.m_Controlled) - { - Creature cControlled = controlled.ToCreature(); - if (cControlled != null) - if (cControlled.IsAIEnabled) - cControlled.GetAI().OwnerAttackedBy(this); - } - } - - if (victim.ToPlayer().GetCommandStatus(PlayerCommandStates.God)) - return 0; - } - - if (damagetype != DamageEffectType.NoDamage) - { - // interrupting auras with SpellAuraInterruptFlags.Damage before checking !damage (absorbed damage breaks that type of auras) - if (spellProto != null) - { - if (!spellProto.HasAttribute(SpellAttr4.DamageDoesntBreakAuras)) - victim.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Damage, spellProto.Id); - } - else - victim.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Damage, 0); - - if (damage == 0 && damagetype != DamageEffectType.DOT && cleanDamage != null && cleanDamage.absorbed_damage != 0) - { - if (victim != this && victim.IsPlayer()) - { - Spell spell = victim.GetCurrentSpell(CurrentSpellTypes.Generic); - if (spell != null) - if (spell.GetState() == SpellState.Preparing && spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamageAbsorb)) - victim.InterruptNonMeleeSpells(false); - } - } - - // We're going to call functions which can modify content of the list during iteration over it's elements - // Let's copy the list so we can prevent iterator invalidation - var vCopyDamageCopy = victim.GetAuraEffectsByType(AuraType.ShareDamagePct); - // copy damage to casters of this aura - foreach (var aura in vCopyDamageCopy) - { - // Check if aura was removed during iteration - we don't need to work on such auras - if (!aura.GetBase().IsAppliedOnTarget(victim.GetGUID())) - continue; - - // check damage school mask - if ((aura.GetMiscValue() & (int)damageSchoolMask) == 0) - continue; - - Unit shareDamageTarget = aura.GetCaster(); - if (shareDamageTarget == null) - continue; - - SpellInfo spell = aura.GetSpellInfo(); - - uint share = MathFunctions.CalculatePct(damage, aura.GetAmount()); - - // @todo check packets if damage is done by victim, or by attacker of victim - DealDamageMods(shareDamageTarget, ref share); - DealDamage(shareDamageTarget, share, null, DamageEffectType.NoDamage, spell.GetSchoolMask(), spell, false); - } - } - - // Rage from Damage made (only from direct weapon damage) - if (cleanDamage != null && (cleanDamage.attackType == WeaponAttackType.BaseAttack || cleanDamage.attackType == WeaponAttackType.OffAttack) && damagetype == DamageEffectType.Direct && this != victim && GetPowerType() == PowerType.Rage) - { - uint rage = (uint)(GetBaseAttackTime(cleanDamage.attackType) / 1000.0f * 1.75f); - if (cleanDamage.attackType == WeaponAttackType.OffAttack) - rage /= 2; - RewardRage(rage); - } - - if (damage == 0) - return 0; - - Log.outDebug(LogFilter.Unit, "DealDamageStart"); - - uint health = (uint)victim.GetHealth(); - Log.outDebug(LogFilter.Unit, "Unit {0} dealt {1} damage to unit {2}", GetGUID(), damage, victim.GetGUID()); - - // duel ends when player has 1 or less hp - bool duel_hasEnded = false; - bool duel_wasMounted = false; - if (victim.IsTypeId(TypeId.Player) && victim.ToPlayer().duel != null && damage >= (health - 1)) - { - // prevent kill only if killed in duel and killed by opponent or opponent controlled creature - if (victim.ToPlayer().duel.opponent == this || victim.ToPlayer().duel.opponent.GetGUID() == GetOwnerGUID()) - damage = health - 1; - - duel_hasEnded = true; - } - else if (victim.IsVehicle() && damage >= (health - 1) && victim.GetCharmer() != null && victim.GetCharmer().IsTypeId(TypeId.Player)) - { - Player victimRider = victim.GetCharmer().ToPlayer(); - if (victimRider != null && victimRider.duel != null && victimRider.duel.isMounted) - { - // prevent kill only if killed in duel and killed by opponent or opponent controlled creature - if (victimRider.duel.opponent == this || victimRider.duel.opponent.GetGUID() == GetCharmerGUID()) - damage = health - 1; - - duel_wasMounted = true; - duel_hasEnded = true; - } - } - - if (IsTypeId(TypeId.Player) && this != victim) - { - Player killer = ToPlayer(); - - // in bg, count dmg if victim is also a player - if (victim.IsTypeId(TypeId.Player)) - { - Battleground bg = killer.GetBattleground(); - if (bg) - bg.UpdatePlayerScore(killer, ScoreType.DamageDone, damage); - } - - killer.UpdateCriteria(CriteriaTypes.DamageDone, health > damage ? damage : health, 0, 0, victim); - killer.UpdateCriteria(CriteriaTypes.HighestHitDealt, damage); - } - - if (victim.IsTypeId(TypeId.Player)) - { - victim.ToPlayer().UpdateCriteria(CriteriaTypes.HighestHitReceived, damage); - } - - damage /= (uint)victim.GetHealthMultiplierForTarget(this); - - if (victim.GetTypeId() != TypeId.Player && (!victim.IsControlledByPlayer() || victim.IsVehicle())) - { - if (!victim.ToCreature().HasLootRecipient()) - victim.ToCreature().SetLootRecipient(this); - - if (IsControlledByPlayer()) - victim.ToCreature().LowerPlayerDamageReq(health < damage ? health : damage); - } - - bool killed = false; - bool skipSettingDeathState = false; - - if (health <= damage) - { - killed = true; - - Log.outDebug(LogFilter.Unit, "DealDamage: victim just died"); - - if (victim.IsTypeId(TypeId.Player) && victim != this) - victim.ToPlayer().UpdateCriteria(CriteriaTypes.TotalDamageReceived, health); - - if (damagetype != DamageEffectType.NoDamage && damagetype != DamageEffectType.Self && victim.HasAuraType(AuraType.SchoolAbsorbOverkill)) - { - var vAbsorbOverkill = victim.GetAuraEffectsByType(AuraType.SchoolAbsorbOverkill); - DamageInfo damageInfo = new(this, victim, damage, spellProto, damageSchoolMask, damagetype, cleanDamage != null ? cleanDamage.attackType : WeaponAttackType.BaseAttack); - - foreach (var absorbAurEff in vAbsorbOverkill) - { - Aura baseAura = absorbAurEff.GetBase(); - AuraApplication aurApp = baseAura.GetApplicationOfTarget(victim.GetGUID()); - if (aurApp == null) - continue; - - if ((absorbAurEff.GetMiscValue() & (int)damageInfo.GetSchoolMask()) == 0) - continue; - - // cannot absorb over limit - if (damage >= victim.CountPctFromMaxHealth(100 + absorbAurEff.GetMiscValueB())) - continue; - - // get amount which can be still absorbed by the aura - int currentAbsorb = absorbAurEff.GetAmount(); - // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety - if (currentAbsorb < 0) - currentAbsorb = 0; - - uint tempAbsorb = (uint)currentAbsorb; - - // This aura type is used both by Spirit of Redemption (death not really prevented, must grant all credit immediately) and Cheat Death (death prevented) - // repurpose PreventDefaultAction for this - bool deathFullyPrevented = false; - - absorbAurEff.GetBase().CallScriptEffectAbsorbHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb, ref deathFullyPrevented); - currentAbsorb = (int)tempAbsorb; - - // absorb must be smaller than the damage itself - currentAbsorb = MathFunctions.RoundToInterval(ref currentAbsorb, 0, (int)damageInfo.GetDamage()); - damageInfo.AbsorbDamage((uint)currentAbsorb); - - if (deathFullyPrevented) - killed = false; - - skipSettingDeathState = true; - - if (currentAbsorb != 0) - { - SpellAbsorbLog absorbLog = new(); - absorbLog.Attacker = GetGUID(); - absorbLog.Victim = victim.GetGUID(); - absorbLog.Caster = baseAura.GetCasterGUID(); - absorbLog.AbsorbedSpellID = spellProto != null ? spellProto.Id : 0; - absorbLog.AbsorbSpellID = baseAura.GetId(); - absorbLog.Absorbed = currentAbsorb; - absorbLog.OriginalDamage = damageInfo.GetOriginalDamage(); - absorbLog.LogData.Initialize(victim); - SendCombatLogMessage(absorbLog); - } - } - - damage = damageInfo.GetDamage(); - } - } - - if (killed) - Kill(victim, durabilityLoss, skipSettingDeathState); - else - { - Log.outDebug(LogFilter.Unit, "DealDamageAlive"); - - if (victim.IsTypeId(TypeId.Player)) - victim.ToPlayer().UpdateCriteria(CriteriaTypes.TotalDamageReceived, damage); - - victim.ModifyHealth(-(int)damage); - - if (damagetype == DamageEffectType.Direct || damagetype == DamageEffectType.SpellDirect) - victim.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.NonPeriodicDamage, spellProto != null ? spellProto.Id : 0); - - if (!victim.IsTypeId(TypeId.Player)) - { - // Part of Evade mechanics. DoT's and Thorns / Retribution Aura do not contribute to this - if (damagetype != DamageEffectType.DOT && damage > 0 && !victim.GetOwnerGUID().IsPlayer() && (spellProto == null || !spellProto.HasAura(AuraType.DamageShield))) - victim.ToCreature().SetLastDamagedTime(GameTime.GetGameTime() + SharedConst.MaxAggroResetTime); - - victim.GetThreatManager().AddThreat(this, damage, spellProto); - } - else // victim is a player - { - // random durability for items (HIT TAKEN) - if (WorldConfig.GetFloatValue(WorldCfg.RateDurabilityLossDamage) > RandomHelper.randChance()) - { - byte slot = (byte)RandomHelper.IRand(0, EquipmentSlot.End - 1); - victim.ToPlayer().DurabilityPointLossForEquipSlot(slot); - } - } - - if (IsTypeId(TypeId.Player)) - { - // random durability for items (HIT DONE) - if (RandomHelper.randChance(WorldConfig.GetFloatValue(WorldCfg.RateDurabilityLossDamage))) - { - byte slot = (byte)RandomHelper.IRand(0, EquipmentSlot.End - 1); - ToPlayer().DurabilityPointLossForEquipSlot(slot); - } - } - - if (damagetype != DamageEffectType.NoDamage) - { - if (victim != this && (spellProto == null || !spellProto.HasAttribute(SpellAttr7.NoPushbackOnDamage))) - { - if (damagetype != DamageEffectType.DOT) - { - Spell spell = victim.GetCurrentSpell(CurrentSpellTypes.Generic); - if (spell != null) - { - if (spell.GetState() == SpellState.Preparing) - { - bool isCastInterrupted() - { - if (damage == 0) - return spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.ZeroDamageCancels); - - if (victim.IsPlayer() && spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamageCancelsPlayerOnly)) - return true; - - if (spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamageCancels)) - return true; - - return false; - }; - - bool isCastDelayed() - { - if (damage == 0) - return false; - - if (victim.IsPlayer() && spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamagePushbackPlayerOnly)) - return true; - - if (spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamagePushback)) - return true; - - return false; - } - - if (isCastInterrupted()) - victim.InterruptNonMeleeSpells(false); - else if (isCastDelayed()) - spell.Delayed(); - } - } - - if (damage != 0 && victim.IsPlayer()) - { - Spell spell1 = victim.GetCurrentSpell(CurrentSpellTypes.Channeled); - if (spell1 != null) - if (spell1.GetState() == SpellState.Casting && spell1.m_spellInfo.HasChannelInterruptFlag(SpellAuraInterruptFlags.DamageChannelDuration)) - spell1.DelayedChannel(); - } - } - } - } - - // last damage from duel opponent - if (duel_hasEnded) - { - Player he = duel_wasMounted ? victim.GetCharmer().ToPlayer() : victim.ToPlayer(); - - Cypher.Assert(he && he.duel != null); - - if (duel_wasMounted) // In this case victim==mount - victim.SetHealth(1); - else - he.SetHealth(1); - - he.duel.opponent.CombatStopWithPets(true); - he.CombatStopWithPets(true); - - he.CastSpell(he, 7267, true); // beg - he.DuelComplete(DuelCompleteType.Won); - } - } - - Log.outDebug(LogFilter.Unit, "DealDamageEnd returned {0} damage", damage); - - return damage; - } - - public long ModifyHealth(long dVal) - { - long gain = 0; - - if (dVal == 0) - return 0; - - long curHealth = (long)GetHealth(); - - long val = dVal + curHealth; - if (val <= 0) - { - SetHealth(0); - return -curHealth; - } - - long maxHealth = (long)GetMaxHealth(); - if (val < maxHealth) - { - SetHealth((ulong)val); - gain = val - curHealth; - } - else if (curHealth != maxHealth) - { - SetHealth((ulong)maxHealth); - gain = maxHealth - curHealth; - } - - if (dVal < 0) - { - HealthUpdate packet = new(); - packet.Guid = GetGUID(); - packet.Health = (long)GetHealth(); - - Player player = GetCharmerOrOwnerPlayerOrPlayerItself(); - if (player) - player.SendPacket(packet); - } - - return gain; - } - public long GetHealthGain(long dVal) - { - long gain = 0; - - if (dVal == 0) - return 0; - - long curHealth = (long)GetHealth(); - - long val = dVal + curHealth; - if (val <= 0) - { - return -curHealth; - } - - long maxHealth = (long)GetMaxHealth(); - - if (val < maxHealth) - gain = dVal; - else if (curHealth != maxHealth) - gain = maxHealth - curHealth; - - return gain; - } - public void SendAttackStateUpdate(HitInfo HitInfo, Unit target, SpellSchoolMask damageSchoolMask, uint Damage, uint AbsorbDamage, uint Resist, VictimState TargetState, uint BlockedAmount) { CalcDamageInfo dmgInfo = new(); @@ -1405,6 +704,7 @@ namespace Game.Entities dmgInfo.blocked_amount = BlockedAmount; SendAttackStateUpdate(dmgInfo); } + public void SendAttackStateUpdate(CalcDamageInfo damageInfo) { AttackerStateUpdate packet = new(); @@ -1434,53 +734,28 @@ namespace Game.Entities SendCombatLogMessage(packet); } - public void CombatStart(Unit target, bool initialAggro = true) + + public void AttackedTarget(Unit target, bool canInitialAggro = true) { - if (initialAggro) - { - if (!target.IsStandState()) - target.SetStandState(UnitStandStateType.Stand); - - if (!target.IsInCombat() && !target.IsTypeId(TypeId.Player) - && !target.ToCreature().HasReactState(ReactStates.Passive) && target.ToCreature().IsAIEnabled) - target.ToCreature().GetAI().AttackStart(this); - - SetInCombatWith(target); - target.SetInCombatWith(this); - } - - Player me = GetCharmerOrOwnerPlayerOrPlayerItself(); - Unit who = target.GetCharmerOrOwnerOrSelf(); - if (me != null && who.IsTypeId(TypeId.Player)) - me.SetContestedPvP(who.ToPlayer()); - - if (me != null && who.IsPvP() && (!who.IsTypeId(TypeId.Player) || me.duel == null || me.duel.opponent != who)) - { - me.UpdatePvP(true); - me.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.PvPActive); - } - } - public void SetInCombatWith(Unit enemy) - { - Unit eOwner = enemy.GetCharmerOrOwnerOrSelf(); - if (eOwner.IsPvP()) - { - SetInCombatState(true, enemy); + if (!target.IsEngaged() && !canInitialAggro) return; - } - // check for duel - if (eOwner.IsTypeId(TypeId.Player) && eOwner.ToPlayer().duel != null) + target.EngageWithTarget(this); + + Unit targetOwner = target.GetCharmerOrOwner(); + if (targetOwner != null) + targetOwner.EngageWithTarget(this); + + Player myPlayerOwner = GetCharmerOrOwnerPlayerOrPlayerItself(); + Player targetPlayerOwner = target.GetCharmerOrOwnerPlayerOrPlayerItself(); + if (myPlayerOwner && targetPlayerOwner && !(myPlayerOwner.duel != null && myPlayerOwner.duel.opponent == targetPlayerOwner)) { - Unit myOwner = GetCharmerOrOwnerOrSelf(); - if (((Player)eOwner).duel.opponent == myOwner) - { - SetInCombatState(true, enemy); - return; - } + myPlayerOwner.UpdatePvP(true); + myPlayerOwner.SetContestedPvP(targetPlayerOwner); + myPlayerOwner.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.PvPActive); } - SetInCombatState(false, enemy); } + public void SetInCombatState(bool PvP, Unit enemy = null) { // only alive units can be in combat @@ -1557,6 +832,11 @@ namespace Game.Entities Cell.VisitWorldObjects(this, notifier, GetVisibilityRange()); } + bool IsThreatened() + { + return !m_threatManager.IsThreatListEmpty(); + } + public void Kill(Unit victim, bool durabilityLoss = true, bool skipSettingDeathState = false) { // Prevent killing unit twice (and giving reward from kill twice) @@ -1689,7 +969,6 @@ namespace Game.Entities } // 10% durability loss on death - // clean InHateListOf Player plrVictim = victim.ToPlayer(); if (plrVictim != null) { @@ -1838,131 +1117,6 @@ namespace Game.Entities public void KillSelf(bool durabilityLoss = true, bool skipSettingDeathState = false) { Kill(this, durabilityLoss, skipSettingDeathState); } - public virtual float GetBlockPercent(uint attackerLevel) { return 30.0f; } - - void UpdateReactives(uint p_time) - { - for (ReactiveType reactive = 0; reactive < ReactiveType.Max; ++reactive) - { - if (!m_reactiveTimer.ContainsKey(reactive)) - continue; - - if (m_reactiveTimer[reactive] <= p_time) - { - m_reactiveTimer[reactive] = 0; - - switch (reactive) - { - case ReactiveType.Defense: - if (HasAuraState(AuraStateType.Defensive)) - ModifyAuraState(AuraStateType.Defensive, false); - break; - case ReactiveType.Defense2: - if (HasAuraState(AuraStateType.Defensive2)) - ModifyAuraState(AuraStateType.Defensive2, false); - break; - } - } - else - { - m_reactiveTimer[reactive] -= p_time; - } - } - } - - public void RewardRage(uint baseRage) - { - float addRage = baseRage; - - // talent who gave more rage on attack - MathFunctions.AddPct(ref addRage, GetTotalAuraModifier(AuraType.ModRageFromDamageDealt)); - - addRage *= WorldConfig.GetFloatValue(WorldCfg.RatePowerRageIncome); - - ModifyPower(PowerType.Rage, (int)(addRage * 10)); - } - - public float GetPPMProcChance(uint WeaponSpeed, float PPM, SpellInfo spellProto) - { - // proc per minute chance calculation - if (PPM <= 0) - return 0.0f; - - // Apply chance modifer aura - if (spellProto != null) - { - Player modOwner = GetSpellModOwner(); - if (modOwner != null) - modOwner.ApplySpellMod(spellProto, SpellModOp.ProcFrequency, ref PPM); - } - - return (float)Math.Floor((WeaponSpeed * PPM) / 600.0f); // result is chance in percents (probability = Speed_in_sec * (PPM / 60)) - } - - public Unit GetNextRandomRaidMemberOrPet(float radius) - { - Player player = null; - if (IsTypeId(TypeId.Player)) - player = ToPlayer(); - // Should we enable this also for charmed units? - else if (IsTypeId(TypeId.Unit) && IsPet()) - player = GetOwner().ToPlayer(); - - if (player == null) - return null; - Group group = player.GetGroup(); - // When there is no group check pet presence - if (!group) - { - // We are pet now, return owner - if (player != this) - return IsWithinDistInMap(player, radius) ? player : null; - Unit pet = GetGuardianPet(); - // No pet, no group, nothing to return - if (pet == null) - return null; - // We are owner now, return pet - return IsWithinDistInMap(pet, radius) ? pet : null; - } - - List nearMembers = new(); - // reserve place for players and pets because resizing vector every unit push is unefficient (vector is reallocated then) - - for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) - { - Player target = refe.GetSource(); - if (target) - { - // IsHostileTo check duel and controlled by enemy - if (target != this && IsWithinDistInMap(target, radius) && target.IsAlive() && !IsHostileTo(target)) - nearMembers.Add(target); - - // Push player's pet to vector - Unit pet = target.GetGuardianPet(); - if (pet) - if (pet != this && IsWithinDistInMap(pet, radius) && pet.IsAlive() && !IsHostileTo(pet)) - nearMembers.Add(pet); - } - } - - if (nearMembers.Empty()) - return null; - - int randTarget = RandomHelper.IRand(0, nearMembers.Count - 1); - return nearMembers[randTarget]; - } - - public void ClearAllReactives() - { - for (ReactiveType i = 0; i < ReactiveType.Max; ++i) - m_reactiveTimer[i] = 0; - - if (HasAuraState(AuraStateType.Defensive)) - ModifyAuraState(AuraStateType.Defensive, false); - if (HasAuraState(AuraStateType.Defensive2)) - ModifyAuraState(AuraStateType.Defensive2, false); - } - public virtual bool CanUseAttackType(WeaponAttackType attacktype) { switch (attacktype) @@ -2162,6 +1316,7 @@ namespace Game.Entities else // Impossible get negative result but.... damageInfo.damage = 0; } + MeleeHitOutcome RollMeleeOutcomeAgainst(Unit victim, WeaponAttackType attType) { if (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsEvadingAttacks()) @@ -2273,8 +1428,9 @@ namespace Game.Entities // 8. HIT return MeleeHitOutcome.Normal; } + public uint CalculateDamage(WeaponAttackType attType, bool normalized, bool addTotalPct) - { + { float minDamage; float maxDamage; @@ -2331,6 +1487,7 @@ namespace Game.Entities return RandomHelper.URand(minDamage, maxDamage); } + public float GetWeaponDamageRange(WeaponAttackType attType, WeaponDamageRange type) { if (attType == WeaponAttackType.OffAttack && !HaveOffhandWeapon()) @@ -2338,6 +1495,7 @@ namespace Game.Entities return m_weaponDamage[(int)attType][(int)type]; } + public float GetAPMultiplier(WeaponAttackType attType, bool normalized) { if (!IsTypeId(TypeId.Player) || (IsInFeralForm() && !normalized)) @@ -2375,6 +1533,7 @@ namespace Game.Entities return weapon.GetTemplate().GetDelay() / 1000.0f; } } + public float GetTotalAttackPowerValue(WeaponAttackType attType, bool includeWeapon = true) { if (attType == WeaponAttackType.RangedAttack) @@ -2404,6 +1563,7 @@ namespace Game.Entities return ap * (1.0f + m_unitData.AttackPowerMultiplier); } } + public bool IsWithinMeleeRange(Unit obj) { if (!obj || !IsInMap(obj) || !IsInPhase(obj)) @@ -2430,6 +1590,7 @@ namespace Game.Entities m_baseAttackSpeed[(int)att] = val; UpdateAttackTimeField(att); } + void UpdateAttackTimeField(WeaponAttackType att) { switch (att) @@ -2445,766 +1606,6 @@ namespace Game.Entities break; ; } } - public virtual void SetPvP(bool state) - { - if (state) - AddPvpFlag(UnitPVPStateFlags.PvP); - else - RemovePvpFlag(UnitPVPStateFlags.PvP); - } - - public bool CanHaveThreatList(bool skipAliveCheck = false) - { - // only creatures can have threat list - if (!IsTypeId(TypeId.Unit)) - return false; - - // only alive units can have threat list - if (!skipAliveCheck && !IsAlive()) - return false; - - // totems can not have threat list - if (IsTotem()) - return false; - - // summons can not have a threat list, unless they are controlled by a creature - if (HasUnitTypeMask(UnitTypeMask.Minion | UnitTypeMask.Guardian | UnitTypeMask.ControlableGuardian) && GetOwnerGUID().IsPlayer()) - return false; - - return true; - } - - uint CalcSpellResistedDamage(Unit attacker, Unit victim, uint damage, SpellSchoolMask schoolMask, SpellInfo spellInfo) - { - // Magic damage, check for resists - if (!Convert.ToBoolean(schoolMask & SpellSchoolMask.Magic)) - return 0; - - // Npcs can have holy resistance - if (schoolMask.HasAnyFlag(SpellSchoolMask.Holy) && victim.GetTypeId() != TypeId.Unit) - return 0; - - // Ignore spells that can't be resisted - if (spellInfo != null) - { - if (spellInfo.HasAttribute(SpellAttr4.IgnoreResistances)) - return 0; - - // Binary spells can't have damage part resisted - if (spellInfo.HasAttribute(SpellCustomAttributes.BinarySpell)) - return 0; - } - - float averageResist = CalculateAverageResistReduction(schoolMask, victim, spellInfo); - - float[] discreteResistProbability = new float[11]; - if (averageResist <= 0.1f) - { - discreteResistProbability[0] = 1.0f - 7.5f * averageResist; - discreteResistProbability[1] = 5.0f * averageResist; - discreteResistProbability[2] = 2.5f * averageResist; - } - else - { - for (uint i = 0; i < 11; ++i) - discreteResistProbability[i] = Math.Max(0.5f - 2.5f * Math.Abs(0.1f * i - averageResist), 0.0f); - } - - float roll = (float)RandomHelper.NextDouble(); - float probabilitySum = 0.0f; - - uint resistance = 0; - for (; resistance < 11; ++resistance) - if (roll < (probabilitySum += discreteResistProbability[resistance])) - break; - - float damageResisted = damage * resistance / 10f; - if (damageResisted > 0.0f) // if any damage was resisted - { - int ignoredResistance = 0; - - ignoredResistance += GetTotalAuraModifierByMiscMask(AuraType.ModIgnoreTargetResist, (int)schoolMask); - - ignoredResistance = Math.Min(ignoredResistance, 100); - MathFunctions.ApplyPct(ref damageResisted, 100 - ignoredResistance); - - // Spells with melee and magic school mask, decide whether resistance or armor absorb is higher - if (spellInfo != null && spellInfo.HasAttribute(SpellCustomAttributes.SchoolmaskNormalWithMagic)) - { - uint damageAfterArmor = CalcArmorReducedDamage(attacker, victim, damage, spellInfo, WeaponAttackType.BaseAttack); - float armorReduction = damage - damageAfterArmor; - - // pick the lower one, the weakest resistance counts - damageResisted = Math.Min(damageResisted, armorReduction); - } - } - - damageResisted = Math.Max(damageResisted, 0.0f); - return (uint)damageResisted; - } - - float CalculateAverageResistReduction(SpellSchoolMask schoolMask, Unit victim, SpellInfo spellInfo) - { - float victimResistance = (float)victim.GetResistance(schoolMask); - - // pets inherit 100% of masters penetration - // excluding traps - Player player = GetSpellModOwner(); - if (player != null && GetEntry() != SharedConst.WorldTrigger) - { - victimResistance += (float)player.GetTotalAuraModifierByMiscMask(AuraType.ModTargetResistance, (int)schoolMask); - victimResistance -= (float)player.GetSpellPenetrationItemMod(); - } - else - victimResistance += (float)GetTotalAuraModifierByMiscMask(AuraType.ModTargetResistance, (int)schoolMask); - - // holy resistance exists in pve and comes from level difference, ignore template values - if (schoolMask.HasAnyFlag(SpellSchoolMask.Holy)) - victimResistance = 0.0f; - - // Chaos Bolt exception, ignore all target resistances (unknown attribute?) - if (spellInfo != null && spellInfo.SpellFamilyName == SpellFamilyNames.Warlock && spellInfo.Id == 116858) - victimResistance = 0.0f; - - victimResistance = Math.Max(victimResistance, 0.0f); - - // level-based resistance does not apply to binary spells, and cannot be overcome by spell penetration - if (spellInfo == null || !spellInfo.HasAttribute(SpellCustomAttributes.BinarySpell)) - victimResistance += Math.Max(((float)victim.GetLevelForTarget(this) - (float)GetLevelForTarget(victim)) * 5.0f, 0.0f); - - uint bossLevel = 83; - float bossResistanceConstant = 510.0f; - uint level = victim.GetLevelForTarget(this); - float resistanceConstant; - - if (level == bossLevel) - resistanceConstant = bossResistanceConstant; - else - resistanceConstant = level * 5.0f; - - return victimResistance / (victimResistance + resistanceConstant); - } - - public void CalcAbsorbResist(DamageInfo damageInfo) - { - if (!damageInfo.GetVictim() || !damageInfo.GetVictim().IsAlive() || damageInfo.GetDamage() == 0) - return; - - uint resistedDamage = CalcSpellResistedDamage(damageInfo.GetAttacker(), damageInfo.GetVictim(), damageInfo.GetDamage(), damageInfo.GetSchoolMask(), damageInfo.GetSpellInfo()); - damageInfo.ResistDamage(resistedDamage); - - // Ignore Absorption Auras - float auraAbsorbMod = GetMaxPositiveAuraModifierByMiscMask(AuraType.ModTargetAbsorbSchool, (uint)damageInfo.GetSchoolMask()); - - MathFunctions.RoundToInterval(ref auraAbsorbMod, 0.0f, 100.0f); - - int absorbIgnoringDamage = (int)MathFunctions.CalculatePct(damageInfo.GetDamage(), auraAbsorbMod); - damageInfo.ModifyDamage(-absorbIgnoringDamage); - - // We're going to call functions which can modify content of the list during iteration over it's elements - // Let's copy the list so we can prevent iterator invalidation - var vSchoolAbsorbCopy = damageInfo.GetVictim().GetAuraEffectsByType(AuraType.SchoolAbsorb); - vSchoolAbsorbCopy.Sort(new AbsorbAuraOrderPred()); - - // absorb without mana cost - for (var i = 0; i < vSchoolAbsorbCopy.Count; ++i) - { - var absorbAurEff = vSchoolAbsorbCopy[i]; - if (damageInfo.GetDamage() == 0) - break; - - // Check if aura was removed during iteration - we don't need to work on such auras - AuraApplication aurApp = absorbAurEff.GetBase().GetApplicationOfTarget(damageInfo.GetVictim().GetGUID()); - if (aurApp == null) - continue; - if (!Convert.ToBoolean(absorbAurEff.GetMiscValue() & (int)damageInfo.GetSchoolMask())) - continue; - - // get amount which can be still absorbed by the aura - int currentAbsorb = absorbAurEff.GetAmount(); - // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety - if (currentAbsorb < 0) - currentAbsorb = 0; - - uint tempAbsorb = (uint)currentAbsorb; - - bool defaultPrevented = false; - - absorbAurEff.GetBase().CallScriptEffectAbsorbHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb, ref defaultPrevented); - currentAbsorb = (int)tempAbsorb; - - if (!defaultPrevented) - { - - // absorb must be smaller than the damage itself - currentAbsorb = MathFunctions.RoundToInterval(ref currentAbsorb, 0, damageInfo.GetDamage()); - - damageInfo.AbsorbDamage((uint)currentAbsorb); - - tempAbsorb = (uint)currentAbsorb; - absorbAurEff.GetBase().CallScriptEffectAfterAbsorbHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb); - - // Check if our aura is using amount to count damage - if (absorbAurEff.GetAmount() >= 0) - { - // Reduce shield amount - absorbAurEff.ChangeAmount(absorbAurEff.GetAmount() - currentAbsorb); - // Aura cannot absorb anything more - remove it - if (absorbAurEff.GetAmount() <= 0) - absorbAurEff.GetBase().Remove(AuraRemoveMode.EnemySpell); - } - } - - if (currentAbsorb != 0) - { - SpellAbsorbLog absorbLog = new(); - absorbLog.Attacker = damageInfo.GetAttacker().GetGUID(); - absorbLog.Victim = damageInfo.GetVictim().GetGUID(); - absorbLog.Caster = absorbAurEff.GetBase().GetCasterGUID(); - absorbLog.AbsorbedSpellID = damageInfo.GetSpellInfo() != null ? damageInfo.GetSpellInfo().Id : 0; - absorbLog.AbsorbSpellID = absorbAurEff.GetId(); - absorbLog.Absorbed = currentAbsorb; - absorbLog.OriginalDamage = damageInfo.GetOriginalDamage(); - absorbLog.LogData.Initialize(damageInfo.GetVictim()); - SendCombatLogMessage(absorbLog); - } - } - - // absorb by mana cost - var vManaShieldCopy = damageInfo.GetVictim().GetAuraEffectsByType(AuraType.ManaShield); - foreach (var absorbAurEff in vManaShieldCopy) - { - if (damageInfo.GetDamage() == 0) - break; - - // Check if aura was removed during iteration - we don't need to work on such auras - AuraApplication aurApp = absorbAurEff.GetBase().GetApplicationOfTarget(damageInfo.GetVictim().GetGUID()); - if (aurApp == null) - continue; - // check damage school mask - if (!Convert.ToBoolean(absorbAurEff.GetMiscValue() & (int)damageInfo.GetSchoolMask())) - continue; - - // get amount which can be still absorbed by the aura - int currentAbsorb = absorbAurEff.GetAmount(); - // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety - if (currentAbsorb < 0) - currentAbsorb = 0; - - uint tempAbsorb = (uint)currentAbsorb; - - bool defaultPrevented = false; - - absorbAurEff.GetBase().CallScriptEffectManaShieldHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb, ref defaultPrevented); - currentAbsorb = (int)tempAbsorb; - - if (!defaultPrevented) - { - // absorb must be smaller than the damage itself - currentAbsorb = MathFunctions.RoundToInterval(ref currentAbsorb, 0, damageInfo.GetDamage()); - - int manaReduction = currentAbsorb; - - // lower absorb amount by talents - float manaMultiplier = absorbAurEff.GetSpellEffectInfo().CalcValueMultiplier(absorbAurEff.GetCaster()); - if (manaMultiplier != 0) - manaReduction = (int)(manaReduction * manaMultiplier); - - int manaTaken = -damageInfo.GetVictim().ModifyPower(PowerType.Mana, -manaReduction); - - // take case when mana has ended up into account - currentAbsorb = currentAbsorb != 0 ? (currentAbsorb * (manaTaken / manaReduction)) : 0; - - damageInfo.AbsorbDamage((uint)currentAbsorb); - - tempAbsorb = (uint)currentAbsorb; - absorbAurEff.GetBase().CallScriptEffectAfterManaShieldHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb); - - // Check if our aura is using amount to count damage - if (absorbAurEff.GetAmount() >= 0) - { - absorbAurEff.ChangeAmount(absorbAurEff.GetAmount() - currentAbsorb); - if ((absorbAurEff.GetAmount() <= 0)) - absorbAurEff.GetBase().Remove(AuraRemoveMode.EnemySpell); - } - } - - if (currentAbsorb != 0) - { - SpellAbsorbLog absorbLog = new(); - absorbLog.Attacker = damageInfo.GetAttacker().GetGUID(); - absorbLog.Victim = damageInfo.GetVictim().GetGUID(); - absorbLog.Caster = absorbAurEff.GetBase().GetCasterGUID(); - absorbLog.AbsorbedSpellID = damageInfo.GetSpellInfo() != null ? damageInfo.GetSpellInfo().Id : 0; - absorbLog.AbsorbSpellID = absorbAurEff.GetId(); - absorbLog.Absorbed = currentAbsorb; - absorbLog.OriginalDamage = damageInfo.GetOriginalDamage(); - absorbLog.LogData.Initialize(damageInfo.GetVictim()); - SendCombatLogMessage(absorbLog); - } - } - - damageInfo.ModifyDamage(absorbIgnoringDamage); - - // split damage auras - only when not damaging self - if (damageInfo.GetVictim() != this) - { - // We're going to call functions which can modify content of the list during iteration over it's elements - // Let's copy the list so we can prevent iterator invalidation - var vSplitDamagePctCopy = damageInfo.GetVictim().GetAuraEffectsByType(AuraType.SplitDamagePct); - foreach (var itr in vSplitDamagePctCopy) - { - if (damageInfo.GetDamage() == 0) - break; - - // Check if aura was removed during iteration - we don't need to work on such auras - AuraApplication aurApp = itr.GetBase().GetApplicationOfTarget(damageInfo.GetVictim().GetGUID()); - if (aurApp == null) - continue; - - // check damage school mask - if (!Convert.ToBoolean(itr.GetMiscValue() & (int)damageInfo.GetSchoolMask())) - continue; - - // Damage can be splitted only if aura has an alive caster - Unit caster = itr.GetCaster(); - if (!caster || (caster == damageInfo.GetVictim()) || !caster.IsInWorld || !caster.IsAlive()) - continue; - - uint splitDamage = MathFunctions.CalculatePct(damageInfo.GetDamage(), itr.GetAmount()); - - itr.GetBase().CallScriptEffectSplitHandlers(itr, aurApp, damageInfo, splitDamage); - - // absorb must be smaller than the damage itself - splitDamage = MathFunctions.RoundToInterval(ref splitDamage, 0, damageInfo.GetDamage()); - - damageInfo.AbsorbDamage(splitDamage); - - // check if caster is immune to damage - if (caster.IsImmunedToDamage(damageInfo.GetSchoolMask())) - { - damageInfo.GetVictim().SendSpellMiss(caster, itr.GetSpellInfo().Id, SpellMissInfo.Immune); - continue; - } - - uint split_absorb = 0; - DealDamageMods(caster, ref splitDamage, ref split_absorb); - - SpellNonMeleeDamage log = new(this, caster, itr.GetSpellInfo(), itr.GetBase().GetSpellVisual(), damageInfo.GetSchoolMask(), itr.GetBase().GetCastGUID()); - CleanDamage cleanDamage = new(splitDamage, 0, WeaponAttackType.BaseAttack, MeleeHitOutcome.Normal); - DealDamage(caster, splitDamage, cleanDamage, DamageEffectType.Direct, damageInfo.GetSchoolMask(), itr.GetSpellInfo(), false); - log.damage = splitDamage; - log.originalDamage = splitDamage; - log.absorb = split_absorb; - SendSpellNonMeleeDamageLog(log); - - // break 'Fear' and similar auras - ProcSkillsAndAuras(caster, ProcFlags.None, ProcFlags.TakenSpellMagicDmgClassNeg, ProcFlagsSpellType.Damage, ProcFlagsSpellPhase.Hit, ProcFlagsHit.None, null, damageInfo, null); - } - } - } - - public void CalcHealAbsorb(HealInfo healInfo) - { - if (healInfo.GetHeal() == 0) - return; - - // Need remove expired auras after - bool existExpired = false; - - // absorb without mana cost - var vHealAbsorb = healInfo.GetTarget().GetAuraEffectsByType(AuraType.SchoolHealAbsorb); - for (var i = 0; i < vHealAbsorb.Count; ++i) - { - var eff = vHealAbsorb[i]; - if (healInfo.GetHeal() <= 0) - break; - - if (!Convert.ToBoolean(eff.GetMiscValue() & (int)healInfo.GetSpellInfo().SchoolMask)) - continue; - - // Max Amount can be absorbed by this aura - int currentAbsorb = eff.GetAmount(); - - // Found empty aura (impossible but..) - if (currentAbsorb <= 0) - { - existExpired = true; - continue; - } - - // currentAbsorb - damage can be absorbed by shield - // If need absorb less damage - currentAbsorb = (int)Math.Min(healInfo.GetHeal(), currentAbsorb); - - healInfo.AbsorbHeal((uint)currentAbsorb); - - // Reduce shield amount - eff.ChangeAmount(eff.GetAmount() - currentAbsorb); - // Need remove it later - if (eff.GetAmount() <= 0) - existExpired = true; - } - - // Remove all expired absorb auras - if (existExpired) - { - for (var i = 0; i < vHealAbsorb.Count;) - { - AuraEffect auraEff = vHealAbsorb[i]; - ++i; - if (auraEff.GetAmount() <= 0) - { - uint removedAuras = healInfo.GetTarget().m_removedAurasCount; - auraEff.GetBase().Remove(AuraRemoveMode.EnemySpell); - if (removedAuras + 1 < healInfo.GetTarget().m_removedAurasCount) - i = 0; - } - } - } - } - - public uint CalcArmorReducedDamage(Unit attacker, Unit victim, uint damage, SpellInfo spellInfo, WeaponAttackType attackType = WeaponAttackType.Max) - { - float armor = victim.GetArmor(); - - armor *= victim.GetArmorMultiplierForTarget(attacker); - - // bypass enemy armor by SPELL_AURA_BYPASS_ARMOR_FOR_CASTER - int armorBypassPct = 0; - var reductionAuras = victim.GetAuraEffectsByType(AuraType.BypassArmorForCaster); - foreach (var eff in reductionAuras) - if (eff.GetCasterGUID() == GetGUID()) - armorBypassPct += eff.GetAmount(); - armor = MathFunctions.CalculatePct(armor, 100 - Math.Min(armorBypassPct, 100)); - - // Ignore enemy armor by SPELL_AURA_MOD_TARGET_RESISTANCE aura - armor += GetTotalAuraModifierByMiscMask(AuraType.ModTargetResistance, (int)SpellSchoolMask.Normal); - - if (spellInfo != null) - { - Player modOwner = GetSpellModOwner(); - if (modOwner != null) - modOwner.ApplySpellMod(spellInfo, SpellModOp.TargetResistance, ref armor); - } - - var resIgnoreAuras = GetAuraEffectsByType(AuraType.ModIgnoreTargetResist); - foreach (var eff in resIgnoreAuras) - { - if (eff.GetMiscValue().HasAnyFlag((int)SpellSchoolMask.Normal) && eff.IsAffectingSpell(spellInfo)) - armor = (float)Math.Floor(MathFunctions.AddPct(ref armor, -eff.GetAmount())); - } - - // Apply Player CR_ARMOR_PENETRATION rating - if (IsTypeId(TypeId.Player)) - { - float arpPct = ToPlayer().GetRatingBonusValue(CombatRating.ArmorPenetration); - - // no more than 100% - MathFunctions.RoundToInterval(ref arpPct, 0.0f, 100.0f); - - float maxArmorPen; - if (victim.GetLevelForTarget(attacker) < 60) - maxArmorPen = 400 + 85 * victim.GetLevelForTarget(attacker); - else - maxArmorPen = 400 + 85 * victim.GetLevelForTarget(attacker) + 4.5f * 85 * (victim.GetLevelForTarget(attacker) - 59); - - // Cap armor penetration to this number - maxArmorPen = Math.Min((armor + maxArmorPen) / 3.0f, armor); - // Figure out how much armor do we ignore - armor -= MathFunctions.CalculatePct(maxArmorPen, arpPct); - } - - if (MathFunctions.fuzzyLe(armor, 0.0f)) - return damage; - - uint attackerLevel = attacker.GetLevelForTarget(victim); - // Expansion and ContentTuningID necessary? Does Player get a ContentTuningID too ? - float armorConstant = Global.DB2Mgr.EvaluateExpectedStat(ExpectedStatType.ArmorConstant, attackerLevel, -2, 0, attacker.GetClass()); - if ((armor + armorConstant) == 0) - return damage; - - float mitigation = Math.Min(armor / (armor + armorConstant), 0.85f); - return Math.Max((uint)(damage * (1.0f - mitigation)), 1); - } - - public uint MeleeDamageBonusDone(Unit victim, uint pdamage, WeaponAttackType attType, DamageEffectType damagetype, SpellInfo spellProto = null) - { - if (victim == null || pdamage == 0) - return 0; - - uint creatureTypeMask = victim.GetCreatureTypeMask(); - - // Done fixed damage bonus auras - int DoneFlatBenefit = 0; - - // ..done - DoneFlatBenefit += GetTotalAuraModifierByMiscMask(AuraType.ModDamageDoneCreature, (int)creatureTypeMask); - - // ..done - // SPELL_AURA_MOD_DAMAGE_DONE included in weapon damage - - // ..done (base at attack power for marked target and base at attack power for creature type) - int APbonus = 0; - - if (attType == WeaponAttackType.RangedAttack) - { - APbonus += victim.GetTotalAuraModifier(AuraType.RangedAttackPowerAttackerBonus); - - // ..done (base at attack power and creature type) - APbonus += GetTotalAuraModifierByMiscMask(AuraType.ModRangedAttackPowerVersus, (int)creatureTypeMask); - } - else - { - APbonus += victim.GetTotalAuraModifier(AuraType.MeleeAttackPowerAttackerBonus); - - // ..done (base at attack power and creature type) - APbonus += GetTotalAuraModifierByMiscMask(AuraType.ModMeleeAttackPowerVersus, (int)creatureTypeMask); - } - - if (APbonus != 0) // Can be negative - { - bool normalized = spellProto != null && spellProto.HasEffect(SpellEffectName.NormalizedWeaponDmg); - DoneFlatBenefit += (int)(APbonus / 3.5f * GetAPMultiplier(attType, normalized)); - } - - // Done total percent damage auras - float DoneTotalMod = 1.0f; - - - if (spellProto != null) - { - // Some spells don't benefit from pct done mods - // mods for SPELL_SCHOOL_MASK_NORMAL are already factored in base melee damage calculation - if (!spellProto.HasAttribute(SpellAttr6.NoDonePctDamageMods) && !spellProto.GetSchoolMask().HasAnyFlag(SpellSchoolMask.Normal)) - { - float maxModDamagePercentSchool = 0.0f; - Player thisPlayer = ToPlayer(); - if (thisPlayer != null) - { - for (var i = SpellSchools.Holy; i < SpellSchools.Max; ++i) - { - if (Convert.ToBoolean((int)spellProto.GetSchoolMask() & (1 << (int)i))) - maxModDamagePercentSchool = Math.Max(maxModDamagePercentSchool, thisPlayer.m_activePlayerData.ModDamageDonePercent[(int)i]); - } - } - else - maxModDamagePercentSchool = GetTotalAuraMultiplierByMiscMask(AuraType.ModDamagePercentDone, (uint)spellProto.GetSchoolMask()); - - DoneTotalMod *= maxModDamagePercentSchool; - } - else - { - // melee attack - foreach (AuraEffect autoAttackDamage in GetAuraEffectsByType(AuraType.ModAutoAttackDamage)) - MathFunctions.AddPct(ref DoneTotalMod, autoAttackDamage.GetAmount()); - } - } - - DoneTotalMod *= GetTotalAuraMultiplierByMiscMask(AuraType.ModDamageDoneVersus, creatureTypeMask); - - // bonus against aurastate - DoneTotalMod *= GetTotalAuraMultiplier(AuraType.ModDamageDoneVersusAurastate, aurEff => - { - if (victim.HasAuraState((AuraStateType)aurEff.GetMiscValue())) - return true; - return false; - }); - - // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus - if (spellProto != null) - MathFunctions.AddPct(ref DoneTotalMod, GetTotalAuraModifierByMiscValue(AuraType.ModDamageDoneForMechanic, (int)spellProto.Mechanic)); - - float tmpDamage = (pdamage + DoneFlatBenefit) * DoneTotalMod; - - // apply spellmod to Done damage - if (spellProto != null) - { - Player modOwner = GetSpellModOwner(); - if (modOwner != null) - { - if (damagetype == DamageEffectType.DOT) - modOwner.ApplySpellMod(spellProto, SpellModOp.PeriodicHealingAndDamage, ref tmpDamage); - else - modOwner.ApplySpellMod(spellProto, SpellModOp.HealingAndDamage, ref tmpDamage); - } - } - - // bonus result can be negative - return (uint)Math.Max(tmpDamage, 0.0f); - } - public uint MeleeDamageBonusTaken(Unit attacker, uint pdamage, WeaponAttackType attType, DamageEffectType damagetype, SpellInfo spellProto = null) - { - if (pdamage == 0) - return 0; - - int TakenFlatBenefit = 0; - float TakenTotalCasterMod = 0.0f; - - // get all auras from caster that allow the spell to ignore resistance (sanctified wrath) - int attackSchoolMask = (int)(spellProto != null ? spellProto.GetSchoolMask() : SpellSchoolMask.Normal); - TakenTotalCasterMod += attacker.GetTotalAuraModifierByMiscMask(AuraType.ModIgnoreTargetResist, attackSchoolMask); - - // ..taken - TakenFlatBenefit += GetTotalAuraModifierByMiscMask(AuraType.ModDamageTaken, (int)attacker.GetMeleeDamageSchoolMask()); - - if (attType != WeaponAttackType.RangedAttack) - TakenFlatBenefit += GetTotalAuraModifier(AuraType.ModMeleeDamageTaken); - else - TakenFlatBenefit += GetTotalAuraModifier(AuraType.ModRangedDamageTaken); - - // Taken total percent damage auras - float TakenTotalMod = 1.0f; - - // ..taken - TakenTotalMod *= GetTotalAuraMultiplierByMiscMask(AuraType.ModDamagePercentTaken, (uint)attacker.GetMeleeDamageSchoolMask()); - - // .. taken pct (special attacks) - if (spellProto != null) - { - // From caster spells - TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModSchoolMaskDamageFromCaster, aurEff => - { - return aurEff.GetCasterGUID() == attacker.GetGUID() && (aurEff.GetMiscValue() & (int)spellProto.GetSchoolMask()) != 0; - }); - - TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModSpellDamageFromCaster, aurEff => - { - return aurEff.GetCasterGUID() == attacker.GetGUID() && aurEff.IsAffectingSpell(spellProto); - }); - - // Mod damage from spell mechanic - uint mechanicMask = spellProto.GetAllEffectsMechanicMask(); - - // Shred, Maul - "Effects which increase Bleed damage also increase Shred damage" - if (spellProto.SpellFamilyName == SpellFamilyNames.Druid && spellProto.SpellFamilyFlags[0].HasAnyFlag(0x00008800u)) - mechanicMask |= (1 << (int)Mechanics.Bleed); - - if (mechanicMask != 0) - { - TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModMechanicDamageTakenPercent, aurEff => - { - if ((mechanicMask & (1 << (aurEff.GetMiscValue()))) != 0) - return true; - return false; - }); - } - - if (damagetype == DamageEffectType.DOT) - TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModPeriodicDamageTaken, aurEff => (aurEff.GetMiscValue() & (uint)spellProto.GetSchoolMask()) != 0); - } - - AuraEffect cheatDeath = GetAuraEffect(45182, 0); - if (cheatDeath != null) - MathFunctions.AddPct(ref TakenTotalMod, cheatDeath.GetAmount()); - - if (attType != WeaponAttackType.RangedAttack) - TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModMeleeDamageTakenPct); - else - TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModRangedDamageTakenPct); - - // Versatility - Player modOwner = GetSpellModOwner(); - if (modOwner) - { - // only 50% of SPELL_AURA_MOD_VERSATILITY for damage reduction - float versaBonus = modOwner.GetTotalAuraModifier(AuraType.ModVersatility) / 2.0f; - MathFunctions.AddPct(ref TakenTotalMod, -(modOwner.GetRatingBonusValue(CombatRating.VersatilityDamageTaken) + versaBonus)); - } - - float tmpDamage = 0.0f; - - if (TakenTotalCasterMod != 0) - { - if (TakenFlatBenefit < 0) - { - if (TakenTotalMod < 1) - tmpDamage = (((MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod) + TakenFlatBenefit) * TakenTotalMod) + MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod)); - else - tmpDamage = (((MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod) + TakenFlatBenefit) + MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod)) * TakenTotalMod); - } - else if (TakenTotalMod < 1) - tmpDamage = ((MathFunctions.CalculatePct(pdamage + TakenFlatBenefit, TakenTotalCasterMod) * TakenTotalMod) + MathFunctions.CalculatePct(pdamage + TakenFlatBenefit, TakenTotalCasterMod)); - } - if (tmpDamage == 0) - tmpDamage = (pdamage + TakenFlatBenefit) * TakenTotalMod; - - // bonus result can be negative - return (uint)Math.Max(tmpDamage, 0.0f); - } - - bool IsBlockCritical() - { - if (RandomHelper.randChance(GetTotalAuraModifier(AuraType.ModBlockCritChance))) - return true; - return false; - } - public virtual SpellSchoolMask GetMeleeDamageSchoolMask() { return SpellSchoolMask.None; } - - // Redirect Threat - public void SetRedirectThreat(ObjectGuid guid, uint pct) { _redirectThreatInfo.Set(guid, pct); } - public void ResetRedirectThreat() { SetRedirectThreat(ObjectGuid.Empty, 0); } - void ModifyRedirectThreat(int amount) { _redirectThreatInfo.ModifyThreatPct(amount); } - public uint GetRedirectThreatPercent() { return _redirectThreatInfo.GetThreatPct(); } - public Unit GetRedirectThreatTarget() - { - return Global.ObjAccessor.GetUnit(this, _redirectThreatInfo.GetTargetGUID()); - } - - float CalculateDefaultCoefficient(SpellInfo spellInfo, DamageEffectType damagetype) - { - // Damage over Time spells bonus calculation - float DotFactor = 1.0f; - if (damagetype == DamageEffectType.DOT) - { - - int DotDuration = spellInfo.GetDuration(); - if (!spellInfo.IsChanneled() && DotDuration > 0) - DotFactor = DotDuration / 15000.0f; - - uint DotTicks = spellInfo.GetMaxTicks(); - if (DotTicks != 0) - DotFactor /= DotTicks; - } - - uint CastingTime = (uint)(spellInfo.IsChanneled() ? spellInfo.GetDuration() : spellInfo.CalcCastTime()); - // Distribute Damage over multiple effects, reduce by AoE - CastingTime = GetCastingTimeForBonus(spellInfo, damagetype, CastingTime); - - // As wowwiki says: C = (Cast Time / 3.5) - return (CastingTime / 3500.0f) * DotFactor; - } - - void ApplyPercentModFloatVar(ref float var, float val, bool apply) - { - var *= (apply ? (100.0f + val) / 100.0f : 100.0f / (100.0f + val)); - } - - public void ApplyAttackTimePercentMod(WeaponAttackType att, float val, bool apply) - { - float remainingTimePct = m_attackTimer[(int)att] / (m_baseAttackSpeed[(int)att] * m_modAttackSpeedPct[(int)att]); - if (val > 0.0f) - { - MathFunctions.ApplyPercentModFloatVar(ref m_modAttackSpeedPct[(int)att], val, !apply); - - if (att == WeaponAttackType.BaseAttack) - ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModHaste), val, !apply); - else if (att == WeaponAttackType.RangedAttack) - ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModRangedHaste), val, !apply); - } - else - { - MathFunctions.ApplyPercentModFloatVar(ref m_modAttackSpeedPct[(int)att], -val, apply); - - if (att == WeaponAttackType.BaseAttack) - ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModHaste), -val, apply); - else if (att == WeaponAttackType.RangedAttack) - ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModRangedHaste), -val, apply); - } - - UpdateAttackTimeField(att); - m_attackTimer[(int)att] = (uint)(m_baseAttackSpeed[(int)att] * m_modAttackSpeedPct[(int)att] * remainingTimePct); - } // function based on function Unit.CanAttack from 13850 client public bool _IsValidAttackTarget(Unit target, SpellInfo bySpell, WorldObject obj = null) @@ -3233,7 +1634,7 @@ namespace Game.Entities { // ignore stealth for aoe spells. Ignore stealth if target is player and unit in combat with same player bool ignoreStealthCheck = (bySpell != null && bySpell.IsAffectingArea()) || - (target.GetTypeId() == TypeId.Player && target.HasStealthAura() && target.IsInCombat() && IsInCombatWith(target)); + (target.GetTypeId() == TypeId.Player && target.HasStealthAura() && IsInCombatWith(target)); if (!CanSeeOrDetect(target, ignoreStealthCheck)) return false; @@ -3344,6 +1745,7 @@ namespace Game.Entities { return _IsValidAssistTarget(target, null); } + // function based on function Unit.CanAssist from 13850 client public bool _IsValidAssistTarget(Unit target, SpellInfo bySpell) { @@ -3437,62 +1839,30 @@ namespace Game.Entities public virtual bool CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEffect aurEff) { return true; } - public virtual void UpdateDamageDoneMods(WeaponAttackType attackType) + public void ApplyAttackTimePercentMod(WeaponAttackType att, float val, bool apply) { - UnitMods unitMod = attackType switch + float remainingTimePct = m_attackTimer[(int)att] / (m_baseAttackSpeed[(int)att] * m_modAttackSpeedPct[(int)att]); + if (val > 0.0f) { - WeaponAttackType.BaseAttack => UnitMods.DamageMainHand, - WeaponAttackType.OffAttack => UnitMods.DamageOffHand, - WeaponAttackType.RangedAttack => UnitMods.DamageRanged, - _ => throw new NotImplementedException(), - }; + MathFunctions.ApplyPercentModFloatVar(ref m_modAttackSpeedPct[(int)att], val, !apply); - float amount = GetTotalAuraModifier(AuraType.ModDamageDone, aurEff => + if (att == WeaponAttackType.BaseAttack) + ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModHaste), val, !apply); + else if (att == WeaponAttackType.RangedAttack) + ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModRangedHaste), val, !apply); + } + else { - if ((aurEff.GetMiscValue() & (int)SpellSchoolMask.Normal) == 0) - return false; + MathFunctions.ApplyPercentModFloatVar(ref m_modAttackSpeedPct[(int)att], -val, apply); - return CheckAttackFitToAuraRequirement(attackType, aurEff); - }); + if (att == WeaponAttackType.BaseAttack) + ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModHaste), -val, apply); + else if (att == WeaponAttackType.RangedAttack) + ApplyPercentModUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.ModRangedHaste), -val, apply); + } - SetStatFlatModifier(unitMod, UnitModifierFlatType.Total, amount); + UpdateAttackTimeField(att); + m_attackTimer[(int)att] = (uint)(m_baseAttackSpeed[(int)att] * m_modAttackSpeedPct[(int)att] * remainingTimePct); } - - public void UpdateAllDamageDoneMods() - { - for (var attackType = WeaponAttackType.BaseAttack; attackType < WeaponAttackType.Max; ++attackType) - UpdateDamageDoneMods(attackType); - } - - public void UpdateDamagePctDoneMods(WeaponAttackType attackType) - { - (UnitMods unitMod, float factor) = attackType switch - { - WeaponAttackType.BaseAttack => (UnitMods.DamageMainHand, 1.0f), - WeaponAttackType.OffAttack => (UnitMods.DamageOffHand, 0.5f), - WeaponAttackType.RangedAttack => (UnitMods.DamageRanged, 1.0f), - _ => throw new NotImplementedException(), - }; - - factor *= GetTotalAuraMultiplier(AuraType.ModDamagePercentDone, aurEff => - { - if (!aurEff.GetMiscValue().HasAnyFlag((int)SpellSchoolMask.Normal)) - return false; - - return CheckAttackFitToAuraRequirement(attackType, aurEff); - }); - - if (attackType == WeaponAttackType.OffAttack) - factor *= GetTotalAuraMultiplier(AuraType.ModOffhandDamagePct, auraEffect => CheckAttackFitToAuraRequirement(attackType, auraEffect)); - - SetStatPctModifier(unitMod, UnitModifierPctType.Total, factor); - } - - public void UpdateAllDamagePctDoneMods() - { - for (var attackType = WeaponAttackType.BaseAttack; attackType < WeaponAttackType.Max; ++attackType) - UpdateDamagePctDoneMods(attackType); - } - } } diff --git a/Source/Game/Entities/Unit/Unit.Fields.cs b/Source/Game/Entities/Unit/Unit.Fields.cs index a1335d7a2..90bac110f 100644 --- a/Source/Game/Entities/Unit/Unit.Fields.cs +++ b/Source/Game/Entities/Unit/Unit.Fields.cs @@ -53,15 +53,14 @@ namespace Game.Entities protected List attackerList = new(); Dictionary m_reactiveTimer = new(); protected float[][] m_weaponDamage = new float[(int)WeaponAttackType.Max][]; - public float[] m_threatModifier = new float[(int)SpellSchools.Max]; uint[] m_baseAttackSpeed = new uint[(int)WeaponAttackType.Max]; float[] m_modAttackSpeedPct = new float[(int)WeaponAttackType.Max]; protected uint[] m_attackTimer = new uint[(int)WeaponAttackType.Max]; - ThreatManager threatManager; - HostileRefManager hostileRefManager; - RedirectThreatInfo _redirectThreatInfo; + CombatManager m_combatManager; + ThreatManager m_threatManager; + protected Unit attacking; public float ModMeleeHitChance { get; set; } @@ -520,27 +519,6 @@ namespace Game.Entities byte _chargesRemoved; } - public struct RedirectThreatInfo - { - ObjectGuid _targetGUID; - uint _threatPct; - - public ObjectGuid GetTargetGUID() { return _targetGUID; } - public uint GetThreatPct() { return _threatPct; } - - public void Set(ObjectGuid guid, uint pct) - { - _targetGUID = guid; - _threatPct = pct; - } - - public void ModifyThreatPct(int amount) - { - amount += (int)_threatPct; - _threatPct = (uint)(Math.Max(0, amount)); - } - } - public class SpellPeriodicAuraLogInfo { public SpellPeriodicAuraLogInfo(AuraEffect _auraEff, uint _damage, uint _originalDamage, uint _overDamage, uint _absorb, uint _resist, float _multiplier, bool _critical) diff --git a/Source/Game/Entities/Unit/Unit.Movement.cs b/Source/Game/Entities/Unit/Unit.Movement.cs index a427ec676..0804c716b 100644 --- a/Source/Game/Entities/Unit/Unit.Movement.cs +++ b/Source/Game/Entities/Unit/Unit.Movement.cs @@ -58,7 +58,10 @@ namespace Game.Entities return true; if (HasUnitFlag2((UnitFlags2)0x1000000)) return false; - return HasUnitFlag(UnitFlags.PetInCombat | UnitFlags.Rename | UnitFlags.Unk15); + if (IsPet() && HasUnitFlag(UnitFlags.PetInCombat)) + return true; + + return HasUnitFlag(UnitFlags.Rename | UnitFlags.Unk15); } public virtual bool IsInWater() { diff --git a/Source/Game/Entities/Unit/Unit.Pets.cs b/Source/Game/Entities/Unit/Unit.Pets.cs index df5f672ff..ea4b4fde0 100644 --- a/Source/Game/Entities/Unit/Unit.Pets.cs +++ b/Source/Game/Entities/Unit/Unit.Pets.cs @@ -274,6 +274,8 @@ namespace Game.Entities } } } + + UpdatePetCombatState(); } public bool SetCharmedBy(Unit charmer, CharmType type, AuraApplication aurApp = null) @@ -314,7 +316,6 @@ namespace Game.Entities CastStop(); CombatStop(); // @todo CombatStop(true) may cause crash (interrupt spells) - GetThreatManager().ClearAllThreat(); Player playerCharmer = charmer.ToPlayer(); @@ -463,8 +464,6 @@ namespace Game.Entities CastStop(); CombatStop(); // @todo CombatStop(true) may cause crash (interrupt spells) - GetHostileRefManager().DeleteReferences(); - GetThreatManager().ClearAllThreat(); if (_oldFactionId != 0) { @@ -545,6 +544,8 @@ namespace Game.Entities player.SetClientControl(this, true); } + EngageWithTarget(charmer); + // a guardian should always have charminfo if (playerCharmer && this != charmer.GetFirstControlled()) playerCharmer.SendRemoveControlBar(); @@ -651,6 +652,8 @@ namespace Game.Entities m_Controlled.Remove(charm); } } + + UpdatePetCombatState(); } public Unit GetFirstControlled() @@ -698,6 +701,8 @@ namespace Game.Entities Log.outFatal(LogFilter.Unit, "Unit {0} is not able to release its minion {1}", GetEntry(), GetMinionGUID()); if (!GetCharmGUID().IsEmpty()) Log.outFatal(LogFilter.Unit, "Unit {0} is not able to release its charm {1}", GetEntry(), GetCharmGUID()); + if (!IsPet()) // pets don't use the flag for this + RemoveUnitFlag(UnitFlags.PetInCombat); // m_controlled is now empty, so we know none of our minions are in combat } public void SendPetActionFeedback(PetActionFeedback msg, uint spellId) @@ -794,5 +799,25 @@ namespace Game.Entities pet.SetFullHealth(); return true; } + + public void UpdatePetCombatState() + { + Cypher.Assert(!IsPet()); // player pets do not use UNIT_FLAG_PET_IN_COMBAT for this purpose - but player pets should also never have minions of their own to call this + + bool state = false; + foreach (Unit minion in m_Controlled) + { + if (minion.IsInCombat()) + { + state = true; + break; + } + } + + if (state) + AddUnitFlag(UnitFlags.PetInCombat); + else + RemoveUnitFlag(UnitFlags.PetInCombat); + } } } diff --git a/Source/Game/Entities/Unit/Unit.Spells.cs b/Source/Game/Entities/Unit/Unit.Spells.cs index 39a11a312..cb3c69e56 100644 --- a/Source/Game/Entities/Unit/Unit.Spells.cs +++ b/Source/Game/Entities/Unit/Unit.Spells.cs @@ -1514,14 +1514,7 @@ namespace Game.Entities SendCombatLogMessage(data); } - public void EnergizeBySpell(Unit victim, uint spellId, int damage, PowerType powerType) - { - SpellInfo info = Global.SpellMgr.GetSpellInfo(spellId, GetMap().GetDifficultyID()); - if (info != null) - EnergizeBySpell(victim, info, damage, powerType); - } - - void EnergizeBySpell(Unit victim, SpellInfo spellInfo, int damage, PowerType powerType) + public void EnergizeBySpell(Unit victim, SpellInfo spellInfo, int damage, PowerType powerType) { int gain = victim.ModifyPower(powerType, damage, false); int overEnergize = damage - gain; diff --git a/Source/Game/Entities/Unit/Unit.cs b/Source/Game/Entities/Unit/Unit.cs index 02e789e48..3ecbab381 100644 --- a/Source/Game/Entities/Unit/Unit.cs +++ b/Source/Game/Entities/Unit/Unit.cs @@ -23,14 +23,15 @@ using Game.BattleGrounds; using Game.Chat; using Game.Combat; using Game.DataStorage; +using Game.Groups; using Game.Maps; using Game.Movement; +using Game.Networking; using Game.Networking.Packets; using Game.Spells; using System; using System.Collections.Generic; using System.Linq; -using Game.Networking; namespace Game.Entities { @@ -40,9 +41,8 @@ namespace Game.Entities { MoveSpline = new MoveSpline(); i_motionMaster = new MotionMaster(this); - threatManager = new ThreatManager(this); - UnitTypeMask = UnitTypeMask.None; - hostileRefManager = new HostileRefManager(this); + m_combatManager = new CombatManager(this); + m_threatManager = new ThreatManager(this); _spellHistory = new SpellHistory(this); m_FollowingRefManager = new RefManager(); @@ -84,13 +84,9 @@ namespace Game.Entities } BaseSpellCritChance = 5; - for (byte i = 0; i < (int)SpellSchools.Max; ++i) - m_threatModifier[i] = 1.0f; - for (byte i = 0; i < (int)UnitMoveType.Max; ++i) m_speed_rate[i] = 1.0f; - _redirectThreatInfo = new RedirectThreatInfo(); m_serverSideVisibility.SetValue(ServerSideVisibilityType.Ghost, GhostVisibilityType.Alive); movesplineTimer = new TimeTrackerSmall(); @@ -151,24 +147,7 @@ namespace Game.Entities // Having this would prevent spells from being proced, so let's crash Cypher.Assert(m_procDeep == 0); - if (CanHaveThreatList() && GetThreatManager().IsNeedUpdateToClient(diff)) - SendThreatListUpdate(); - - // update combat timer only for players and pets (only pets with PetAI) - if (IsInCombat() && (IsTypeId(TypeId.Player) || (IsPet() && IsControlledByPlayer()))) - { - // Check UNIT_STATE_MELEE_ATTACKING or UNIT_STATE_CHASE (without UNIT_STATE_FOLLOW in this case) so pets can reach far away - // targets without stopping half way there and running off. - // These flags are reset after target dies or another command is given. - if (hostileRefManager.IsEmpty()) - { - // m_CombatTimer set at aura start and it will be freeze until aura removing - if (combatTimer <= diff) - ClearInCombat(); - else - combatTimer -= diff; - } - } + m_combatManager.Update(diff); uint att; // not implemented before 3.0.2 @@ -435,6 +414,7 @@ namespace Game.Entities AddToNotify(NotifyFlags.VisibilityChanged); else { + m_threatManager.UpdateOnlineStates(true, true); base.UpdateObjectVisibility(true); // call MoveInLineOfSight for nearby creatures AIRelocationNotifier notifier = new(this); @@ -511,7 +491,6 @@ namespace Game.Entities m_Events.KillAllEvents(false); // non-delatable (currently casted spells) will not deleted now but it will deleted at call in Map.RemoveAllObjectsInRemoveList CombatStop(); GetThreatManager().ClearAllThreat(); - GetHostileRefManager().DeleteReferences(); } public override void CleanupsBeforeDelete(bool finalCleanup = true) { @@ -1202,50 +1181,13 @@ namespace Game.Entities public bool IsCharmed() { return !GetCharmerGUID().IsEmpty(); } public bool IsPossessed() { return HasUnitState(UnitState.Possessed); } - public HostileRefManager GetHostileRefManager() { return hostileRefManager; } - public void OnPhaseChange() { if (!IsInWorld) return; if (IsTypeId(TypeId.Unit) || !ToPlayer().GetSession().PlayerLogout()) - { - HostileRefManager refManager = GetHostileRefManager(); - HostileReference refe = refManager.GetFirst(); - - while (refe != null) - { - Unit unit = refe.GetSource().GetOwner(); - if (unit != null) - { - Creature creature = unit.ToCreature(); - if (creature != null) - refManager.SetOnlineOfflineState(creature, creature.IsInPhase(this)); - } - - refe = refe.Next(); - } - - // modify threat lists for new phasemask - if (!IsTypeId(TypeId.Player)) - { - List threatList = GetThreatManager().GetThreatList(); - List offlineThreatList = GetThreatManager().GetOfflineThreatList(); - - // merge expects sorted lists - threatList.Sort(); - offlineThreatList.Sort(); - threatList.AddRange(offlineThreatList); - - foreach (var host in threatList) - { - Unit unit = host.GetTarget(); - if (unit != null) - unit.GetHostileRefManager().SetOnlineOfflineState(ToCreature(), unit.IsInPhase(this)); - } - } - } + m_threatManager.UpdateOnlineStates(true, true); } public uint GetModelForForm(ShapeShiftForm form, uint spellId) @@ -1384,7 +1326,6 @@ namespace Game.Entities { CombatStop(); GetThreatManager().ClearAllThreat(); - GetHostileRefManager().DeleteReferences(); if (IsNonMeleeSpellCast(false)) InterruptNonMeleeSpells(false); @@ -2582,5 +2523,1565 @@ namespace Game.Entities base.DestroyForPlayer(target); } + + public bool CanDualWield() { return m_canDualWield; } + + public virtual void SetCanDualWield(bool value) { m_canDualWield = value; } + + public DeathState GetDeathState() + { + return m_deathState; + } + + public bool HaveOffhandWeapon() + { + if (IsTypeId(TypeId.Player)) + return ToPlayer().GetWeaponForAttack(WeaponAttackType.OffAttack, true) != null; + else + return m_canDualWield; + } + + void StartReactiveTimer(ReactiveType reactive) { m_reactiveTimer[reactive] = 4000; } + + void DealDamageMods(Unit victim, ref uint damage) + { + if (victim == null || !victim.IsAlive() || victim.HasUnitState(UnitState.InFlight) + || (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsInEvadeMode())) + { + damage = 0; + } + } + public void DealDamageMods(Unit victim, ref uint damage, ref uint absorb) + { + if (victim == null || !victim.IsAlive() || victim.HasUnitState(UnitState.InFlight) + || (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsEvadingAttacks())) + { + absorb += damage; + damage = 0; + return; + } + + damage = (uint)(damage * GetDamageMultiplierForTarget(victim)); + } + void DealMeleeDamage(CalcDamageInfo damageInfo, bool durabilityLoss) + { + Unit victim = damageInfo.target; + + if (!victim.IsAlive() || victim.HasUnitState(UnitState.InFlight) || (victim.IsTypeId(TypeId.Unit) && victim.ToCreature().IsEvadingAttacks())) + return; + + // Hmmmm dont like this emotes client must by self do all animations + if (damageInfo.HitInfo.HasAnyFlag(HitInfo.CriticalHit)) + victim.HandleEmoteCommand(Emote.OneshotWoundCritical); + if (damageInfo.blocked_amount != 0 && damageInfo.TargetState != VictimState.Blocks) + victim.HandleEmoteCommand(Emote.OneshotParryShield); + + if (damageInfo.TargetState == VictimState.Parry && + (!IsTypeId(TypeId.Unit) || !ToCreature().GetCreatureTemplate().FlagsExtra.HasAnyFlag(CreatureFlagsExtra.NoParryHasten))) + { + // Get attack timers + float offtime = victim.GetAttackTimer(WeaponAttackType.OffAttack); + float basetime = victim.GetAttackTimer(WeaponAttackType.BaseAttack); + // Reduce attack time + if (victim.HaveOffhandWeapon() && offtime < basetime) + { + float percent20 = victim.GetBaseAttackTime(WeaponAttackType.OffAttack) * 0.20f; + float percent60 = 3.0f * percent20; + if (offtime > percent20 && offtime <= percent60) + victim.SetAttackTimer(WeaponAttackType.OffAttack, (uint)percent20); + else if (offtime > percent60) + { + offtime -= 2.0f * percent20; + victim.SetAttackTimer(WeaponAttackType.OffAttack, (uint)offtime); + } + } + else + { + float percent20 = victim.GetBaseAttackTime(WeaponAttackType.BaseAttack) * 0.20f; + float percent60 = 3.0f * percent20; + if (basetime > percent20 && basetime <= percent60) + victim.SetAttackTimer(WeaponAttackType.BaseAttack, (uint)percent20); + else if (basetime > percent60) + { + basetime -= 2.0f * percent20; + victim.SetAttackTimer(WeaponAttackType.BaseAttack, (uint)basetime); + } + } + } + + // Call default DealDamage + CleanDamage cleanDamage = new(damageInfo.cleanDamage, damageInfo.absorb, damageInfo.attackType, damageInfo.hitOutCome); + DealDamage(victim, damageInfo.damage, cleanDamage, DamageEffectType.Direct, (SpellSchoolMask)damageInfo.damageSchoolMask, null, durabilityLoss); + + // If this is a creature and it attacks from behind it has a probability to daze it's victim + if ((damageInfo.hitOutCome == MeleeHitOutcome.Crit || damageInfo.hitOutCome == MeleeHitOutcome.Crushing || damageInfo.hitOutCome == MeleeHitOutcome.Normal || damageInfo.hitOutCome == MeleeHitOutcome.Glancing) && + !IsTypeId(TypeId.Player) && !ToCreature().IsControlledByPlayer() && !victim.HasInArc(MathFunctions.PI, this) + && (victim.IsTypeId(TypeId.Player) || !victim.ToCreature().IsWorldBoss()) && !victim.IsVehicle()) + { + // 20% base chance + float chance = 20.0f; + + // there is a newbie protection, at level 10 just 7% base chance; assuming linear function + if (victim.GetLevel() < 30) + chance = 0.65f * victim.GetLevelForTarget(this) + 0.5f; + + uint victimDefense = victim.GetMaxSkillValueForLevel(this); + uint attackerMeleeSkill = GetMaxSkillValueForLevel(); + + chance *= attackerMeleeSkill / (float)victimDefense * 0.16f; + + // -probability is between 0% and 40% + MathFunctions.RoundToInterval(ref chance, 0.0f, 40.0f); + + if (RandomHelper.randChance(chance)) + CastSpell(victim, 1604, true); + } + + if (IsTypeId(TypeId.Player)) + { + DamageInfo dmgInfo = new(damageInfo); + ToPlayer().CastItemCombatSpell(dmgInfo); + } + + // Do effect if any damage done to target + if (damageInfo.damage != 0) + { + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + var vDamageShieldsCopy = victim.GetAuraEffectsByType(AuraType.DamageShield); + foreach (var dmgShield in vDamageShieldsCopy) + { + SpellInfo spellInfo = dmgShield.GetSpellInfo(); + + // Damage shield can be resisted... + var missInfo = victim.SpellHitResult(this, spellInfo, false); + if (missInfo != SpellMissInfo.None) + { + victim.SendSpellMiss(this, spellInfo.Id, missInfo); + continue; + } + + // ...or immuned + if (IsImmunedToDamage(spellInfo)) + { + victim.SendSpellDamageImmune(this, spellInfo.Id, false); + continue; + } + + uint damage = (uint)dmgShield.GetAmount(); + Unit caster = dmgShield.GetCaster(); + if (caster) + { + damage = caster.SpellDamageBonusDone(this, spellInfo, damage, DamageEffectType.SpellDirect, dmgShield.GetSpellEffectInfo()); + damage = SpellDamageBonusTaken(caster, spellInfo, damage, DamageEffectType.SpellDirect, dmgShield.GetSpellEffectInfo()); + } + + DamageInfo damageInfo1 = new(this, victim, damage, spellInfo, spellInfo.GetSchoolMask(), DamageEffectType.SpellDirect, WeaponAttackType.BaseAttack); + victim.CalcAbsorbResist(damageInfo1); + damage = damageInfo1.GetDamage(); + + victim.DealDamageMods(this, ref damage); + + SpellDamageShield damageShield = new(); + damageShield.Attacker = victim.GetGUID(); + damageShield.Defender = GetGUID(); + damageShield.SpellID = spellInfo.Id; + damageShield.TotalDamage = damage; + damageShield.OriginalDamage = (int)damageInfo.originalDamage; + damageShield.OverKill = (uint)Math.Max(damage - GetHealth(), 0); + damageShield.SchoolMask = (uint)spellInfo.SchoolMask; + damageShield.LogAbsorbed = damageInfo1.GetAbsorb(); + + victim.DealDamage(this, damage, null, DamageEffectType.SpellDirect, spellInfo.GetSchoolMask(), spellInfo, true); + damageShield.LogData.Initialize(this); + + victim.SendCombatLogMessage(damageShield); + } + } + } + public uint DealDamage(Unit victim, uint damage, CleanDamage cleanDamage = null, DamageEffectType damagetype = DamageEffectType.Direct, SpellSchoolMask damageSchoolMask = SpellSchoolMask.Normal, SpellInfo spellProto = null, bool durabilityLoss = true) + { + if (victim.IsAIEnabled) + victim.GetAI().DamageTaken(this, ref damage); + + if (IsAIEnabled) + GetAI().DamageDealt(victim, ref damage, damagetype); + + // Hook for OnDamage Event + Global.ScriptMgr.OnDamage(this, victim, ref damage); + + if (victim.IsTypeId(TypeId.Player) && this != victim) + { + // Signal to pets that their owner was attacked - except when DOT. + if (damagetype != DamageEffectType.DOT) + { + foreach (Unit controlled in victim.m_Controlled) + { + Creature cControlled = controlled.ToCreature(); + if (cControlled != null) + if (cControlled.IsAIEnabled) + cControlled.GetAI().OwnerAttackedBy(this); + } + } + + if (victim.ToPlayer().GetCommandStatus(PlayerCommandStates.God)) + return 0; + } + + if (damagetype != DamageEffectType.NoDamage) + { + // interrupting auras with SpellAuraInterruptFlags.Damage before checking !damage (absorbed damage breaks that type of auras) + if (spellProto != null) + { + if (!spellProto.HasAttribute(SpellAttr4.DamageDoesntBreakAuras)) + victim.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Damage, spellProto.Id); + } + else + victim.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Damage, 0); + + if (damage == 0 && damagetype != DamageEffectType.DOT && cleanDamage != null && cleanDamage.absorbed_damage != 0) + { + if (victim != this && victim.IsPlayer()) + { + Spell spell = victim.GetCurrentSpell(CurrentSpellTypes.Generic); + if (spell != null) + if (spell.GetState() == SpellState.Preparing && spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamageAbsorb)) + victim.InterruptNonMeleeSpells(false); + } + } + + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + var vCopyDamageCopy = victim.GetAuraEffectsByType(AuraType.ShareDamagePct); + // copy damage to casters of this aura + foreach (var aura in vCopyDamageCopy) + { + // Check if aura was removed during iteration - we don't need to work on such auras + if (!aura.GetBase().IsAppliedOnTarget(victim.GetGUID())) + continue; + + // check damage school mask + if ((aura.GetMiscValue() & (int)damageSchoolMask) == 0) + continue; + + Unit shareDamageTarget = aura.GetCaster(); + if (shareDamageTarget == null) + continue; + + SpellInfo spell = aura.GetSpellInfo(); + + uint share = MathFunctions.CalculatePct(damage, aura.GetAmount()); + + // @todo check packets if damage is done by victim, or by attacker of victim + DealDamageMods(shareDamageTarget, ref share); + DealDamage(shareDamageTarget, share, null, DamageEffectType.NoDamage, spell.GetSchoolMask(), spell, false); + } + } + + // Rage from Damage made (only from direct weapon damage) + if (cleanDamage != null && (cleanDamage.attackType == WeaponAttackType.BaseAttack || cleanDamage.attackType == WeaponAttackType.OffAttack) && damagetype == DamageEffectType.Direct && this != victim && GetPowerType() == PowerType.Rage) + { + uint rage = (uint)(GetBaseAttackTime(cleanDamage.attackType) / 1000.0f * 1.75f); + if (cleanDamage.attackType == WeaponAttackType.OffAttack) + rage /= 2; + RewardRage(rage); + } + + if (damage == 0) + return 0; + + Log.outDebug(LogFilter.Unit, "DealDamageStart"); + + uint health = (uint)victim.GetHealth(); + Log.outDebug(LogFilter.Unit, "Unit {0} dealt {1} damage to unit {2}", GetGUID(), damage, victim.GetGUID()); + + // duel ends when player has 1 or less hp + bool duel_hasEnded = false; + bool duel_wasMounted = false; + if (victim.IsTypeId(TypeId.Player) && victim.ToPlayer().duel != null && damage >= (health - 1)) + { + // prevent kill only if killed in duel and killed by opponent or opponent controlled creature + if (victim.ToPlayer().duel.opponent == this || victim.ToPlayer().duel.opponent.GetGUID() == GetOwnerGUID()) + damage = health - 1; + + duel_hasEnded = true; + } + else if (victim.IsVehicle() && damage >= (health - 1) && victim.GetCharmer() != null && victim.GetCharmer().IsTypeId(TypeId.Player)) + { + Player victimRider = victim.GetCharmer().ToPlayer(); + if (victimRider != null && victimRider.duel != null && victimRider.duel.isMounted) + { + // prevent kill only if killed in duel and killed by opponent or opponent controlled creature + if (victimRider.duel.opponent == this || victimRider.duel.opponent.GetGUID() == GetCharmerGUID()) + damage = health - 1; + + duel_wasMounted = true; + duel_hasEnded = true; + } + } + + if (IsTypeId(TypeId.Player) && this != victim) + { + Player killer = ToPlayer(); + + // in bg, count dmg if victim is also a player + if (victim.IsTypeId(TypeId.Player)) + { + Battleground bg = killer.GetBattleground(); + if (bg) + bg.UpdatePlayerScore(killer, ScoreType.DamageDone, damage); + } + + killer.UpdateCriteria(CriteriaTypes.DamageDone, health > damage ? damage : health, 0, 0, victim); + killer.UpdateCriteria(CriteriaTypes.HighestHitDealt, damage); + } + + if (victim.IsTypeId(TypeId.Player)) + { + victim.ToPlayer().UpdateCriteria(CriteriaTypes.HighestHitReceived, damage); + } + + damage /= (uint)victim.GetHealthMultiplierForTarget(this); + + if (victim.GetTypeId() != TypeId.Player && (!victim.IsControlledByPlayer() || victim.IsVehicle())) + { + if (!victim.ToCreature().HasLootRecipient()) + victim.ToCreature().SetLootRecipient(this); + + if (IsControlledByPlayer()) + victim.ToCreature().LowerPlayerDamageReq(health < damage ? health : damage); + } + + bool killed = false; + bool skipSettingDeathState = false; + + if (health <= damage) + { + killed = true; + + Log.outDebug(LogFilter.Unit, "DealDamage: victim just died"); + + if (victim.IsTypeId(TypeId.Player) && victim != this) + victim.ToPlayer().UpdateCriteria(CriteriaTypes.TotalDamageReceived, health); + + if (damagetype != DamageEffectType.NoDamage && damagetype != DamageEffectType.Self && victim.HasAuraType(AuraType.SchoolAbsorbOverkill)) + { + var vAbsorbOverkill = victim.GetAuraEffectsByType(AuraType.SchoolAbsorbOverkill); + DamageInfo damageInfo = new(this, victim, damage, spellProto, damageSchoolMask, damagetype, cleanDamage != null ? cleanDamage.attackType : WeaponAttackType.BaseAttack); + + foreach (var absorbAurEff in vAbsorbOverkill) + { + Aura baseAura = absorbAurEff.GetBase(); + AuraApplication aurApp = baseAura.GetApplicationOfTarget(victim.GetGUID()); + if (aurApp == null) + continue; + + if ((absorbAurEff.GetMiscValue() & (int)damageInfo.GetSchoolMask()) == 0) + continue; + + // cannot absorb over limit + if (damage >= victim.CountPctFromMaxHealth(100 + absorbAurEff.GetMiscValueB())) + continue; + + // get amount which can be still absorbed by the aura + int currentAbsorb = absorbAurEff.GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + uint tempAbsorb = (uint)currentAbsorb; + + // This aura type is used both by Spirit of Redemption (death not really prevented, must grant all credit immediately) and Cheat Death (death prevented) + // repurpose PreventDefaultAction for this + bool deathFullyPrevented = false; + + absorbAurEff.GetBase().CallScriptEffectAbsorbHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb, ref deathFullyPrevented); + currentAbsorb = (int)tempAbsorb; + + // absorb must be smaller than the damage itself + currentAbsorb = MathFunctions.RoundToInterval(ref currentAbsorb, 0, (int)damageInfo.GetDamage()); + damageInfo.AbsorbDamage((uint)currentAbsorb); + + if (deathFullyPrevented) + killed = false; + + skipSettingDeathState = true; + + if (currentAbsorb != 0) + { + SpellAbsorbLog absorbLog = new(); + absorbLog.Attacker = GetGUID(); + absorbLog.Victim = victim.GetGUID(); + absorbLog.Caster = baseAura.GetCasterGUID(); + absorbLog.AbsorbedSpellID = spellProto != null ? spellProto.Id : 0; + absorbLog.AbsorbSpellID = baseAura.GetId(); + absorbLog.Absorbed = currentAbsorb; + absorbLog.OriginalDamage = damageInfo.GetOriginalDamage(); + absorbLog.LogData.Initialize(victim); + SendCombatLogMessage(absorbLog); + } + } + + damage = damageInfo.GetDamage(); + } + } + + if (killed) + Kill(victim, durabilityLoss, skipSettingDeathState); + else + { + Log.outDebug(LogFilter.Unit, "DealDamageAlive"); + + if (victim.IsTypeId(TypeId.Player)) + victim.ToPlayer().UpdateCriteria(CriteriaTypes.TotalDamageReceived, damage); + + victim.ModifyHealth(-(int)damage); + + if (damagetype == DamageEffectType.Direct || damagetype == DamageEffectType.SpellDirect) + victim.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.NonPeriodicDamage, spellProto != null ? spellProto.Id : 0); + + if (!victim.IsTypeId(TypeId.Player)) + { + // Part of Evade mechanics. DoT's and Thorns / Retribution Aura do not contribute to this + if (damagetype != DamageEffectType.DOT && damage > 0 && !victim.GetOwnerGUID().IsPlayer() && (spellProto == null || !spellProto.HasAura(AuraType.DamageShield))) + victim.ToCreature().SetLastDamagedTime(GameTime.GetGameTime() + SharedConst.MaxAggroResetTime); + + victim.GetThreatManager().AddThreat(this, damage, spellProto); + } + else // victim is a player + { + // random durability for items (HIT TAKEN) + if (WorldConfig.GetFloatValue(WorldCfg.RateDurabilityLossDamage) > RandomHelper.randChance()) + { + byte slot = (byte)RandomHelper.IRand(0, EquipmentSlot.End - 1); + victim.ToPlayer().DurabilityPointLossForEquipSlot(slot); + } + } + + if (IsTypeId(TypeId.Player)) + { + // random durability for items (HIT DONE) + if (RandomHelper.randChance(WorldConfig.GetFloatValue(WorldCfg.RateDurabilityLossDamage))) + { + byte slot = (byte)RandomHelper.IRand(0, EquipmentSlot.End - 1); + ToPlayer().DurabilityPointLossForEquipSlot(slot); + } + } + + if (damagetype != DamageEffectType.NoDamage) + { + if (victim != this && (spellProto == null || !spellProto.HasAttribute(SpellAttr7.NoPushbackOnDamage))) + { + if (damagetype != DamageEffectType.DOT) + { + Spell spell = victim.GetCurrentSpell(CurrentSpellTypes.Generic); + if (spell != null) + { + if (spell.GetState() == SpellState.Preparing) + { + bool isCastInterrupted() + { + if (damage == 0) + return spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.ZeroDamageCancels); + + if (victim.IsPlayer() && spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamageCancelsPlayerOnly)) + return true; + + if (spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamageCancels)) + return true; + + return false; + }; + + bool isCastDelayed() + { + if (damage == 0) + return false; + + if (victim.IsPlayer() && spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamagePushbackPlayerOnly)) + return true; + + if (spell.m_spellInfo.InterruptFlags.HasAnyFlag(SpellInterruptFlags.DamagePushback)) + return true; + + return false; + } + + if (isCastInterrupted()) + victim.InterruptNonMeleeSpells(false); + else if (isCastDelayed()) + spell.Delayed(); + } + } + + if (damage != 0 && victim.IsPlayer()) + { + Spell spell1 = victim.GetCurrentSpell(CurrentSpellTypes.Channeled); + if (spell1 != null) + if (spell1.GetState() == SpellState.Casting && spell1.m_spellInfo.HasChannelInterruptFlag(SpellAuraInterruptFlags.DamageChannelDuration)) + spell1.DelayedChannel(); + } + } + } + } + + // last damage from duel opponent + if (duel_hasEnded) + { + Player he = duel_wasMounted ? victim.GetCharmer().ToPlayer() : victim.ToPlayer(); + + Cypher.Assert(he && he.duel != null); + + if (duel_wasMounted) // In this case victim==mount + victim.SetHealth(1); + else + he.SetHealth(1); + + he.duel.opponent.CombatStopWithPets(true); + he.CombatStopWithPets(true); + + he.CastSpell(he, 7267, true); // beg + he.DuelComplete(DuelCompleteType.Won); + } + } + + Log.outDebug(LogFilter.Unit, "DealDamageEnd returned {0} damage", damage); + + return damage; + } + + public long ModifyHealth(long dVal) + { + long gain = 0; + + if (dVal == 0) + return 0; + + long curHealth = (long)GetHealth(); + + long val = dVal + curHealth; + if (val <= 0) + { + SetHealth(0); + return -curHealth; + } + + long maxHealth = (long)GetMaxHealth(); + if (val < maxHealth) + { + SetHealth((ulong)val); + gain = val - curHealth; + } + else if (curHealth != maxHealth) + { + SetHealth((ulong)maxHealth); + gain = maxHealth - curHealth; + } + + if (dVal < 0) + { + HealthUpdate packet = new(); + packet.Guid = GetGUID(); + packet.Health = (long)GetHealth(); + + Player player = GetCharmerOrOwnerPlayerOrPlayerItself(); + if (player) + player.SendPacket(packet); + } + + return gain; + } + public long GetHealthGain(long dVal) + { + long gain = 0; + + if (dVal == 0) + return 0; + + long curHealth = (long)GetHealth(); + + long val = dVal + curHealth; + if (val <= 0) + { + return -curHealth; + } + + long maxHealth = (long)GetMaxHealth(); + + if (val < maxHealth) + gain = dVal; + else if (curHealth != maxHealth) + gain = maxHealth - curHealth; + + return gain; + } + + void SetImmuneToAll(bool apply, bool keepCombat) + { + if (apply) + { + AddUnitFlag(UnitFlags.ImmuneToPc | UnitFlags.ImmuneToNpc); + ValidateAttackersAndOwnTarget(); + if (keepCombat) + m_threatManager.UpdateOnlineStates(true, true); + else + m_combatManager.EndAllCombat(); + } + else + { + RemoveUnitFlag(UnitFlags.ImmuneToPc | UnitFlags.ImmuneToNpc); + m_threatManager.UpdateOnlineStates(true, true); + } + } + + void SetImmuneToPC(bool apply, bool keepCombat) + { + if (apply) + { + AddUnitFlag(UnitFlags.ImmuneToPc); + ValidateAttackersAndOwnTarget(); + if (keepCombat) + m_threatManager.UpdateOnlineStates(true, true); + else + { + List toEnd = new(); + foreach (var pair in m_combatManager.GetPvECombatRefs()) + if (pair.Value.GetOther(this).HasUnitFlag(UnitFlags.PvpAttackable)) + toEnd.Add(pair.Value); + + foreach (var pair in m_combatManager.GetPvPCombatRefs()) + if (pair.Value.GetOther(this).HasUnitFlag(UnitFlags.PvpAttackable)) + toEnd.Add(pair.Value); + + foreach (CombatReference refe in toEnd) + refe.EndCombat(); + } + } + else + { + RemoveUnitFlag(UnitFlags.ImmuneToPc); + m_threatManager.UpdateOnlineStates(true, true); + } + } + + void SetImmuneToNPC(bool apply, bool keepCombat) + { + if (apply) + { + AddUnitFlag(UnitFlags.ImmuneToNpc); + ValidateAttackersAndOwnTarget(); + if (keepCombat) + m_threatManager.UpdateOnlineStates(true, true); + else + { + List toEnd = new(); + foreach (var pair in m_combatManager.GetPvECombatRefs()) + if (!pair.Value.GetOther(this).HasUnitFlag(UnitFlags.PvpAttackable)) + toEnd.Add(pair.Value); + + foreach (var pair in m_combatManager.GetPvPCombatRefs()) + if (!pair.Value.GetOther(this).HasUnitFlag(UnitFlags.PvpAttackable)) + toEnd.Add(pair.Value); + + foreach (CombatReference refe in toEnd) + refe.EndCombat(); + } + } + else + { + RemoveUnitFlag(UnitFlags.ImmuneToNpc); + m_threatManager.UpdateOnlineStates(true, true); + } + } + + public virtual float GetBlockPercent(uint attackerLevel) { return 30.0f; } + + void UpdateReactives(uint p_time) + { + for (ReactiveType reactive = 0; reactive < ReactiveType.Max; ++reactive) + { + if (!m_reactiveTimer.ContainsKey(reactive)) + continue; + + if (m_reactiveTimer[reactive] <= p_time) + { + m_reactiveTimer[reactive] = 0; + + switch (reactive) + { + case ReactiveType.Defense: + if (HasAuraState(AuraStateType.Defensive)) + ModifyAuraState(AuraStateType.Defensive, false); + break; + case ReactiveType.Defense2: + if (HasAuraState(AuraStateType.Defensive2)) + ModifyAuraState(AuraStateType.Defensive2, false); + break; + } + } + else + { + m_reactiveTimer[reactive] -= p_time; + } + } + } + + public void RewardRage(uint baseRage) + { + float addRage = baseRage; + + // talent who gave more rage on attack + MathFunctions.AddPct(ref addRage, GetTotalAuraModifier(AuraType.ModRageFromDamageDealt)); + + addRage *= WorldConfig.GetFloatValue(WorldCfg.RatePowerRageIncome); + + ModifyPower(PowerType.Rage, (int)(addRage * 10)); + } + + public float GetPPMProcChance(uint WeaponSpeed, float PPM, SpellInfo spellProto) + { + // proc per minute chance calculation + if (PPM <= 0) + return 0.0f; + + // Apply chance modifer aura + if (spellProto != null) + { + Player modOwner = GetSpellModOwner(); + if (modOwner != null) + modOwner.ApplySpellMod(spellProto, SpellModOp.ProcFrequency, ref PPM); + } + + return (float)Math.Floor((WeaponSpeed * PPM) / 600.0f); // result is chance in percents (probability = Speed_in_sec * (PPM / 60)) + } + + public Unit GetNextRandomRaidMemberOrPet(float radius) + { + Player player = null; + if (IsTypeId(TypeId.Player)) + player = ToPlayer(); + // Should we enable this also for charmed units? + else if (IsTypeId(TypeId.Unit) && IsPet()) + player = GetOwner().ToPlayer(); + + if (player == null) + return null; + Group group = player.GetGroup(); + // When there is no group check pet presence + if (!group) + { + // We are pet now, return owner + if (player != this) + return IsWithinDistInMap(player, radius) ? player : null; + Unit pet = GetGuardianPet(); + // No pet, no group, nothing to return + if (pet == null) + return null; + // We are owner now, return pet + return IsWithinDistInMap(pet, radius) ? pet : null; + } + + List nearMembers = new(); + // reserve place for players and pets because resizing vector every unit push is unefficient (vector is reallocated then) + + for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) + { + Player target = refe.GetSource(); + if (target) + { + // IsHostileTo check duel and controlled by enemy + if (target != this && IsWithinDistInMap(target, radius) && target.IsAlive() && !IsHostileTo(target)) + nearMembers.Add(target); + + // Push player's pet to vector + Unit pet = target.GetGuardianPet(); + if (pet) + if (pet != this && IsWithinDistInMap(pet, radius) && pet.IsAlive() && !IsHostileTo(pet)) + nearMembers.Add(pet); + } + } + + if (nearMembers.Empty()) + return null; + + int randTarget = RandomHelper.IRand(0, nearMembers.Count - 1); + return nearMembers[randTarget]; + } + + public void ClearAllReactives() + { + for (ReactiveType i = 0; i < ReactiveType.Max; ++i) + m_reactiveTimer[i] = 0; + + if (HasAuraState(AuraStateType.Defensive)) + ModifyAuraState(AuraStateType.Defensive, false); + if (HasAuraState(AuraStateType.Defensive2)) + ModifyAuraState(AuraStateType.Defensive2, false); + } + + public virtual void SetPvP(bool state) + { + if (state) + AddPvpFlag(UnitPVPStateFlags.PvP); + else + RemovePvpFlag(UnitPVPStateFlags.PvP); + } + + uint CalcSpellResistedDamage(Unit attacker, Unit victim, uint damage, SpellSchoolMask schoolMask, SpellInfo spellInfo) + { + // Magic damage, check for resists + if (!Convert.ToBoolean(schoolMask & SpellSchoolMask.Magic)) + return 0; + + // Npcs can have holy resistance + if (schoolMask.HasAnyFlag(SpellSchoolMask.Holy) && victim.GetTypeId() != TypeId.Unit) + return 0; + + // Ignore spells that can't be resisted + if (spellInfo != null) + { + if (spellInfo.HasAttribute(SpellAttr4.IgnoreResistances)) + return 0; + + // Binary spells can't have damage part resisted + if (spellInfo.HasAttribute(SpellCustomAttributes.BinarySpell)) + return 0; + } + + float averageResist = CalculateAverageResistReduction(schoolMask, victim, spellInfo); + + float[] discreteResistProbability = new float[11]; + if (averageResist <= 0.1f) + { + discreteResistProbability[0] = 1.0f - 7.5f * averageResist; + discreteResistProbability[1] = 5.0f * averageResist; + discreteResistProbability[2] = 2.5f * averageResist; + } + else + { + for (uint i = 0; i < 11; ++i) + discreteResistProbability[i] = Math.Max(0.5f - 2.5f * Math.Abs(0.1f * i - averageResist), 0.0f); + } + + float roll = (float)RandomHelper.NextDouble(); + float probabilitySum = 0.0f; + + uint resistance = 0; + for (; resistance < 11; ++resistance) + if (roll < (probabilitySum += discreteResistProbability[resistance])) + break; + + float damageResisted = damage * resistance / 10f; + if (damageResisted > 0.0f) // if any damage was resisted + { + int ignoredResistance = 0; + + ignoredResistance += GetTotalAuraModifierByMiscMask(AuraType.ModIgnoreTargetResist, (int)schoolMask); + + ignoredResistance = Math.Min(ignoredResistance, 100); + MathFunctions.ApplyPct(ref damageResisted, 100 - ignoredResistance); + + // Spells with melee and magic school mask, decide whether resistance or armor absorb is higher + if (spellInfo != null && spellInfo.HasAttribute(SpellCustomAttributes.SchoolmaskNormalWithMagic)) + { + uint damageAfterArmor = CalcArmorReducedDamage(attacker, victim, damage, spellInfo, WeaponAttackType.BaseAttack); + float armorReduction = damage - damageAfterArmor; + + // pick the lower one, the weakest resistance counts + damageResisted = Math.Min(damageResisted, armorReduction); + } + } + + damageResisted = Math.Max(damageResisted, 0.0f); + return (uint)damageResisted; + } + + float CalculateAverageResistReduction(SpellSchoolMask schoolMask, Unit victim, SpellInfo spellInfo) + { + float victimResistance = (float)victim.GetResistance(schoolMask); + + // pets inherit 100% of masters penetration + // excluding traps + Player player = GetSpellModOwner(); + if (player != null && GetEntry() != SharedConst.WorldTrigger) + { + victimResistance += (float)player.GetTotalAuraModifierByMiscMask(AuraType.ModTargetResistance, (int)schoolMask); + victimResistance -= (float)player.GetSpellPenetrationItemMod(); + } + else + victimResistance += (float)GetTotalAuraModifierByMiscMask(AuraType.ModTargetResistance, (int)schoolMask); + + // holy resistance exists in pve and comes from level difference, ignore template values + if (schoolMask.HasAnyFlag(SpellSchoolMask.Holy)) + victimResistance = 0.0f; + + // Chaos Bolt exception, ignore all target resistances (unknown attribute?) + if (spellInfo != null && spellInfo.SpellFamilyName == SpellFamilyNames.Warlock && spellInfo.Id == 116858) + victimResistance = 0.0f; + + victimResistance = Math.Max(victimResistance, 0.0f); + + // level-based resistance does not apply to binary spells, and cannot be overcome by spell penetration + if (spellInfo == null || !spellInfo.HasAttribute(SpellCustomAttributes.BinarySpell)) + victimResistance += Math.Max(((float)victim.GetLevelForTarget(this) - (float)GetLevelForTarget(victim)) * 5.0f, 0.0f); + + uint bossLevel = 83; + float bossResistanceConstant = 510.0f; + uint level = victim.GetLevelForTarget(this); + float resistanceConstant; + + if (level == bossLevel) + resistanceConstant = bossResistanceConstant; + else + resistanceConstant = level * 5.0f; + + return victimResistance / (victimResistance + resistanceConstant); + } + + public void CalcAbsorbResist(DamageInfo damageInfo) + { + if (!damageInfo.GetVictim() || !damageInfo.GetVictim().IsAlive() || damageInfo.GetDamage() == 0) + return; + + uint resistedDamage = CalcSpellResistedDamage(damageInfo.GetAttacker(), damageInfo.GetVictim(), damageInfo.GetDamage(), damageInfo.GetSchoolMask(), damageInfo.GetSpellInfo()); + damageInfo.ResistDamage(resistedDamage); + + // Ignore Absorption Auras + float auraAbsorbMod = GetMaxPositiveAuraModifierByMiscMask(AuraType.ModTargetAbsorbSchool, (uint)damageInfo.GetSchoolMask()); + + MathFunctions.RoundToInterval(ref auraAbsorbMod, 0.0f, 100.0f); + + int absorbIgnoringDamage = (int)MathFunctions.CalculatePct(damageInfo.GetDamage(), auraAbsorbMod); + damageInfo.ModifyDamage(-absorbIgnoringDamage); + + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + var vSchoolAbsorbCopy = damageInfo.GetVictim().GetAuraEffectsByType(AuraType.SchoolAbsorb); + vSchoolAbsorbCopy.Sort(new AbsorbAuraOrderPred()); + + // absorb without mana cost + for (var i = 0; i < vSchoolAbsorbCopy.Count; ++i) + { + var absorbAurEff = vSchoolAbsorbCopy[i]; + if (damageInfo.GetDamage() == 0) + break; + + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication aurApp = absorbAurEff.GetBase().GetApplicationOfTarget(damageInfo.GetVictim().GetGUID()); + if (aurApp == null) + continue; + if (!Convert.ToBoolean(absorbAurEff.GetMiscValue() & (int)damageInfo.GetSchoolMask())) + continue; + + // get amount which can be still absorbed by the aura + int currentAbsorb = absorbAurEff.GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + uint tempAbsorb = (uint)currentAbsorb; + + bool defaultPrevented = false; + + absorbAurEff.GetBase().CallScriptEffectAbsorbHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb, ref defaultPrevented); + currentAbsorb = (int)tempAbsorb; + + if (!defaultPrevented) + { + + // absorb must be smaller than the damage itself + currentAbsorb = MathFunctions.RoundToInterval(ref currentAbsorb, 0, damageInfo.GetDamage()); + + damageInfo.AbsorbDamage((uint)currentAbsorb); + + tempAbsorb = (uint)currentAbsorb; + absorbAurEff.GetBase().CallScriptEffectAfterAbsorbHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb); + + // Check if our aura is using amount to count damage + if (absorbAurEff.GetAmount() >= 0) + { + // Reduce shield amount + absorbAurEff.ChangeAmount(absorbAurEff.GetAmount() - currentAbsorb); + // Aura cannot absorb anything more - remove it + if (absorbAurEff.GetAmount() <= 0) + absorbAurEff.GetBase().Remove(AuraRemoveMode.EnemySpell); + } + } + + if (currentAbsorb != 0) + { + SpellAbsorbLog absorbLog = new(); + absorbLog.Attacker = damageInfo.GetAttacker().GetGUID(); + absorbLog.Victim = damageInfo.GetVictim().GetGUID(); + absorbLog.Caster = absorbAurEff.GetBase().GetCasterGUID(); + absorbLog.AbsorbedSpellID = damageInfo.GetSpellInfo() != null ? damageInfo.GetSpellInfo().Id : 0; + absorbLog.AbsorbSpellID = absorbAurEff.GetId(); + absorbLog.Absorbed = currentAbsorb; + absorbLog.OriginalDamage = damageInfo.GetOriginalDamage(); + absorbLog.LogData.Initialize(damageInfo.GetVictim()); + SendCombatLogMessage(absorbLog); + } + } + + // absorb by mana cost + var vManaShieldCopy = damageInfo.GetVictim().GetAuraEffectsByType(AuraType.ManaShield); + foreach (var absorbAurEff in vManaShieldCopy) + { + if (damageInfo.GetDamage() == 0) + break; + + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication aurApp = absorbAurEff.GetBase().GetApplicationOfTarget(damageInfo.GetVictim().GetGUID()); + if (aurApp == null) + continue; + // check damage school mask + if (!Convert.ToBoolean(absorbAurEff.GetMiscValue() & (int)damageInfo.GetSchoolMask())) + continue; + + // get amount which can be still absorbed by the aura + int currentAbsorb = absorbAurEff.GetAmount(); + // aura with infinite absorb amount - let the scripts handle absorbtion amount, set here to 0 for safety + if (currentAbsorb < 0) + currentAbsorb = 0; + + uint tempAbsorb = (uint)currentAbsorb; + + bool defaultPrevented = false; + + absorbAurEff.GetBase().CallScriptEffectManaShieldHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb, ref defaultPrevented); + currentAbsorb = (int)tempAbsorb; + + if (!defaultPrevented) + { + // absorb must be smaller than the damage itself + currentAbsorb = MathFunctions.RoundToInterval(ref currentAbsorb, 0, damageInfo.GetDamage()); + + int manaReduction = currentAbsorb; + + // lower absorb amount by talents + float manaMultiplier = absorbAurEff.GetSpellEffectInfo().CalcValueMultiplier(absorbAurEff.GetCaster()); + if (manaMultiplier != 0) + manaReduction = (int)(manaReduction * manaMultiplier); + + int manaTaken = -damageInfo.GetVictim().ModifyPower(PowerType.Mana, -manaReduction); + + // take case when mana has ended up into account + currentAbsorb = currentAbsorb != 0 ? (currentAbsorb * (manaTaken / manaReduction)) : 0; + + damageInfo.AbsorbDamage((uint)currentAbsorb); + + tempAbsorb = (uint)currentAbsorb; + absorbAurEff.GetBase().CallScriptEffectAfterManaShieldHandlers(absorbAurEff, aurApp, damageInfo, ref tempAbsorb); + + // Check if our aura is using amount to count damage + if (absorbAurEff.GetAmount() >= 0) + { + absorbAurEff.ChangeAmount(absorbAurEff.GetAmount() - currentAbsorb); + if ((absorbAurEff.GetAmount() <= 0)) + absorbAurEff.GetBase().Remove(AuraRemoveMode.EnemySpell); + } + } + + if (currentAbsorb != 0) + { + SpellAbsorbLog absorbLog = new(); + absorbLog.Attacker = damageInfo.GetAttacker().GetGUID(); + absorbLog.Victim = damageInfo.GetVictim().GetGUID(); + absorbLog.Caster = absorbAurEff.GetBase().GetCasterGUID(); + absorbLog.AbsorbedSpellID = damageInfo.GetSpellInfo() != null ? damageInfo.GetSpellInfo().Id : 0; + absorbLog.AbsorbSpellID = absorbAurEff.GetId(); + absorbLog.Absorbed = currentAbsorb; + absorbLog.OriginalDamage = damageInfo.GetOriginalDamage(); + absorbLog.LogData.Initialize(damageInfo.GetVictim()); + SendCombatLogMessage(absorbLog); + } + } + + damageInfo.ModifyDamage(absorbIgnoringDamage); + + // split damage auras - only when not damaging self + if (damageInfo.GetVictim() != this) + { + // We're going to call functions which can modify content of the list during iteration over it's elements + // Let's copy the list so we can prevent iterator invalidation + var vSplitDamagePctCopy = damageInfo.GetVictim().GetAuraEffectsByType(AuraType.SplitDamagePct); + foreach (var itr in vSplitDamagePctCopy) + { + if (damageInfo.GetDamage() == 0) + break; + + // Check if aura was removed during iteration - we don't need to work on such auras + AuraApplication aurApp = itr.GetBase().GetApplicationOfTarget(damageInfo.GetVictim().GetGUID()); + if (aurApp == null) + continue; + + // check damage school mask + if (!Convert.ToBoolean(itr.GetMiscValue() & (int)damageInfo.GetSchoolMask())) + continue; + + // Damage can be splitted only if aura has an alive caster + Unit caster = itr.GetCaster(); + if (!caster || (caster == damageInfo.GetVictim()) || !caster.IsInWorld || !caster.IsAlive()) + continue; + + uint splitDamage = MathFunctions.CalculatePct(damageInfo.GetDamage(), itr.GetAmount()); + + itr.GetBase().CallScriptEffectSplitHandlers(itr, aurApp, damageInfo, splitDamage); + + // absorb must be smaller than the damage itself + splitDamage = MathFunctions.RoundToInterval(ref splitDamage, 0, damageInfo.GetDamage()); + + damageInfo.AbsorbDamage(splitDamage); + + // check if caster is immune to damage + if (caster.IsImmunedToDamage(damageInfo.GetSchoolMask())) + { + damageInfo.GetVictim().SendSpellMiss(caster, itr.GetSpellInfo().Id, SpellMissInfo.Immune); + continue; + } + + uint split_absorb = 0; + DealDamageMods(caster, ref splitDamage, ref split_absorb); + + SpellNonMeleeDamage log = new(this, caster, itr.GetSpellInfo(), itr.GetBase().GetSpellVisual(), damageInfo.GetSchoolMask(), itr.GetBase().GetCastGUID()); + CleanDamage cleanDamage = new(splitDamage, 0, WeaponAttackType.BaseAttack, MeleeHitOutcome.Normal); + DealDamage(caster, splitDamage, cleanDamage, DamageEffectType.Direct, damageInfo.GetSchoolMask(), itr.GetSpellInfo(), false); + log.damage = splitDamage; + log.originalDamage = splitDamage; + log.absorb = split_absorb; + SendSpellNonMeleeDamageLog(log); + + // break 'Fear' and similar auras + ProcSkillsAndAuras(caster, ProcFlags.None, ProcFlags.TakenSpellMagicDmgClassNeg, ProcFlagsSpellType.Damage, ProcFlagsSpellPhase.Hit, ProcFlagsHit.None, null, damageInfo, null); + } + } + } + + public void CalcHealAbsorb(HealInfo healInfo) + { + if (healInfo.GetHeal() == 0) + return; + + // Need remove expired auras after + bool existExpired = false; + + // absorb without mana cost + var vHealAbsorb = healInfo.GetTarget().GetAuraEffectsByType(AuraType.SchoolHealAbsorb); + for (var i = 0; i < vHealAbsorb.Count; ++i) + { + var eff = vHealAbsorb[i]; + if (healInfo.GetHeal() <= 0) + break; + + if (!Convert.ToBoolean(eff.GetMiscValue() & (int)healInfo.GetSpellInfo().SchoolMask)) + continue; + + // Max Amount can be absorbed by this aura + int currentAbsorb = eff.GetAmount(); + + // Found empty aura (impossible but..) + if (currentAbsorb <= 0) + { + existExpired = true; + continue; + } + + // currentAbsorb - damage can be absorbed by shield + // If need absorb less damage + currentAbsorb = (int)Math.Min(healInfo.GetHeal(), currentAbsorb); + + healInfo.AbsorbHeal((uint)currentAbsorb); + + // Reduce shield amount + eff.ChangeAmount(eff.GetAmount() - currentAbsorb); + // Need remove it later + if (eff.GetAmount() <= 0) + existExpired = true; + } + + // Remove all expired absorb auras + if (existExpired) + { + for (var i = 0; i < vHealAbsorb.Count;) + { + AuraEffect auraEff = vHealAbsorb[i]; + ++i; + if (auraEff.GetAmount() <= 0) + { + uint removedAuras = healInfo.GetTarget().m_removedAurasCount; + auraEff.GetBase().Remove(AuraRemoveMode.EnemySpell); + if (removedAuras + 1 < healInfo.GetTarget().m_removedAurasCount) + i = 0; + } + } + } + } + + public uint CalcArmorReducedDamage(Unit attacker, Unit victim, uint damage, SpellInfo spellInfo, WeaponAttackType attackType = WeaponAttackType.Max) + { + float armor = victim.GetArmor(); + + armor *= victim.GetArmorMultiplierForTarget(attacker); + + // bypass enemy armor by SPELL_AURA_BYPASS_ARMOR_FOR_CASTER + int armorBypassPct = 0; + var reductionAuras = victim.GetAuraEffectsByType(AuraType.BypassArmorForCaster); + foreach (var eff in reductionAuras) + if (eff.GetCasterGUID() == GetGUID()) + armorBypassPct += eff.GetAmount(); + armor = MathFunctions.CalculatePct(armor, 100 - Math.Min(armorBypassPct, 100)); + + // Ignore enemy armor by SPELL_AURA_MOD_TARGET_RESISTANCE aura + armor += GetTotalAuraModifierByMiscMask(AuraType.ModTargetResistance, (int)SpellSchoolMask.Normal); + + if (spellInfo != null) + { + Player modOwner = GetSpellModOwner(); + if (modOwner != null) + modOwner.ApplySpellMod(spellInfo, SpellModOp.TargetResistance, ref armor); + } + + var resIgnoreAuras = GetAuraEffectsByType(AuraType.ModIgnoreTargetResist); + foreach (var eff in resIgnoreAuras) + { + if (eff.GetMiscValue().HasAnyFlag((int)SpellSchoolMask.Normal) && eff.IsAffectingSpell(spellInfo)) + armor = (float)Math.Floor(MathFunctions.AddPct(ref armor, -eff.GetAmount())); + } + + // Apply Player CR_ARMOR_PENETRATION rating + if (IsTypeId(TypeId.Player)) + { + float arpPct = ToPlayer().GetRatingBonusValue(CombatRating.ArmorPenetration); + + // no more than 100% + MathFunctions.RoundToInterval(ref arpPct, 0.0f, 100.0f); + + float maxArmorPen; + if (victim.GetLevelForTarget(attacker) < 60) + maxArmorPen = 400 + 85 * victim.GetLevelForTarget(attacker); + else + maxArmorPen = 400 + 85 * victim.GetLevelForTarget(attacker) + 4.5f * 85 * (victim.GetLevelForTarget(attacker) - 59); + + // Cap armor penetration to this number + maxArmorPen = Math.Min((armor + maxArmorPen) / 3.0f, armor); + // Figure out how much armor do we ignore + armor -= MathFunctions.CalculatePct(maxArmorPen, arpPct); + } + + if (MathFunctions.fuzzyLe(armor, 0.0f)) + return damage; + + uint attackerLevel = attacker.GetLevelForTarget(victim); + // Expansion and ContentTuningID necessary? Does Player get a ContentTuningID too ? + float armorConstant = Global.DB2Mgr.EvaluateExpectedStat(ExpectedStatType.ArmorConstant, attackerLevel, -2, 0, attacker.GetClass()); + if ((armor + armorConstant) == 0) + return damage; + + float mitigation = Math.Min(armor / (armor + armorConstant), 0.85f); + return Math.Max((uint)(damage * (1.0f - mitigation)), 1); + } + + public uint MeleeDamageBonusDone(Unit victim, uint pdamage, WeaponAttackType attType, DamageEffectType damagetype, SpellInfo spellProto = null) + { + if (victim == null || pdamage == 0) + return 0; + + uint creatureTypeMask = victim.GetCreatureTypeMask(); + + // Done fixed damage bonus auras + int DoneFlatBenefit = 0; + + // ..done + DoneFlatBenefit += GetTotalAuraModifierByMiscMask(AuraType.ModDamageDoneCreature, (int)creatureTypeMask); + + // ..done + // SPELL_AURA_MOD_DAMAGE_DONE included in weapon damage + + // ..done (base at attack power for marked target and base at attack power for creature type) + int APbonus = 0; + + if (attType == WeaponAttackType.RangedAttack) + { + APbonus += victim.GetTotalAuraModifier(AuraType.RangedAttackPowerAttackerBonus); + + // ..done (base at attack power and creature type) + APbonus += GetTotalAuraModifierByMiscMask(AuraType.ModRangedAttackPowerVersus, (int)creatureTypeMask); + } + else + { + APbonus += victim.GetTotalAuraModifier(AuraType.MeleeAttackPowerAttackerBonus); + + // ..done (base at attack power and creature type) + APbonus += GetTotalAuraModifierByMiscMask(AuraType.ModMeleeAttackPowerVersus, (int)creatureTypeMask); + } + + if (APbonus != 0) // Can be negative + { + bool normalized = spellProto != null && spellProto.HasEffect(SpellEffectName.NormalizedWeaponDmg); + DoneFlatBenefit += (int)(APbonus / 3.5f * GetAPMultiplier(attType, normalized)); + } + + // Done total percent damage auras + float DoneTotalMod = 1.0f; + + + if (spellProto != null) + { + // Some spells don't benefit from pct done mods + // mods for SPELL_SCHOOL_MASK_NORMAL are already factored in base melee damage calculation + if (!spellProto.HasAttribute(SpellAttr6.NoDonePctDamageMods) && !spellProto.GetSchoolMask().HasAnyFlag(SpellSchoolMask.Normal)) + { + float maxModDamagePercentSchool = 0.0f; + Player thisPlayer = ToPlayer(); + if (thisPlayer != null) + { + for (var i = SpellSchools.Holy; i < SpellSchools.Max; ++i) + { + if (Convert.ToBoolean((int)spellProto.GetSchoolMask() & (1 << (int)i))) + maxModDamagePercentSchool = Math.Max(maxModDamagePercentSchool, thisPlayer.m_activePlayerData.ModDamageDonePercent[(int)i]); + } + } + else + maxModDamagePercentSchool = GetTotalAuraMultiplierByMiscMask(AuraType.ModDamagePercentDone, (uint)spellProto.GetSchoolMask()); + + DoneTotalMod *= maxModDamagePercentSchool; + } + else + { + // melee attack + foreach (AuraEffect autoAttackDamage in GetAuraEffectsByType(AuraType.ModAutoAttackDamage)) + MathFunctions.AddPct(ref DoneTotalMod, autoAttackDamage.GetAmount()); + } + } + + DoneTotalMod *= GetTotalAuraMultiplierByMiscMask(AuraType.ModDamageDoneVersus, creatureTypeMask); + + // bonus against aurastate + DoneTotalMod *= GetTotalAuraMultiplier(AuraType.ModDamageDoneVersusAurastate, aurEff => + { + if (victim.HasAuraState((AuraStateType)aurEff.GetMiscValue())) + return true; + return false; + }); + + // Add SPELL_AURA_MOD_DAMAGE_DONE_FOR_MECHANIC percent bonus + if (spellProto != null) + MathFunctions.AddPct(ref DoneTotalMod, GetTotalAuraModifierByMiscValue(AuraType.ModDamageDoneForMechanic, (int)spellProto.Mechanic)); + + float tmpDamage = (pdamage + DoneFlatBenefit) * DoneTotalMod; + + // apply spellmod to Done damage + if (spellProto != null) + { + Player modOwner = GetSpellModOwner(); + if (modOwner != null) + { + if (damagetype == DamageEffectType.DOT) + modOwner.ApplySpellMod(spellProto, SpellModOp.PeriodicHealingAndDamage, ref tmpDamage); + else + modOwner.ApplySpellMod(spellProto, SpellModOp.HealingAndDamage, ref tmpDamage); + } + } + + // bonus result can be negative + return (uint)Math.Max(tmpDamage, 0.0f); + } + + public uint MeleeDamageBonusTaken(Unit attacker, uint pdamage, WeaponAttackType attType, DamageEffectType damagetype, SpellInfo spellProto = null) + { + if (pdamage == 0) + return 0; + + int TakenFlatBenefit = 0; + float TakenTotalCasterMod = 0.0f; + + // get all auras from caster that allow the spell to ignore resistance (sanctified wrath) + int attackSchoolMask = (int)(spellProto != null ? spellProto.GetSchoolMask() : SpellSchoolMask.Normal); + TakenTotalCasterMod += attacker.GetTotalAuraModifierByMiscMask(AuraType.ModIgnoreTargetResist, attackSchoolMask); + + // ..taken + TakenFlatBenefit += GetTotalAuraModifierByMiscMask(AuraType.ModDamageTaken, (int)attacker.GetMeleeDamageSchoolMask()); + + if (attType != WeaponAttackType.RangedAttack) + TakenFlatBenefit += GetTotalAuraModifier(AuraType.ModMeleeDamageTaken); + else + TakenFlatBenefit += GetTotalAuraModifier(AuraType.ModRangedDamageTaken); + + // Taken total percent damage auras + float TakenTotalMod = 1.0f; + + // ..taken + TakenTotalMod *= GetTotalAuraMultiplierByMiscMask(AuraType.ModDamagePercentTaken, (uint)attacker.GetMeleeDamageSchoolMask()); + + // .. taken pct (special attacks) + if (spellProto != null) + { + // From caster spells + TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModSchoolMaskDamageFromCaster, aurEff => + { + return aurEff.GetCasterGUID() == attacker.GetGUID() && (aurEff.GetMiscValue() & (int)spellProto.GetSchoolMask()) != 0; + }); + + TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModSpellDamageFromCaster, aurEff => + { + return aurEff.GetCasterGUID() == attacker.GetGUID() && aurEff.IsAffectingSpell(spellProto); + }); + + // Mod damage from spell mechanic + uint mechanicMask = spellProto.GetAllEffectsMechanicMask(); + + // Shred, Maul - "Effects which increase Bleed damage also increase Shred damage" + if (spellProto.SpellFamilyName == SpellFamilyNames.Druid && spellProto.SpellFamilyFlags[0].HasAnyFlag(0x00008800u)) + mechanicMask |= (1 << (int)Mechanics.Bleed); + + if (mechanicMask != 0) + { + TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModMechanicDamageTakenPercent, aurEff => + { + if ((mechanicMask & (1 << (aurEff.GetMiscValue()))) != 0) + return true; + return false; + }); + } + + if (damagetype == DamageEffectType.DOT) + TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModPeriodicDamageTaken, aurEff => (aurEff.GetMiscValue() & (uint)spellProto.GetSchoolMask()) != 0); + } + + AuraEffect cheatDeath = GetAuraEffect(45182, 0); + if (cheatDeath != null) + MathFunctions.AddPct(ref TakenTotalMod, cheatDeath.GetAmount()); + + if (attType != WeaponAttackType.RangedAttack) + TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModMeleeDamageTakenPct); + else + TakenTotalMod *= GetTotalAuraMultiplier(AuraType.ModRangedDamageTakenPct); + + // Versatility + Player modOwner = GetSpellModOwner(); + if (modOwner) + { + // only 50% of SPELL_AURA_MOD_VERSATILITY for damage reduction + float versaBonus = modOwner.GetTotalAuraModifier(AuraType.ModVersatility) / 2.0f; + MathFunctions.AddPct(ref TakenTotalMod, -(modOwner.GetRatingBonusValue(CombatRating.VersatilityDamageTaken) + versaBonus)); + } + + float tmpDamage = 0.0f; + + if (TakenTotalCasterMod != 0) + { + if (TakenFlatBenefit < 0) + { + if (TakenTotalMod < 1) + tmpDamage = (((MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod) + TakenFlatBenefit) * TakenTotalMod) + MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod)); + else + tmpDamage = (((MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod) + TakenFlatBenefit) + MathFunctions.CalculatePct(pdamage, TakenTotalCasterMod)) * TakenTotalMod); + } + else if (TakenTotalMod < 1) + tmpDamage = ((MathFunctions.CalculatePct(pdamage + TakenFlatBenefit, TakenTotalCasterMod) * TakenTotalMod) + MathFunctions.CalculatePct(pdamage + TakenFlatBenefit, TakenTotalCasterMod)); + } + if (tmpDamage == 0) + tmpDamage = (pdamage + TakenFlatBenefit) * TakenTotalMod; + + // bonus result can be negative + return (uint)Math.Max(tmpDamage, 0.0f); + } + + bool IsBlockCritical() + { + if (RandomHelper.randChance(GetTotalAuraModifier(AuraType.ModBlockCritChance))) + return true; + return false; + } + + public virtual SpellSchoolMask GetMeleeDamageSchoolMask() { return SpellSchoolMask.None; } + + float CalculateDefaultCoefficient(SpellInfo spellInfo, DamageEffectType damagetype) + { + // Damage over Time spells bonus calculation + float DotFactor = 1.0f; + if (damagetype == DamageEffectType.DOT) + { + + int DotDuration = spellInfo.GetDuration(); + if (!spellInfo.IsChanneled() && DotDuration > 0) + DotFactor = DotDuration / 15000.0f; + + uint DotTicks = spellInfo.GetMaxTicks(); + if (DotTicks != 0) + DotFactor /= DotTicks; + } + + uint CastingTime = (uint)(spellInfo.IsChanneled() ? spellInfo.GetDuration() : spellInfo.CalcCastTime()); + // Distribute Damage over multiple effects, reduce by AoE + CastingTime = GetCastingTimeForBonus(spellInfo, damagetype, CastingTime); + + // As wowwiki says: C = (Cast Time / 3.5) + return (CastingTime / 3500.0f) * DotFactor; + } + + public virtual void UpdateDamageDoneMods(WeaponAttackType attackType) + { + UnitMods unitMod = attackType switch + { + WeaponAttackType.BaseAttack => UnitMods.DamageMainHand, + WeaponAttackType.OffAttack => UnitMods.DamageOffHand, + WeaponAttackType.RangedAttack => UnitMods.DamageRanged, + _ => throw new NotImplementedException(), + }; + + float amount = GetTotalAuraModifier(AuraType.ModDamageDone, aurEff => + { + if ((aurEff.GetMiscValue() & (int)SpellSchoolMask.Normal) == 0) + return false; + + return CheckAttackFitToAuraRequirement(attackType, aurEff); + }); + + SetStatFlatModifier(unitMod, UnitModifierFlatType.Total, amount); + } + + public void UpdateAllDamageDoneMods() + { + for (var attackType = WeaponAttackType.BaseAttack; attackType < WeaponAttackType.Max; ++attackType) + UpdateDamageDoneMods(attackType); + } + + public void UpdateDamagePctDoneMods(WeaponAttackType attackType) + { + (UnitMods unitMod, float factor) = attackType switch + { + WeaponAttackType.BaseAttack => (UnitMods.DamageMainHand, 1.0f), + WeaponAttackType.OffAttack => (UnitMods.DamageOffHand, 0.5f), + WeaponAttackType.RangedAttack => (UnitMods.DamageRanged, 1.0f), + _ => throw new NotImplementedException(), + }; + + factor *= GetTotalAuraMultiplier(AuraType.ModDamagePercentDone, aurEff => + { + if (!aurEff.GetMiscValue().HasAnyFlag((int)SpellSchoolMask.Normal)) + return false; + + return CheckAttackFitToAuraRequirement(attackType, aurEff); + }); + + if (attackType == WeaponAttackType.OffAttack) + factor *= GetTotalAuraMultiplier(AuraType.ModOffhandDamagePct, auraEffect => CheckAttackFitToAuraRequirement(attackType, auraEffect)); + + SetStatPctModifier(unitMod, UnitModifierPctType.Total, factor); + } + + public void UpdateAllDamagePctDoneMods() + { + for (var attackType = WeaponAttackType.BaseAttack; attackType < WeaponAttackType.Max; ++attackType) + UpdateDamagePctDoneMods(attackType); + } + + public CombatManager GetCombatManager() { return m_combatManager; } + + // Exposes the threat manager directly - be careful when interfacing with this + // As a general rule of thumb, any unit pointer MUST be null checked BEFORE passing it to threatmanager methods + // threatmanager will NOT null check your pointers for you - misuse = crash + public ThreatManager GetThreatManager() { return m_threatManager; } } } diff --git a/Source/Game/Handlers/PetHandler.cs b/Source/Game/Handlers/PetHandler.cs index 62af747b5..6cd04cf81 100644 --- a/Source/Game/Handlers/PetHandler.cs +++ b/Source/Game/Handlers/PetHandler.cs @@ -122,7 +122,6 @@ namespace Game return; pet.AttackStop(); - pet.ClearInPetCombat(); } void HandlePetActionHelper(Unit pet, ObjectGuid guid1, uint spellid, ActiveStates flag, ObjectGuid guid2, float x, float y, float z) @@ -156,7 +155,6 @@ namespace Game case CommandStates.Follow: //spellid=1792 //FOLLOW pet.AttackStop(); pet.InterruptNonMeleeSpells(false); - pet.ClearInPetCombat(); pet.GetMotionMaster().MoveFollow(GetPlayer(), SharedConst.PetFollowDist, pet.GetFollowAngle()); charmInfo.SetCommandState(CommandStates.Follow); @@ -271,7 +269,6 @@ namespace Game { case ReactStates.Passive: //passive pet.AttackStop(); - pet.ClearInPetCombat(); goto case ReactStates.Defensive; case ReactStates.Defensive: //recovery case ReactStates.Aggressive: //activete diff --git a/Source/Game/Maps/Map.cs b/Source/Game/Maps/Map.cs index de1aecc0a..39dff8df8 100644 --- a/Source/Game/Maps/Map.cs +++ b/Source/Game/Maps/Map.cs @@ -762,22 +762,13 @@ namespace Game.Maps // Handle updates for creatures in combat with player and are more than 60 yards away if (player.IsInCombat()) { - List updateList = new(); - HostileReference refe = player.GetHostileRefManager().GetFirst(); - - while (refe != null) + foreach (var pair in player.GetCombatManager().GetPvECombatRefs()) { - Unit unit = refe.GetSource().GetOwner(); - if (unit) - if (unit.ToCreature() && unit.GetMapId() == player.GetMapId() && !unit.IsWithinDistInMap(player, GetVisibilityRange(), false)) - updateList.Add(unit.ToCreature()); - - refe = refe.Next(); + Creature unit = pair.Value.GetOther(player).ToCreature(); + if (unit != null) + if (unit.GetMapId() == player.GetMapId() && !unit.IsWithinDistInMap(player, GetVisibilityRange(), false)) + VisitNearbyCellsOf(unit, grid_object_update, world_object_update); } - - // Process deferred update list for player - foreach (Creature c in updateList) - VisitNearbyCellsOf(c, grid_object_update, world_object_update); } } @@ -927,7 +918,7 @@ namespace Game.Maps player.UpdateZone(MapConst.InvalidZone, 0); Global.ScriptMgr.OnPlayerLeaveMap(this, player); - player.GetHostileRefManager().DeleteReferences(); // multithreading crashfix + player.CombatStop(); bool inWorld = player.IsInWorld; player.RemoveFromWorld(); diff --git a/Source/Game/Maps/ObjectGridLoader.cs b/Source/Game/Maps/ObjectGridLoader.cs index 390750468..832dd031e 100644 --- a/Source/Game/Maps/ObjectGridLoader.cs +++ b/Source/Game/Maps/ObjectGridLoader.cs @@ -221,7 +221,7 @@ namespace Game.Maps creature.RemoveAllDynObjects(); creature.RemoveAllAreaTriggers(); - if (creature.IsInCombat() || !creature.GetThreatManager().IsThreatListsEmpty()) + if (creature.IsInCombat()) { creature.CombatStop(); creature.GetThreatManager().ClearAllThreat(); diff --git a/Source/Game/Movement/Generators/FlightPathMovementGenerator.cs b/Source/Game/Movement/Generators/FlightPathMovementGenerator.cs index 37485e00c..e32766d0b 100644 --- a/Source/Game/Movement/Generators/FlightPathMovementGenerator.cs +++ b/Source/Game/Movement/Generators/FlightPathMovementGenerator.cs @@ -108,7 +108,6 @@ namespace Game.Movement if (owner.m_taxi.Empty()) { - owner.GetHostileRefManager().SetOnlineOfflineState(true); // update z position to ground and orientation for landing point // this prevent cheating with landing point at lags // when client side flight end early in comparison server side @@ -122,8 +121,8 @@ namespace Game.Movement public override void DoReset(Player owner) { - owner.GetHostileRefManager().SetOnlineOfflineState(false); owner.AddUnitState(UnitState.InFlight); + owner.CombatStopWithPets(); owner.AddUnitFlag(UnitFlags.RemoveClientControl | UnitFlags.TaxiFlight); MoveSplineInit init = new(owner); diff --git a/Source/Game/Server/WorldSession.cs b/Source/Game/Server/WorldSession.cs index 8248af40d..c860ecbb5 100644 --- a/Source/Game/Server/WorldSession.cs +++ b/Source/Game/Server/WorldSession.cs @@ -108,7 +108,7 @@ namespace Game //FIXME: logout must be delayed in case lost connection with client in time of combat if (GetPlayer().GetDeathTimer() != 0) { - _player.GetHostileRefManager().DeleteReferences(); + _player.CombatStop(); _player.BuildPlayerRepop(); _player.RepopAtGraveyard(); } diff --git a/Source/Game/Spells/Auras/AuraEffect.cs b/Source/Game/Spells/Auras/AuraEffect.cs index 2fb6487a5..b7b6c96b0 100644 --- a/Source/Game/Spells/Auras/AuraEffect.cs +++ b/Source/Game/Spells/Auras/AuraEffect.cs @@ -1522,6 +1522,8 @@ namespace Game.Spells } } } + + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.ModScale)] @@ -1596,13 +1598,21 @@ namespace Game.Spells } } } - target.CombatStop(); + + if (target.GetMap().IsDungeon()) // feign death does not remove combat in dungeons + { + target.AttackStop(); + Player targetPlayer = target.ToPlayer(); + if (targetPlayer != null) + targetPlayer.SendAttackSwingCancelAttack(); + } + else + target.CombatStop(false, false); // prevent interrupt message if (GetCasterGUID() == target.GetGUID() && target.GetCurrentSpell(CurrentSpellTypes.Generic) != null) target.FinishSpell(CurrentSpellTypes.Generic, false); target.InterruptNonMeleeSpells(true); - target.GetHostileRefManager().DeleteReferences(); // stop handling the effect if it was removed by linked event if (aurApp.HasRemoveMode()) @@ -1612,6 +1622,10 @@ namespace Game.Spells target.AddUnitFlag2(UnitFlags2.FeignDeath); target.AddDynamicFlag(UnitDynFlags.Dead); target.AddUnitState(UnitState.Died); + + Creature creature = target.ToCreature(); + if (creature != null) + creature.SetReactState(ReactStates.Passive); } else { @@ -1619,7 +1633,13 @@ namespace Game.Spells target.RemoveUnitFlag2(UnitFlags2.FeignDeath); target.RemoveDynamicFlag(UnitDynFlags.Dead); target.ClearUnitState(UnitState.Died); + + Creature creature = target.ToCreature(); + if (creature != null) + creature.InitializeReactState(); } + + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.ModUnattackable)] @@ -1641,7 +1661,17 @@ namespace Game.Spells // call functions which may have additional effects after chainging state of unit if (apply && mode.HasAnyFlag(AuraEffectHandleModes.Real)) - target.CombatStop(); + { + if (target.GetMap().IsDungeon()) + { + target.AttackStop(); + Player targetPlayer = target.ToPlayer(); + if (targetPlayer != null) + targetPlayer.SendAttackSwingCancelAttack(); + } + else + target.CombatStop(); + } } [AuraEffectHandler(AuraType.ModDisarm)] @@ -2239,18 +2269,7 @@ namespace Game.Spells if (!mode.HasAnyFlag(AuraEffectHandleModes.ChangeAmountMask)) return; - Unit target = aurApp.GetTarget(); - for (byte i = 0; i < (int)SpellSchools.Max; ++i) - if (Convert.ToBoolean(GetMiscValue() & (1 << i))) - { - if (apply) - MathFunctions.AddPct(ref target.m_threatModifier[i], GetAmount()); - else - { - float amount = target.GetTotalAuraMultiplierByMiscMask(AuraType.ModThreat, 1u << i); - target.m_threatModifier[i] = amount; - } - } + aurApp.GetTarget().GetThreatManager().UpdateMySpellSchoolModifiers(); } [AuraEffectHandler(AuraType.ModTotalThreat)] @@ -2266,7 +2285,7 @@ namespace Game.Spells Unit caster = GetCaster(); if (caster != null && caster.IsAlive()) - target.GetHostileRefManager().AddTempThreat(GetAmount(), apply); + caster.GetThreatManager().UpdateMyTempModifiers(); } [AuraEffectHandler(AuraType.ModTaunt)] @@ -2280,17 +2299,22 @@ namespace Game.Spells if (!target.IsAlive() || !target.CanHaveThreatList()) return; - Unit caster = GetCaster(); - if (caster == null || !caster.IsAlive()) + target.GetThreatManager().TauntUpdate(); + } + + [AuraEffectHandler(AuraType.ModDetaunt)] + void HandleModDetaunt(AuraApplication aurApp, AuraEffectHandleModes mode, bool apply) + { + if (!mode.HasAnyFlag(AuraEffectHandleModes.Real)) return; - if (apply) - target.TauntApply(caster); - else - { - // When taunt aura fades out, mob will switch to previous target if current has less than 1.1 * secondthreat - target.TauntFadeOut(caster); - } + Unit caster = GetCaster(); + Unit target = aurApp.GetTarget(); + + if (!caster || !caster.IsAlive() || !target.IsAlive() || !caster.CanHaveThreatList()) + return; + + caster.GetThreatManager().TauntUpdate(); } /*****************************/ @@ -2305,6 +2329,7 @@ namespace Game.Spells Unit target = aurApp.GetTarget(); target.SetControlled(apply, UnitState.Confused); + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.ModFear)] @@ -2316,6 +2341,7 @@ namespace Game.Spells Unit target = aurApp.GetTarget(); target.SetControlled(apply, UnitState.Fleeing); + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.ModStun)] @@ -2327,6 +2353,7 @@ namespace Game.Spells Unit target = aurApp.GetTarget(); target.SetControlled(apply, UnitState.Stunned); + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.ModRoot)] @@ -2339,6 +2366,7 @@ namespace Game.Spells Unit target = aurApp.GetTarget(); target.SetControlled(apply, UnitState.Root); + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.PreventsFleeing)] @@ -2743,6 +2771,8 @@ namespace Game.Spells && GetSpellInfo().HasAttribute(SpellAttr2.DamageReducedShield)) target.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.StealthOrInvis); } + + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.DamageImmunity)] @@ -2753,6 +2783,8 @@ namespace Game.Spells Unit target = aurApp.GetTarget(); m_spellInfo.ApplyAllSpellImmunitiesTo(target, GetSpellEffectInfo(), apply); + + target.GetThreatManager().UpdateOnlineStates(true, false); } [AuraEffectHandler(AuraType.DispelImmunity)] @@ -4466,7 +4498,7 @@ namespace Game.Spells player.GetReputationMgr().ApplyForceReaction(factionId, factionRank, apply); player.GetReputationMgr().SendForceReactions(); - // stop fighting if at apply forced rank friendly or at remove real rank friendly + // stop fighting at apply (if forced rank friendly) or at remove (if real rank friendly) if ((apply && factionRank >= ReputationRank.Friendly) || (!apply && player.GetReputationRank(factionId) >= ReputationRank.Friendly)) player.StopAttackFaction(factionId); } diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index e4c55c2ec..74702747d 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -2033,7 +2033,7 @@ namespace Game.Spells // spellHitTarget can be null if spell is missed in DoSpellHitOnUnit if (missInfo != SpellMissInfo.Evade && spellHitTarget && !m_caster.IsFriendlyTo(unit) && (!IsPositive() || m_spellInfo.HasEffect(SpellEffectName.Dispel))) { - m_caster.CombatStart(unit, m_spellInfo.HasInitialAggro()); + m_caster.AttackedTarget(unit, m_spellInfo.HasInitialAggro()); if (!unit.IsStandState()) unit.SetStandState(UnitStandStateType.Stand); @@ -2133,7 +2133,8 @@ namespace Game.Spells } if (unit.IsInCombat() && m_spellInfo.HasInitialAggro()) { - m_caster.SetInCombatState(unit.GetCombatTimer() > 0, unit); + if (m_caster.HasUnitFlag(UnitFlags.PvpAttackable)) // only do explicit combat forwarding for PvP enabled units + m_caster.GetCombatManager().InheritCombatStatesFrom(unit); // for creature v creature combat, the threat forward does it for us unit.GetThreatManager().ForwardThreatForAssistingMe(m_caster, 0.0f, null, true); } } @@ -6849,6 +6850,10 @@ namespace Game.Spells if (unit == null) return; + // This will only cause combat - the target will engage once the projectile hits (in DoAllEffectOnTarget) + if (targetInfo.missCondition != SpellMissInfo.Evade && !m_caster.IsFriendlyTo(unit) && (!m_spellInfo.IsPositive() || m_spellInfo.HasEffect(SpellEffectName.Dispel)) && (m_spellInfo.HasInitialAggro() || unit.IsEngaged())) + m_caster.SetInCombatWith(unit); + foreach (SpellEffectInfo effect in m_spellInfo.GetEffects()) { if (effect != null && Convert.ToBoolean(targetInfo.effectMask & (1 << (int)effect.EffectIndex))) diff --git a/Source/Game/Spells/SpellEffects.cs b/Source/Game/Spells/SpellEffects.cs index 66cff1ff7..b3dc97964 100644 --- a/Source/Game/Spells/SpellEffects.cs +++ b/Source/Game/Spells/SpellEffects.cs @@ -788,7 +788,7 @@ namespace Game.Spells int gain = (int)(newDamage * gainMultiplier); - m_caster.EnergizeBySpell(m_caster, m_spellInfo.Id, gain, powerType); + m_caster.EnergizeBySpell(m_caster, m_spellInfo, gain, powerType); } ExecuteLogEffectTakeTargetPower(effIndex, unitTarget, powerType, (uint)newDamage, gainMultiplier); } @@ -1229,7 +1229,7 @@ namespace Game.Spells break; } - m_caster.EnergizeBySpell(unitTarget, m_spellInfo.Id, damage, power); + m_caster.EnergizeBySpell(unitTarget, m_spellInfo, damage, power); } [SpellEffectHandler(SpellEffectName.EnergizePct)] @@ -1252,7 +1252,7 @@ namespace Game.Spells return; int gain = (int)MathFunctions.CalculatePct(maxPower, damage); - m_caster.EnergizeBySpell(unitTarget, m_spellInfo.Id, gain, power); + m_caster.EnergizeBySpell(unitTarget, m_spellInfo, gain, power); } void SendLoot(ObjectGuid guid, LootType loottype) @@ -2411,28 +2411,22 @@ namespace Game.Spells // this effect use before aura Taunt apply for prevent taunt already attacking target // for spell as marked "non effective at already attacking target" - if (unitTarget == null || !unitTarget.CanHaveThreatList() || unitTarget.GetVictim() == m_caster) + if (unitTarget == null || !unitTarget.CanHaveThreatList()) { SendCastResult(SpellCastResult.DontReport); return; } - if (!unitTarget.GetThreatManager().GetOnlineContainer().Empty()) + ThreatManager mgr = unitTarget.GetThreatManager(); + if (mgr.GetCurrentVictim() == m_caster) { - // Also use this effect to set the taunter's threat to the taunted creature's highest value - float myThreat = unitTarget.GetThreatManager().GetThreat(m_caster); - float topThreat = unitTarget.GetThreatManager().GetOnlineContainer().GetMostHated().GetThreat(); - if (topThreat > myThreat) - unitTarget.GetThreatManager().AddThreat(m_caster, topThreat - myThreat); - - //Set aggro victim to caster - HostileReference forcedVictim = unitTarget.GetThreatManager().GetOnlineContainer().GetReferenceByTarget(m_caster); - if (forcedVictim != null) - unitTarget.GetThreatManager().SetCurrentVictim(forcedVictim); + SendCastResult(SpellCastResult.DontReport); + return; } - if (unitTarget.ToCreature().IsAIEnabled && !unitTarget.ToCreature().HasReactState(ReactStates.Passive)) - unitTarget.ToCreature().GetAI().AttackStart(m_caster); + if (!mgr.IsThreatListEmpty()) + // Set threat equal to highest threat currently on target + mgr.MatchUnitThreatToHighestThreat(m_caster); } [SpellEffectHandler(SpellEffectName.WeaponDamageNoSchool)] @@ -2654,13 +2648,13 @@ namespace Game.Spells if (effectHandleMode != SpellEffectHandleMode.HitTarget) return; - if (unitTarget == null || !unitTarget.IsAlive() || !m_caster.IsAlive()) + if (unitTarget == null || !m_caster.IsAlive()) return; if (!unitTarget.CanHaveThreatList()) return; - unitTarget.GetThreatManager().AddThreat(m_caster, damage); + unitTarget.GetThreatManager().AddThreat(m_caster, damage, m_spellInfo, true); } [SpellEffectHandler(SpellEffectName.HealMaxHealth)] @@ -3241,19 +3235,19 @@ namespace Game.Spells if (unitTarget == null) return; - if (unitTarget.IsTypeId(TypeId.Player)) - unitTarget.ToPlayer().SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel - - unitTarget.GetHostileRefManager().UpdateVisibility(); - - var attackers = unitTarget.GetAttackers(); - for (var i = 0; i < attackers.Count; ++i) + if (unitTarget.IsPlayer() && !unitTarget.GetMap().IsDungeon()) { - var unit = attackers[i]; - if (!unit.CanSeeOrDetect(unitTarget)) - unit.AttackStop(); + // stop all pve combat for players outside dungeons, suppress pvp combat + unitTarget.CombatStop(false, false); + } + else + { + // in dungeons (or for nonplayers), reset this unit on all enemies' threat lists + foreach (var pair in unitTarget.GetThreatManager().GetThreatenedByMeList()) + pair.Value.SetThreat(0.0f); } + // makes spells cast before this time fizzle unitTarget.LastSanctuaryTime = GameTime.GetGameTimeMS(); } @@ -4808,7 +4802,7 @@ namespace Game.Spells return; if (unitTarget != null) - m_caster.SetRedirectThreat(unitTarget.GetGUID(), (uint)damage); + m_caster.GetThreatManager().RegisterRedirectThreat(m_spellInfo.Id, unitTarget.GetGUID(), (uint)damage); } [SpellEffectHandler(SpellEffectName.GameObjectDamage)] diff --git a/Source/Game/Spells/SpellInfo.cs b/Source/Game/Spells/SpellInfo.cs index bd8a8113c..d2bfb028f 100644 --- a/Source/Game/Spells/SpellInfo.cs +++ b/Source/Game/Spells/SpellInfo.cs @@ -1022,8 +1022,8 @@ namespace Game.Spells // creature/player specific target checks if (unitTarget != null) { - // spells cannot be cast if player is in fake combat also - if (HasAttribute(SpellAttr1.CantTargetInCombat) && (unitTarget.IsInCombat() || unitTarget.IsPetInCombat())) + // spells cannot be cast if target has a pet in combat either + if (HasAttribute(SpellAttr1.CantTargetInCombat) && (unitTarget.IsInCombat() || unitTarget.HasUnitFlag(UnitFlags.PetInCombat))) return SpellCastResult.TargetAffectingCombat; // only spells with SPELL_ATTR3_ONLY_TARGET_GHOSTS can target ghosts diff --git a/Source/Scripts/Pets/Mage.cs b/Source/Scripts/Pets/Mage.cs index e990b2fc8..813ec4f74 100644 --- a/Source/Scripts/Pets/Mage.cs +++ b/Source/Scripts/Pets/Mage.cs @@ -76,21 +76,13 @@ namespace Scripts.Pets continue; } // else compare best fit unit with current unit - var triggers = unit.GetThreatManager().GetThreatList(); - foreach (var reference in triggers) + float threat = unit.GetThreatManager().GetThreat(owner); + // Check if best fit hostile unit hs lower threat than this current unit + if (highestThreat < threat) { - // Try to find threat referenced to owner - if (reference.GetTarget() == owner) - { - // Check if best fit hostile unit hs lower threat than this current unit - if (highestThreat < reference.GetThreat()) - { - // If so, update best fit unit - highestThreat = reference.GetThreat(); - highestThreatUnit = unit; - break; - } - } + // If so, update best fit unit + highestThreat = threat; + highestThreatUnit = unit; } // In case no unit with threat was found so far, always check for nearest unit (only for players) if (unit.IsTypeId(TypeId.Player)) @@ -112,30 +104,7 @@ namespace Scripts.Pets bool IsInThreatList(Unit target) { Unit owner = me.GetCharmerOrOwner(); - - List targets = new List(); - var u_check = new AnyUnfriendlyUnitInObjectRangeCheck(me, me, 30.0f); - var searcher = new UnitListSearcher(me, targets, u_check); - Cell.VisitAllObjects(me, searcher, 40.0f); - - foreach (var unit in targets) - { - if (unit == target) - { - // Consider only units without CC - if (!unit.HasBreakableByDamageCrowdControlAura(unit)) - { - var triggers = unit.GetThreatManager().GetThreatList(); - foreach (var reference in triggers) - { - // Try to find threat referenced to owner - if (reference.GetTarget() == owner) - return true; - } - } - } - } - return false; + return owner && target.IsThreatenedBy(owner); } public override void InitializeAI() diff --git a/Source/Scripts/Spells/Generic.cs b/Source/Scripts/Spells/Generic.cs index 19b5b4de9..23384cf58 100644 --- a/Source/Scripts/Spells/Generic.cs +++ b/Source/Scripts/Spells/Generic.cs @@ -2179,7 +2179,7 @@ namespace Scripts.Spells.Generic if (mana != 0) { mana /= 10; - caster.EnergizeBySpell(caster, GetId(), mana, PowerType.Mana); + caster.EnergizeBySpell(caster, GetSpellInfo(), mana, PowerType.Mana); } } diff --git a/Source/Scripts/Spells/Hunter.cs b/Source/Scripts/Spells/Hunter.cs index af79c92ab..33e25fc95 100644 --- a/Source/Scripts/Spells/Hunter.cs +++ b/Source/Scripts/Spells/Hunter.cs @@ -33,6 +33,7 @@ namespace Scripts.Spells.Hunter public const uint ExhilarationR2 = 231546; public const uint Lonewolf = 155228; public const uint MastersCallTriggered = 62305; + public const uint Misdirection = 34477; public const uint MisdirectionProc = 35079; public const uint PetLastStandTriggered = 53479; public const uint PetHeartOfThePhoenixTriggered = 54114; @@ -205,12 +206,7 @@ namespace Scripts.Spells.Hunter return; if (!GetTarget().HasAura(SpellIds.MisdirectionProc)) - GetTarget().ResetRedirectThreat(); - } - - bool CheckProc(ProcEventInfo eventInfo) - { - return GetTarget().GetRedirectThreatTarget(); + GetTarget().GetThreatManager().UnregisterRedirectThreat(SpellIds.Misdirection); } void HandleProc(AuraEffect aurEff, ProcEventInfo eventInfo) @@ -222,7 +218,6 @@ namespace Scripts.Spells.Hunter public override void Register() { AfterEffectRemove.Add(new EffectApplyHandler(OnRemove, 1, AuraType.Dummy, AuraEffectHandleModes.Real)); - DoCheckProc.Add(new CheckProcHandler(CheckProc)); OnEffectProc.Add(new EffectProcHandler(HandleProc, 1, AuraType.Dummy)); } } @@ -233,7 +228,7 @@ namespace Scripts.Spells.Hunter { void OnRemove(AuraEffect aurEff, AuraEffectHandleModes mode) { - GetTarget().ResetRedirectThreat(); + GetTarget().GetThreatManager().UnregisterRedirectThreat(SpellIds.Misdirection); } public override void Register() diff --git a/Source/Scripts/World/NpcSpecial.cs b/Source/Scripts/World/NpcSpecial.cs index 04b309f6a..1fac21a34 100644 --- a/Source/Scripts/World/NpcSpecial.cs +++ b/Source/Scripts/World/NpcSpecial.cs @@ -1550,18 +1550,24 @@ namespace Scripts.World.NpcSpecial _scheduler.Schedule(TimeSpan.FromSeconds(1), task => { long now = GameTime.GetGameTime(); - foreach (var pair in _damageTimes.ToList()) + var pveRefs = me.GetCombatManager().GetPvECombatRefs(); + foreach (var pair in _damageTimes) { // If unit has not dealt damage to training dummy for 5 seconds, Remove him from combat if (pair.Value < now - 5) { - Unit unit = Global.ObjAccessor.GetUnit(me, pair.Key); - if (unit) - unit.GetHostileRefManager().DeleteReference(me); + var refe = pveRefs.LookupByKey(pair.Key); + if (refe != null) + refe.EndCombat(); _damageTimes.Remove(pair.Key); } } + + foreach (var pair in pveRefs) + if (!_damageTimes.ContainsKey(pair.Key)) + _damageTimes[pair.Key] = now; + task.Repeat(); }); }