From 984baa92ddaf852a737cbe8f06528d6dd4c5a7a7 Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:36:59 -0500 Subject: [PATCH] refactor(Core/Combat): Port TrinityCore heap-based threat system (#24715) Co-authored-by: blinkysc Co-authored-by: Treeston Co-authored-by: killerwife Co-authored-by: Claude Opus 4.6 --- src/server/game/AI/CoreAI/PassiveAI.cpp | 2 +- src/server/game/AI/CoreAI/PassiveAI.h | 5 + src/server/game/AI/CoreAI/PetAI.cpp | 4 +- src/server/game/AI/CoreAI/UnitAI.cpp | 77 +- src/server/game/AI/CoreAI/UnitAI.h | 19 +- src/server/game/AI/CreatureAI.cpp | 120 +- src/server/game/AI/CreatureAI.h | 28 +- .../game/AI/ScriptedAI/ScriptedCreature.cpp | 13 +- src/server/game/AI/SmartScripts/SmartAI.cpp | 32 +- .../game/AI/SmartScripts/SmartScript.cpp | 44 +- .../game/Battlegrounds/Battleground.cpp | 2 +- src/server/game/Combat/CombatManager.cpp | 460 ++++ src/server/game/Combat/CombatManager.h | 167 ++ src/server/game/Combat/HostileRefMgr.cpp | 250 -- src/server/game/Combat/HostileRefMgr.h | 74 - src/server/game/Combat/ThreatManager.cpp | 947 +++++++ src/server/game/Combat/ThreatManager.h | 329 +++ src/server/game/Combat/ThreatMgr.cpp | 733 ------ src/server/game/Combat/ThreatMgr.h | 335 --- src/server/game/Combat/UnitEvents.h | 134 - .../game/Entities/Creature/Creature.cpp | 163 +- src/server/game/Entities/Creature/Creature.h | 8 +- .../game/Entities/Creature/CreatureGroups.cpp | 4 +- src/server/game/Entities/Pet/Pet.cpp | 1 + src/server/game/Entities/Player/Player.cpp | 10 +- .../game/Entities/Player/PlayerUpdates.cpp | 7 +- src/server/game/Entities/Unit/Unit.cpp | 622 ++--- src/server/game/Entities/Unit/Unit.h | 67 +- src/server/game/Entities/Vehicle/Vehicle.cpp | 4 + .../game/Grids/Notifiers/GridNotifiers.h | 9 +- src/server/game/Handlers/MovementHandler.cpp | 4 +- src/server/game/Maps/Map.cpp | 2 +- .../HomeMovementGenerator.cpp | 3 + .../TargetedMovementGenerator.cpp | 11 +- .../WaypointMovementGenerator.cpp | 2 - src/server/game/Server/WorldSession.cpp | 2 +- .../game/Spells/Auras/SpellAuraDefines.h | 2 +- .../game/Spells/Auras/SpellAuraEffects.cpp | 81 +- .../game/Spells/Auras/SpellAuraEffects.h | 1 + src/server/game/Spells/Auras/SpellAuras.h | 6 + src/server/game/Spells/Spell.cpp | 60 +- src/server/game/Spells/SpellEffects.cpp | 57 +- src/server/game/Spells/SpellInfo.cpp | 5 + src/server/game/Spells/SpellInfo.h | 1 + src/server/scripts/Commands/cs_debug.cpp | 213 +- src/server/scripts/Commands/cs_misc.cpp | 2 +- src/server/scripts/Commands/cs_spellinfo.cpp | 2 +- .../BlackwingLair/boss_nefarian.cpp | 7 +- .../BlackwingLair/boss_razorgore.cpp | 4 +- .../MoltenCore/boss_ragnaros.cpp | 2 +- .../Karazhan/boss_midnight.cpp | 9 +- .../boss_priestess_delrissa.cpp | 10 +- .../Scholomance/instance_scholomance.cpp | 10 +- .../ZulGurub/boss_hazzarah.cpp | 14 +- .../ZulGurub/boss_mandokir.cpp | 6 +- .../ZulGurub/boss_renataki.cpp | 19 +- .../Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp | 2 +- .../AzjolNerub/AzjolNerub/boss_hadronox.cpp | 7 +- .../RubySanctum/boss_halion.cpp | 4 +- .../boss_argent_challenge.cpp | 4 +- .../boss_grand_champions.cpp | 1 - .../boss_faction_champions.cpp | 31 +- .../boss_icecrown_gunship_battle.cpp | 4 +- .../boss_lady_deathwhisper.cpp | 12 +- .../boss_professor_putricide.cpp | 2 +- .../IcecrownCitadel/boss_the_lich_king.cpp | 6 +- .../boss_valithria_dreamwalker.cpp | 12 +- .../IcecrownCitadel/icecrown_citadel.cpp | 23 +- .../Northrend/Naxxramas/boss_gothik.cpp | 14 +- .../Northrend/Naxxramas/boss_kelthuzad.cpp | 18 +- .../Northrend/Naxxramas/boss_patchwerk.cpp | 6 +- .../Northrend/Naxxramas/boss_sapphiron.cpp | 10 +- .../Northrend/Naxxramas/boss_thaddius.cpp | 4 +- .../scripts/Northrend/zone_dragonblight.cpp | 2 +- .../boss_blackheart_the_inciter.cpp | 13 +- .../ShadowLabyrinth/boss_murmur.cpp | 2 +- .../Outland/BlackTemple/boss_illidan.cpp | 2 +- .../Outland/BlackTemple/boss_supremus.cpp | 13 +- .../BlackTemple/instance_black_temple.cpp | 4 +- .../SerpentShrine/boss_lurker_below.cpp | 5 +- .../Outland/TempestKeep/Eye/boss_alar.cpp | 2 +- .../Outland/TempestKeep/Eye/boss_kaelthas.cpp | 11 +- .../botanica/instance_the_botanica.cpp | 4 +- .../Outland/zone_blades_edge_mountains.cpp | 1 - src/server/scripts/Pet/pet_hunter.cpp | 2 +- src/server/scripts/Pet/pet_mage.cpp | 8 +- src/server/scripts/Spells/spell_dk.cpp | 15 + src/server/scripts/Spells/spell_hunter.cpp | 7 +- src/server/scripts/Spells/spell_priest.cpp | 5 +- src/server/scripts/Spells/spell_rogue.cpp | 74 +- src/server/scripts/Spells/spell_warrior.cpp | 2 +- .../scripts/World/boss_emerald_dragons.cpp | 8 +- .../scripts/World/npc_stave_of_ancients.cpp | 21 +- .../scripts/World/npc_stave_of_ancients.h | 1 - src/server/scripts/World/npcs_special.cpp | 162 +- src/test/mocks/TestCreature.cpp | 141 + src/test/mocks/TestCreature.h | 87 + src/test/mocks/TestMap.cpp | 62 + src/test/mocks/TestMap.h | 51 + .../server/game/Combat/CombatManagerTest.cpp | 1320 ++++++++++ .../server/game/Combat/ThreatManagerTest.cpp | 2314 +++++++++++++++++ 101 files changed, 7045 insertions(+), 2659 deletions(-) create mode 100644 src/server/game/Combat/CombatManager.cpp create mode 100644 src/server/game/Combat/CombatManager.h delete mode 100644 src/server/game/Combat/HostileRefMgr.cpp delete mode 100644 src/server/game/Combat/HostileRefMgr.h create mode 100644 src/server/game/Combat/ThreatManager.cpp create mode 100644 src/server/game/Combat/ThreatManager.h delete mode 100644 src/server/game/Combat/ThreatMgr.cpp delete mode 100644 src/server/game/Combat/ThreatMgr.h delete mode 100644 src/server/game/Combat/UnitEvents.h create mode 100644 src/test/mocks/TestCreature.cpp create mode 100644 src/test/mocks/TestCreature.h create mode 100644 src/test/mocks/TestMap.cpp create mode 100644 src/test/mocks/TestMap.h create mode 100644 src/test/server/game/Combat/CombatManagerTest.cpp create mode 100644 src/test/server/game/Combat/ThreatManagerTest.cpp diff --git a/src/server/game/AI/CoreAI/PassiveAI.cpp b/src/server/game/AI/CoreAI/PassiveAI.cpp index 5d9ecca75..b0d605926 100644 --- a/src/server/game/AI/CoreAI/PassiveAI.cpp +++ b/src/server/game/AI/CoreAI/PassiveAI.cpp @@ -35,7 +35,7 @@ int32 NullCreatureAI::Permissible(Creature const* creature) void PassiveAI::UpdateAI(uint32) { - if (me->IsInCombat() && me->getAttackers().empty()) + if (me->IsEngaged() && !me->IsInCombat()) EnterEvadeMode(EVADE_REASON_NO_HOSTILES); } diff --git a/src/server/game/AI/CoreAI/PassiveAI.h b/src/server/game/AI/CoreAI/PassiveAI.h index 9f44ef3f8..f0a2dbbe5 100644 --- a/src/server/game/AI/CoreAI/PassiveAI.h +++ b/src/server/game/AI/CoreAI/PassiveAI.h @@ -40,6 +40,9 @@ public: void MoveInLineOfSight(Unit*) override {} void AttackStart(Unit* target) override; + void JustEnteredCombat(Unit* who) override { EngagementStart(who); } + void JustExitedCombat() override { EngagementOver(); } + void JustStartedThreateningMe(Unit*) override {} void UpdateAI(uint32) override; void EnterEvadeMode(EvadeReason /*why*/) override {} @@ -56,6 +59,8 @@ public: void MoveInLineOfSight(Unit*) override {} void AttackStart(Unit*) override {} + void JustStartedThreateningMe(Unit*) override {} + void JustEnteredCombat(Unit*) override {} void UpdateAI(uint32) override {} void EnterEvadeMode(EvadeReason /*why*/) override {} void OnCharmed(bool /*apply*/) override {} diff --git a/src/server/game/AI/CoreAI/PetAI.cpp b/src/server/game/AI/CoreAI/PetAI.cpp index 57f753421..14bd1d530 100644 --- a/src/server/game/AI/CoreAI/PetAI.cpp +++ b/src/server/game/AI/CoreAI/PetAI.cpp @@ -77,7 +77,7 @@ void PetAI::_stopAttack() me->GetMotionMaster()->Clear(); me->GetMotionMaster()->MoveIdle(); me->CombatStop(); - me->getHostileRefMgr().deleteReferences(); + me->GetThreatMgr().RemoveMeFromThreatLists(); return; } @@ -498,7 +498,7 @@ Unit* PetAI::SelectNextTarget(bool allowAutoSelect) const if (!tauntAuras.empty()) for (Unit::AuraEffectList::const_reverse_iterator itr = tauntAuras.rbegin(); itr != tauntAuras.rend(); ++itr) if (Unit* caster = (*itr)->GetCaster()) - if (me->CanCreatureAttack(caster) && !caster->HasAuraTypeWithCaster(SPELL_AURA_IGNORED, me->GetGUID())) + if (me->CanCreatureAttack(caster) && !caster->HasAuraTypeWithCaster(SPELL_AURA_MOD_DETAUNT, me->GetGUID())) return caster; } diff --git a/src/server/game/AI/CoreAI/UnitAI.cpp b/src/server/game/AI/CoreAI/UnitAI.cpp index 81bd5ab52..ee6ddcd03 100644 --- a/src/server/game/AI/CoreAI/UnitAI.cpp +++ b/src/server/game/AI/CoreAI/UnitAI.cpp @@ -17,6 +17,7 @@ #include "UnitAI.h" #include "Creature.h" +#include "CreatureAI.h" #include "CreatureAIImpl.h" #include "Player.h" #include "Spell.h" @@ -135,10 +136,12 @@ SpellCastResult UnitAI::DoAddAuraToAllHostilePlayers(uint32 spellid) { if (me->IsInCombat()) { - ThreatContainer::StorageType threatlist = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatlist.begin(); itr != threatlist.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* unit = ObjectAccessor::GetUnit(*me, (*itr)->getUnitGuid())) + if (ref->IsOffline()) + continue; + + if (Unit* unit = ref->GetVictim()) { if (unit->IsPlayer()) { @@ -146,8 +149,6 @@ SpellCastResult UnitAI::DoAddAuraToAllHostilePlayers(uint32 spellid) return SPELL_CAST_OK; } } - else - return SPELL_FAILED_BAD_TARGETS; } } @@ -158,16 +159,16 @@ SpellCastResult UnitAI::DoCastToAllHostilePlayers(uint32 spellid, bool triggered { if (me->IsInCombat()) { - ThreatContainer::StorageType threatlist = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatlist.begin(); itr != threatlist.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* unit = ObjectAccessor::GetUnit(*me, (*itr)->getUnitGuid())) + if (ref->IsOffline()) + continue; + + if (Unit* unit = ref->GetVictim()) { if (unit->IsPlayer()) return me->CastSpell(unit, spellid, triggered); } - else - return SPELL_FAILED_BAD_TARGETS; } } @@ -376,7 +377,7 @@ void UnitAI::FillAISpellInfo() } } -ThreatMgr& UnitAI::GetThreatMgr() +ThreatManager& UnitAI::GetThreatMgr() { return me->GetThreatMgr(); } @@ -386,6 +387,60 @@ void UnitAI::SortByDistance(std::list& list, bool ascending) list.sort(Acore::ObjectDistanceOrderPred(me, ascending)); } +void UnitAI::EvadeTimerExpired() +{ + Creature* creature = me->ToCreature(); + if (!creature) + return; + + CreatureAI* ai = creature->AI(); + if (!ai) + return; + + // Check if we can teleport an unreachable player first + if (ObjectGuid targetGuid = creature->GetCannotReachTarget()) + { + if (Player* player = ObjectAccessor::GetPlayer(*creature, targetGuid)) + { + if (creature->IsEngagedBy(player) && ai->OnTeleportUnreacheablePlayer(player)) + { + creature->SetCannotReachTarget(); + return; + } + } + } + + if (creature->GetMap()->IsRaid()) + { + creature->GetCombatManager().ContinueEvadeRegen(); + return; + } + + // If only one target, enter evade mode + if (creature->GetThreatMgr().GetThreatListSize() <= 1) + { + ai->EnterEvadeMode(CreatureAI::EVADE_REASON_NO_PATH); + return; + } + + // Multiple targets - clear threat on unreachable target and try another + if (ObjectGuid targetGuid = creature->GetCannotReachTarget()) + { + if (Unit* target = ObjectAccessor::GetUnit(*creature, targetGuid)) + { + if (creature->GetThreatMgr().IsThreatenedBy(target)) + { + creature->GetThreatMgr().ClearThreat(target); + creature->SetCannotReachTarget(); + return; + } + } + } + + // Fallback - enter evade + ai->EnterEvadeMode(CreatureAI::EVADE_REASON_NO_PATH); +} + //Enable PlayerAI when charmed void PlayerAI::OnCharmed(bool apply) { diff --git a/src/server/game/AI/CoreAI/UnitAI.h b/src/server/game/AI/CoreAI/UnitAI.h index 178aaf93b..af3f4c907 100644 --- a/src/server/game/AI/CoreAI/UnitAI.h +++ b/src/server/game/AI/CoreAI/UnitAI.h @@ -63,7 +63,7 @@ struct DefaultTargetSelector : public Acore::unary_function // playerOnly: self explaining // withMainTank: allow current tank to be selected // aura: if 0: ignored, if > 0: the target shall have the aura, if < 0, the target shall NOT have the aura - DefaultTargetSelector(Unit const* unit, float dist, bool playerOnly, bool withMainTank, int32 aura) : me(unit), m_dist(dist), except(!withMainTank ? me->GetThreatMgr().GetCurrentVictim() : nullptr), m_playerOnly(playerOnly), m_aura(aura) {} + DefaultTargetSelector(Unit const* unit, float dist, bool playerOnly, bool withMainTank, int32 aura) : me(unit), m_dist(dist), except(!withMainTank ? me->GetThreatMgr().GetLastVictim() : nullptr), m_playerOnly(playerOnly), m_aura(aura) {} bool operator()(Unit const* target) const { @@ -126,7 +126,7 @@ public: bool operator()(Unit const* target) const; private: - Creature const* _source; + Creature* _source; bool _playerOnly; }; @@ -146,7 +146,7 @@ struct PowerUsersSelector : public Acore::unary_function if (!_me || !target) return false; - if (!_withTank && target == _me->GetThreatMgr().GetCurrentVictim()) + if (!_withTank && target == _me->GetThreatMgr().GetLastVictim()) return false; if (target->getPowerType() != _power) @@ -237,7 +237,7 @@ public: template Unit* SelectTarget(SelectTargetMethod targetType, uint32 position, PREDICATE const& predicate) { - ThreatMgr& mgr = GetThreatMgr(); + ThreatManager& mgr = GetThreatMgr(); // shortcut: if we ignore the first elements, and there are at most elements, then we ignore ALL elements if (mgr.GetThreatListSize() <= position) return nullptr; @@ -282,7 +282,7 @@ public: void SelectTargetList(std::list& targetList, uint32 num, SelectTargetMethod targetType, uint32 position, PREDICATE const& predicate) { targetList.clear(); - ThreatMgr& mgr = GetThreatMgr(); + ThreatManager& mgr = GetThreatMgr(); // shortcut: we're gonna ignore the first elements, and there's at most elements, so we ignore them all - nothing to do here if (mgr.GetThreatListSize() <= position) return; @@ -352,11 +352,14 @@ public: /** * @brief Called when the unit leaves combat - * - * @todo Never invoked right now. Preparation for Combat Threat refactor */ virtual void JustExitedCombat() { } + /** + * @brief Called when evade timer expires (target unreachable for too long) + */ + virtual void EvadeTimerExpired(); + /// @brief Called at any Damage to any victim (before damage apply) virtual void DamageDealt(Unit* /*victim*/, uint32& /*damage*/, DamageEffectType /*damageType*/, SpellSchoolMask /*damageSchoolMask*/) {} @@ -421,7 +424,7 @@ public: virtual std::string GetDebugInfo() const; private: - ThreatMgr& GetThreatMgr(); + ThreatManager& GetThreatMgr(); void SortByDistance(std::list& list, bool ascending = true); }; diff --git a/src/server/game/AI/CreatureAI.cpp b/src/server/game/AI/CreatureAI.cpp index f21e93230..e9c0ee353 100644 --- a/src/server/game/AI/CreatureAI.cpp +++ b/src/server/game/AI/CreatureAI.cpp @@ -33,9 +33,23 @@ #include //Disable CreatureAI when charmed -void CreatureAI::OnCharmed(bool /*apply*/) +void CreatureAI::OnCharmed(bool /* apply */) { - //me->IsAIEnabled = !apply;*/ + if (!me->IsCharmed() && !me->LastCharmerGUID.IsEmpty()) + { + if (!me->HasReactState(REACT_PASSIVE)) + { + if (Unit* lastCharmer = ObjectAccessor::GetUnit(*me, me->LastCharmerGUID)) + me->EngageWithTarget(lastCharmer); + } + + me->LastCharmerGUID.Clear(); + + if (!me->IsInCombat()) + EnterEvadeMode(EVADE_REASON_NO_HOSTILES); + } + + // trigger AI switch me->NeedChangeAI = true; me->IsAIEnabled = false; } @@ -139,13 +153,13 @@ void CreatureAI::DoZoneInCombat(Creature* creature /*= nullptr*/, float maxRange continue; } - creature->SetInCombatWith(player); - player->SetInCombatWith(creature); + creature->EngageWithTarget(player); - if (creature->CanHaveThreatList()) - { - creature->AddThreat(player, 0.0f); - } + for (Unit* pet : player->m_Controlled) + creature->EngageWithTarget(pet); + + if (Unit* vehicle = player->GetVehicleBase()) + creature->EngageWithTarget(vehicle); } } } @@ -179,6 +193,15 @@ void CreatureAI::MoveInLineOfSight(Unit* who) AttackStart(who); } +void CreatureAI::OnOwnerCombatInteraction(Unit* target) +{ + if (!target || !me->IsAlive()) + return; + + if (!me->HasReactState(REACT_PASSIVE) && me->CanStartAttack(target)) + me->EngageWithTarget(target); +} + // Distract creature, if player gets too close while stealthed/prowling void CreatureAI::TriggerAlert(Unit const* who) const { @@ -241,6 +264,47 @@ void CreatureAI::EnterEvadeMode(EvadeReason why) } } +void CreatureAI::JustEnteredCombat(Unit* who) +{ + // Creatures without threat list use JustEnteredCombat to trigger engagement + if (!IsEngaged() && !me->CanHaveThreatList()) + EngagementStart(who); +} + +void CreatureAI::EngagementStart(Unit* who) +{ + if (_isEngaged) + { + LOG_ERROR("scripts.ai", "CreatureAI::EngagementStart called even though creature is already engaged. Creature debug info:\n{}", me->GetDebugInfo()); + return; + } + _isEngaged = true; + + me->AtEngage(who); +} + +void CreatureAI::EngagementOver() +{ + if (!_isEngaged) + { + LOG_DEBUG("scripts.ai", "CreatureAI::EngagementOver called even though creature is not currently engaged. Creature debug info:\n{}", me->GetDebugInfo()); + return; + } + _isEngaged = false; + + me->AtDisengage(); +} + +void CreatureAI::JustExitedCombat() +{ + EngagementOver(); + + // If creature is alive, in world, and not already evading, trigger evade to return home + // Check IsInWorld to avoid evade during server shutdown/cleanup + if (me->IsAlive() && me->IsInWorld() && !me->IsInEvadeMode()) + EnterEvadeMode(EVADE_REASON_NO_HOSTILES); +} + /*void CreatureAI::AttackedBy(Unit* attacker) { if (!me->GetVictim()) @@ -279,38 +343,50 @@ bool CreatureAI::UpdateVictim() if (!me->IsEngaged()) return false; + if (!me->IsAlive()) + { + EngagementOver(); + return false; + } + if (!me->HasReactState(REACT_PASSIVE)) { if (Unit* victim = me->SelectVictim()) - AttackStart(victim); - return me->GetVictim(); + if (victim != me->GetVictim()) + AttackStart(victim); + return me->GetVictim() != nullptr; } - // xinef: if we have any victim, just return true - else if (me->GetVictim() && me->GetExactDist(me->GetVictim()) < 30.0f) - return true; - else if (me->GetThreatMgr().isThreatListEmpty()) + else if (!me->IsInCombat()) { - EnterEvadeMode(); + EnterEvadeMode(EVADE_REASON_NO_HOSTILES); return false; } + else if (me->GetVictim()) + me->AttackStop(); return true; } bool CreatureAI::_EnterEvadeMode(EvadeReason /*why*/) { + if (me->IsInEvadeMode()) + return false; + if (!me->IsAlive()) { + EngagementOver(); return false; } + // Set evade state early to prevent recursion when CombatStop triggers JustExitedCombat + // This will be cleared in MoveTargetedHome for pets/guardians following their owner + me->AddUnitState(UNIT_STATE_EVADE); + // don't remove vehicle auras, passengers aren't supposed to drop off the vehicle // don't remove clone caster on evade (to be verified) me->RemoveEvadeAuras(); me->ClearComboPointHolders(); // Remove all combo points targeting this unit - // sometimes bosses stuck in combat? - me->GetThreatMgr().ClearAllThreat(); me->CombatStop(true); me->LoadCreaturesAddon(true); me->SetLootRecipient(nullptr); @@ -321,14 +397,8 @@ bool CreatureAI::_EnterEvadeMode(EvadeReason /*why*/) if (ZoneScript* zoneScript = me->GetZoneScript() ? me->GetZoneScript() : (ZoneScript*)me->GetInstanceScript()) zoneScript->OnCreatureEvade(me); - if (me->IsInEvadeMode()) - { - return false; - } - else if (CreatureGroup* formation = me->GetFormation()) - { + if (CreatureGroup* formation = me->GetFormation()) formation->MemberEvaded(me); - } if (TempSummon* summon = me->ToTempSummon()) { @@ -345,6 +415,8 @@ bool CreatureAI::_EnterEvadeMode(EvadeReason /*why*/) } } + EngagementOver(); + return true; } diff --git a/src/server/game/AI/CreatureAI.h b/src/server/game/AI/CreatureAI.h index bf5ee28d5..6509e4c0e 100644 --- a/src/server/game/AI/CreatureAI.h +++ b/src/server/game/AI/CreatureAI.h @@ -97,10 +97,13 @@ public: WorldObject* GetSummoner() const; - explicit CreatureAI(Creature* creature) : UnitAI(creature), me(creature), _boundary(nullptr), _negateBoundary(false), m_MoveInLineOfSight_locked(false) { } + explicit CreatureAI(Creature* creature) : UnitAI(creature), me(creature), _boundary(nullptr), _negateBoundary(false), _isEngaged(false), m_MoveInLineOfSight_locked(false) { } ~CreatureAI() override {} + /// @brief Check if creature is currently engaged in combat + bool IsEngaged() const { return _isEngaged; } + void MoveCircleChecks(); void MoveBackwardsChecks(); @@ -118,11 +121,24 @@ public: // Called for reaction at stopping attack at no attackers or targets virtual void EnterEvadeMode(EvadeReason why = EVADE_REASON_OTHER); + // Called for reaction whenever we start being in combat (overridden from UnitAI) + void JustEnteredCombat(Unit* who) override; + + // Called for reaction when a new non-offline unit is added to the threat list + virtual void JustStartedThreateningMe(Unit* who) { if (!IsEngaged()) EngagementStart(who); } + /** - * @brief Called for reaction when initially engaged + * @brief Called for reaction when initially engaged - this happens _after_ JustEnteredCombat */ virtual void JustEngagedWith(Unit* /*who*/) {} + // Engagement handling + void EngagementStart(Unit* who); + void EngagementOver(); + + // Called when combat ends - clears engagement state and triggers evade if needed + void JustExitedCombat() override; + // Called when the creature is killed virtual void JustDied(Unit* /*killer*/) {} @@ -181,10 +197,13 @@ public: virtual void ReceiveEmote(Player* /*player*/, uint32 /*emoteId*/) {} // Called when owner takes damage - virtual void OwnerAttackedBy(Unit* /*attacker*/) {} + virtual void OwnerAttackedBy(Unit* attacker) { OnOwnerCombatInteraction(attacker); } // Called when owner attacks something - virtual void OwnerAttacked(Unit* /*target*/) {} + virtual void OwnerAttacked(Unit* target) { OnOwnerCombatInteraction(target); } + + // Default handler for owner combat interactions — makes controlled creatures auto-engage + void OnOwnerCombatInteraction(Unit* target); /// == Triggered Actions Requested ================== @@ -248,6 +267,7 @@ protected: CreatureBoundary const* _boundary; bool _negateBoundary; + bool _isEngaged; private: bool m_MoveInLineOfSight_locked; diff --git a/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp b/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp index ddce144a1..b19fdddce 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp +++ b/src/server/game/AI/ScriptedAI/ScriptedCreature.cpp @@ -472,7 +472,7 @@ void ScriptedAI::DoResetThreat(Unit* unit) void ScriptedAI::DoResetThreatList() { - if (!me->CanHaveThreatList() || me->GetThreatMgr().isThreatListEmpty()) + if (!me->CanHaveThreatList() || me->GetThreatMgr().IsThreatListEmpty()) { LOG_ERROR("entities.unit.ai", "DoResetThreatList called for creature that either cannot have threat list or has empty threat list (me entry = {})", me->GetEntry()); return; @@ -714,11 +714,12 @@ void BossAI::TeleportCheaters() float x, y, z; me->GetPosition(x, y, z); - ThreatContainer::StorageType threatList = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatList.begin(); itr != threatList.end(); ++itr) - if (Unit* target = (*itr)->getTarget()) - if (target->IsPlayer() && !IsInBoundary(target)) - target->NearTeleportTo(x, y, z, 0); + for (auto const& pair : me->GetCombatManager().GetPvECombatRefs()) + { + Unit* target = pair.second->GetOther(me); + if (target->IsControlledByPlayer() && !IsInBoundary(target)) + target->NearTeleportTo(x, y, z, 0); + } } void BossAI::JustSummoned(Creature* summon) diff --git a/src/server/game/AI/SmartScripts/SmartAI.cpp b/src/server/game/AI/SmartScripts/SmartAI.cpp index 2b0359c1c..284a6e661 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.cpp +++ b/src/server/game/AI/SmartScripts/SmartAI.cpp @@ -523,8 +523,16 @@ void SmartAI::CheckConditions(const uint32 diff) void SmartAI::UpdateAI(uint32 diff) { - bool hasVictim = UpdateVictim(); + if (!me->IsAlive()) + { + if (IsEngaged()) + EngagementOver(); + return; + } + CheckConditions(diff); + + bool hasVictim = UpdateVictim(); GetScript()->OnUpdate(diff); UpdatePath(diff); UpdateDespawn(diff); @@ -689,7 +697,12 @@ void SmartAI::EnterEvadeMode(EvadeReason /*why*/) GetScript()->ProcessEventsFor(SMART_EVENT_EVADE); //must be after aura clear so we can cast spells from db - if (HasEscortState(SMART_ESCORT_ESCORTING)) + if (Unit* owner = me->GetCharmerOrOwner()) + { + me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, me->GetFollowAngle()); + me->ClearUnitState(UNIT_STATE_EVADE); + } + else if (HasEscortState(SMART_ESCORT_ESCORTING)) { AddEscortState(SMART_ESCORT_RETURNING); ReturnToLastOOCPos(); @@ -1003,13 +1016,24 @@ void SmartAI::OnCharmed(bool /* apply */) mIsCharmed = charmed; + if (charmed && !me->isPossessed() && !me->IsVehicle()) + me->GetMotionMaster()->MoveFollow(me->GetCharmer(), PET_FOLLOW_DIST, me->GetFollowAngle()); + if (!charmed && !me->IsInEvadeMode()) { if (mCanRepeatPath) StartPath(mForcedMovement, GetScript()->GetPathId(), true); - if (Unit* charmer = me->GetCharmer()) - AttackStart(charmer); + if (!me->LastCharmerGUID.IsEmpty()) + { + if (!me->HasReactState(REACT_PASSIVE)) + if (Unit* lastCharmer = ObjectAccessor::GetUnit(*me, me->LastCharmerGUID)) + me->EngageWithTarget(lastCharmer); + me->LastCharmerGUID.Clear(); + + if (!me->IsInCombat()) + EnterEvadeMode(EVADE_REASON_NO_HOSTILES); + } } GetScript()->ProcessEventsFor(SMART_EVENT_CHARMED, nullptr, 0, 0, charmed); diff --git a/src/server/game/AI/SmartScripts/SmartScript.cpp b/src/server/game/AI/SmartScripts/SmartScript.cpp index ec4bf27d6..d126ff9d8 100644 --- a/src/server/game/AI/SmartScripts/SmartScript.cpp +++ b/src/server/game/AI/SmartScripts/SmartScript.cpp @@ -585,15 +585,11 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u if (!me) break; - ThreatContainer::StorageType threatList = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator i = threatList.begin(); i != threatList.end(); ++i) + for (ThreatReference* ref : me->GetThreatMgr().GetModifiableThreatList()) { - if (Unit* target = ObjectAccessor::GetUnit(*me, (*i)->getUnitGuid())) - { - me->GetThreatMgr().ModifyThreatByPercent(target, e.action.threatPCT.threatINC ? (int32)e.action.threatPCT.threatINC : -(int32)e.action.threatPCT.threatDEC); - LOG_DEBUG("sql.sql", "SmartScript::ProcessAction:: SMART_ACTION_THREAT_ALL_PCT: Creature {} modify threat for unit {}, value {}", - me->GetGUID().ToString(), target->GetGUID().ToString(), e.action.threatPCT.threatINC ? (int32)e.action.threatPCT.threatINC : -(int32)e.action.threatPCT.threatDEC); - } + ref->ModifyThreatByPercent(std::max(-100, int32(e.action.threatPCT.threatINC) - int32(e.action.threatPCT.threatDEC))); + LOG_DEBUG("sql.sql", "SmartScript::ProcessAction:: SMART_ACTION_THREAT_ALL_PCT: Creature {} modify threat for unit {}, value {}", + me->GetGUID().ToString(), ref->GetVictim()->GetGUID().ToString(), int32(e.action.threatPCT.threatINC) - int32(e.action.threatPCT.threatDEC)); } break; } @@ -606,9 +602,9 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u { if (IsUnit(target)) { - me->GetThreatMgr().ModifyThreatByPercent(target->ToUnit(), e.action.threatPCT.threatINC ? (int32)e.action.threatPCT.threatINC : -(int32)e.action.threatPCT.threatDEC); - LOG_DEBUG("scripts.ai", "SmartScript::ProcessAction:: SMART_ACTION_THREAT_SINGLE_PCT: Creature guidLow {} modify threat for unit {}, value {}", - me->GetGUID().ToString(), target->GetGUID().ToString(), e.action.threatPCT.threatINC ? (int32)e.action.threatPCT.threatINC : -(int32)e.action.threatPCT.threatDEC); + me->GetThreatMgr().ModifyThreatByPercent(target->ToUnit(), std::max(-100, int32(e.action.threatPCT.threatINC) - int32(e.action.threatPCT.threatDEC))); + LOG_DEBUG("scripts.ai", "SmartScript::ProcessAction:: SMART_ACTION_THREAT_SINGLE_PCT: Creature {} modify threat for unit {}, value {}", + me->GetGUID().ToString(), target->GetGUID().ToString(), int32(e.action.threatPCT.threatINC) - int32(e.action.threatPCT.threatDEC)); } } break; @@ -1294,9 +1290,7 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u for (WorldObject* unit : units) if (IsPlayer(unit) && !unit->ToPlayer()->isDead()) { - me->SetInCombatWith(unit->ToPlayer()); - unit->ToPlayer()->SetInCombatWith(me); - me->AddThreat(unit->ToPlayer(), 0.0f); + me->EngageWithTarget(unit->ToPlayer()); } } else @@ -2611,9 +2605,11 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u } case SMART_ACTION_ADD_THREAT: { + if (!me->CanHaveThreatList()) + break; for (WorldObject* const target : targets) if (IsUnit(target)) - me->AddThreat(target->ToUnit(), float(e.action.threatPCT.threatINC) - float(e.action.threatPCT.threatDEC)); + me->GetThreatMgr().AddThreat(target->ToUnit(), float(e.action.threat.threatINC) - float(e.action.threat.threatDEC), nullptr, true, true); break; } case SMART_ACTION_LOAD_EQUIPMENT: @@ -3900,9 +3896,8 @@ void SmartScript::GetTargets(ObjectVector& targets, SmartScriptHolder const& e, { if (me) { - ThreatContainer::StorageType threatList = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator i = threatList.begin(); i != threatList.end(); ++i) - if (Unit* temp = ObjectAccessor::GetUnit(*me, (*i)->getUnitGuid())) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) + if (Unit* temp = ref->GetVictim()) // Xinef: added distance check if (e.target.threatList.maxDist == 0 || me->IsWithinCombatRange(temp, (float)e.target.threatList.maxDist)) targets.push_back(temp); @@ -4848,12 +4843,11 @@ void SmartScript::ProcessEvent(SmartScriptHolder& e, Unit* unit, uint32 var0, ui if (!me || !me->IsEngaged()) return; - ThreatContainer::StorageType threatList = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator i = threatList.begin(); i != threatList.end(); ++i) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* target = ObjectAccessor::GetUnit(*me, (*i)->getUnitGuid())) + if (Unit* target = ref->GetVictim()) { - if (!target || !IsPlayer(target) || !target->IsNonMeleeSpellCast(false, false, true)) + if (!IsPlayer(target) || !target->IsNonMeleeSpellCast(false, false, true)) continue; if (!(me->IsInRange(target, (float)e.event.minMaxRepeat.rangeMin, (float)e.event.minMaxRepeat.rangeMax))) @@ -4867,6 +4861,7 @@ void SmartScript::ProcessEvent(SmartScriptHolder& e, Unit* unit, uint32 var0, ui // No targets found RecalcTimer(e, e.event.minMaxRepeat.repeatMin, e.event.minMaxRepeat.repeatMax); + break; } case SMART_EVENT_AREA_RANGE: @@ -4874,10 +4869,9 @@ void SmartScript::ProcessEvent(SmartScriptHolder& e, Unit* unit, uint32 var0, ui if (!me || !me->IsEngaged()) return; - ThreatContainer::StorageType threatList = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator i = threatList.begin(); i != threatList.end(); ++i) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* target = ObjectAccessor::GetUnit(*me, (*i)->getUnitGuid())) + if (Unit* target = ref->GetVictim()) { if (!(me->IsInRange(target, (float)e.event.minMaxRepeat.rangeMin, (float)e.event.minMaxRepeat.rangeMax))) continue; diff --git a/src/server/game/Battlegrounds/Battleground.cpp b/src/server/game/Battlegrounds/Battleground.cpp index 13c6f6bf0..40f342a19 100644 --- a/src/server/game/Battlegrounds/Battleground.cpp +++ b/src/server/game/Battlegrounds/Battleground.cpp @@ -903,7 +903,7 @@ void Battleground::EndBattleground(PvPTeamId winnerTeamId) { //needed cause else in av some creatures will kill the players at the end player->CombatStop(); - player->getHostileRefMgr().deleteReferences(); + player->GetThreatMgr().RemoveMeFromThreatLists(); } uint32 winner_kills = player->GetRandomWinner() ? sWorld->getIntConfig(CONFIG_BG_REWARD_WINNER_HONOR_LAST) : sWorld->getIntConfig(CONFIG_BG_REWARD_WINNER_HONOR_FIRST); diff --git a/src/server/game/Combat/CombatManager.cpp b/src/server/game/Combat/CombatManager.cpp new file mode 100644 index 000000000..64ba173e7 --- /dev/null +++ b/src/server/game/Combat/CombatManager.cpp @@ -0,0 +1,460 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +/* + * Ported from TrinityCore with modifications for AzerothCore + * Original authors: + * Treeston - Combat/Threat system rewrite (PR #19930) + * Shauren + * Spp + */ + +#include "CombatManager.h" +#include "Containers.h" +#include "Creature.h" +#include "CreatureAI.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "ThreatManager.h" +#include "Unit.h" +#include "UnitAI.h" + +/*static*/ bool CombatManager::CanBeginCombat(Unit const* a, Unit const* 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 (!a->InSamePhase(b)) + return false; + if (a->HasUnitState(UNIT_STATE_EVADE) || b->HasUnitState(UNIT_STATE_EVADE)) + return false; + if (a->HasUnitState(UNIT_STATE_IN_FLIGHT) || b->HasUnitState(UNIT_STATE_IN_FLIGHT)) + return false; + // ... both units must be allowed to enter combat + if (a->IsCombatDisallowed() || b->IsCombatDisallowed()) + return false; + if (a->IsFriendlyTo(b) || b->IsFriendlyTo(a)) + return false; + Player const* playerA = a->GetCharmerOrOwnerPlayerOrPlayerItself(); + Player const* 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; +} + +void CombatReference::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->GetThreatMgr().ClearThreat(second); + second->GetThreatMgr().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 const needFirstAI = first->GetCombatManager().UpdateOwnerCombatState(); + bool const needSecondAI = second->GetCombatManager().UpdateOwnerCombatState(); + + // ...and if that happened, also notify the AI of it... + if (needFirstAI) + if (UnitAI* firstAI = first->GetAI()) + firstAI->JustExitedCombat(); + if (needSecondAI) + if (UnitAI* secondAI = second->GetAI()) + secondAI->JustExitedCombat(); + + // ...and finally clean up the reference object + delete this; +} + +void CombatReference::Refresh() +{ + 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 CombatReference::SuppressFor(Unit* who) +{ + Suppress(who); + if (who->GetCombatManager().UpdateOwnerCombatState()) + if (UnitAI* ai = who->GetAI()) + ai->JustExitedCombat(); +} + +bool PvPCombatReference::Update(uint32 tdiff) +{ + if (_combatTimer <= tdiff) + return false; + _combatTimer -= tdiff; + return true; +} + +void PvPCombatReference::RefreshTimer() +{ + _combatTimer = PVP_COMBAT_TIMEOUT; +} + +CombatManager::~CombatManager() +{ + ASSERT(_pveRefs.empty(), "CombatManager::~CombatManager - %s: we still have %zu PvE combat references, one of them is with %s", _owner->GetGUID().ToString().c_str(), _pveRefs.size(), _pveRefs.begin()->first.ToString().c_str()); + ASSERT(_pvpRefs.empty(), "CombatManager::~CombatManager - %s: we still have %zu PvP combat references, one of them is with %s", _owner->GetGUID().ToString().c_str(), _pvpRefs.size(), _pvpRefs.begin()->first.ToString().c_str()); +} + +void CombatManager::Update(uint32 tdiff) +{ + // Update evade timer + if (_evadeTimer) + { + if (_evadeTimer <= tdiff) + { + _evadeTimer = 0; + // Timer expired - trigger evade check via AI + if (UnitAI* ai = _owner->GetAI()) + ai->EvadeTimerExpired(); + } + else + _evadeTimer -= tdiff; + } + + auto it = _pvpRefs.begin(), end = _pvpRefs.end(); + while (it != end) + { + PvPCombatReference* const ref = it->second; + if (ref->first == _owner && !ref->Update(tdiff)) // only update if we're the first unit involved (otherwise double decrement) + { + it = _pvpRefs.erase(it), end = _pvpRefs.end(); // remove it from our refs first to prevent invalidation + ref->EndCombat(); // this will remove it from the other side + } + else + ++it; + } +} + +bool CombatManager::HasPvECombat() const +{ + for (auto const& [guid, ref] : _pveRefs) + if (!ref->IsSuppressedFor(_owner)) + return true; + return false; +} + +bool CombatManager::HasPvECombatWithPlayers() const +{ + for (std::pair const& reference : _pveRefs) + if (!reference.second->IsSuppressedFor(_owner) && reference.second->GetOther(_owner)->IsPlayer()) + return true; + + return false; +} + +bool CombatManager::HasPvPCombat() const +{ + for (auto const& pair : _pvpRefs) + if (!pair.second->IsSuppressedFor(_owner)) + return true; + return false; +} + +Unit* CombatManager::GetAnyTarget() const +{ + for (auto const& pair : _pveRefs) + if (!pair.second->IsSuppressedFor(_owner)) + return pair.second->GetOther(_owner); + for (auto const& pair : _pvpRefs) + if (!pair.second->IsSuppressedFor(_owner)) + return pair.second->GetOther(_owner); + return nullptr; +} + +bool CombatManager::SetInCombatWith(Unit* who, bool addSecondUnitSuppressed) +{ + // Are we already in combat? If yes, refresh pvp combat + if (PvPCombatReference* existingPvpRef = Acore::Containers::MapGetValuePtr(_pvpRefs, who->GetGUID())) + { + existingPvpRef->RefreshTimer(); + existingPvpRef->Refresh(); + return true; + } + if (CombatReference* existingPveRef = Acore::Containers::MapGetValuePtr(_pveRefs, who->GetGUID())) + { + existingPveRef->Refresh(); + return true; + } + + // Otherwise, check validity... + if (!CombatManager::CanBeginCombat(_owner, who)) + return false; + + // ...then create new reference + CombatReference* ref; + if (_owner->IsControlledByPlayer() && who->IsControlledByPlayer()) + ref = new PvPCombatReference(_owner, who); + else + ref = new CombatReference(_owner, who); + + if (addSecondUnitSuppressed) + ref->Suppress(who); + + // ...and insert it into both managers + PutReference(who->GetGUID(), ref); + who->GetCombatManager().PutReference(_owner->GetGUID(), ref); + + // 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 const needSelfAI = UpdateOwnerCombatState(); + bool const 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 IsInCombatWith(who); +} + +bool CombatManager::IsInCombatWith(ObjectGuid const& guid) const +{ + return (_pveRefs.find(guid) != _pveRefs.end()) || (_pvpRefs.find(guid) != _pvpRefs.end()); +} + +bool CombatManager::IsInCombatWith(Unit const* who) const +{ + return IsInCombatWith(who->GetGUID()); +} + +void CombatManager::InheritCombatStatesFrom(Unit const* who) +{ + CombatManager const& mgr = who->GetCombatManager(); + for (auto& ref : mgr._pveRefs) + { + if (!IsInCombatWith(ref.first)) + { + Unit* target = ref.second->GetOther(who); + if ((_owner->IsImmuneToPC() && target->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) || + (_owner->IsImmuneToNPC() && !target->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED))) + continue; + SetInCombatWith(target); + } + } + for (auto& ref : mgr._pvpRefs) + { + Unit* target = ref.second->GetOther(who); + if ((_owner->IsImmuneToPC() && target->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) || + (_owner->IsImmuneToNPC() && !target->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED))) + continue; + SetInCombatWith(target); + } +} + +void CombatManager::EndCombatBeyondRange(float range, bool includingPvP) +{ + auto it = _pveRefs.begin(), end = _pveRefs.end(); + while (it != end) + { + CombatReference* const ref = it->second; + if (!ref->first->IsWithinDistInMap(ref->second, range)) + { + it = _pveRefs.erase(it), end = _pveRefs.end(); // erase manually here to avoid iterator invalidation + ref->EndCombat(); + } + else + ++it; + } + + if (!includingPvP) + return; + + auto it2 = _pvpRefs.begin(), end2 = _pvpRefs.end(); + while (it2 != end2) + { + CombatReference* const ref = it2->second; + if (!ref->first->IsWithinDistInMap(ref->second, range)) + { + it2 = _pvpRefs.erase(it2), end2 = _pvpRefs.end(); // erase manually here to avoid iterator invalidation + ref->EndCombat(); + } + else + ++it2; + } +} + +void CombatManager::SuppressPvPCombat() +{ + for (auto const& pair : _pvpRefs) + pair.second->Suppress(_owner); + if (UpdateOwnerCombatState()) + if (UnitAI* ownerAI = _owner->GetAI()) + ownerAI->JustExitedCombat(); +} + +void CombatManager::EndAllPvECombat() +{ + // cannot have threat without combat + _owner->GetThreatMgr().RemoveMeFromThreatLists(); + _owner->GetThreatMgr().ClearAllThreat(); + while (!_pveRefs.empty()) + _pveRefs.begin()->second->EndCombat(); +} + +void CombatManager::RevalidateCombat() +{ + auto it = _pveRefs.begin(), end = _pveRefs.end(); + while (it != end) + { + CombatReference* const ref = it->second; + if (!CanBeginCombat(_owner, ref->GetOther(_owner))) + { + it = _pveRefs.erase(it), end = _pveRefs.end(); // erase manually here to avoid iterator invalidation + ref->EndCombat(); + } + else + ++it; + } + + auto it2 = _pvpRefs.begin(), end2 = _pvpRefs.end(); + while (it2 != end2) + { + CombatReference* const ref = it2->second; + if (!CanBeginCombat(_owner, ref->GetOther(_owner))) + { + it2 = _pvpRefs.erase(it2), end2 = _pvpRefs.end(); // erase manually here to avoid iterator invalidation + ref->EndCombat(); + } + else + ++it2; + } +} + +void CombatManager::EndAllPvPCombat() +{ + while (!_pvpRefs.empty()) + _pvpRefs.begin()->second->EndCombat(); +} + +/*static*/ void CombatManager::NotifyAICombat(Unit* me, Unit* other) +{ + if (UnitAI* ai = me->GetAI()) + ai->JustEnteredCombat(other); +} + +void CombatManager::PutReference(ObjectGuid const& guid, CombatReference* ref) +{ + if (ref->_isPvP) + { + auto& inMap = _pvpRefs[guid]; + ASSERT(!inMap, "Duplicate combat state at %p being inserted for %s vs %s - memory leak!", (void*)ref, _owner->GetGUID().ToString().c_str(), guid.ToString().c_str()); + inMap = static_cast(ref); + } + else + { + auto& inMap = _pveRefs[guid]; + ASSERT(!inMap, "Duplicate combat state at %p being inserted for %s vs %s - memory leak!", (void*)ref, _owner->GetGUID().ToString().c_str(), guid.ToString().c_str()); + inMap = ref; + } +} + +void CombatManager::PurgeReference(ObjectGuid const& guid, bool pvp) +{ + if (pvp) + _pvpRefs.erase(guid); + else + _pveRefs.erase(guid); +} + +bool CombatManager::UpdateOwnerCombatState() const +{ + bool const combatState = HasCombat(); + if (combatState == _owner->IsInCombat()) + return false; + + if (combatState) + { + _owner->SetUnitFlag(UNIT_FLAG_IN_COMBAT); + _owner->AtEnterCombat(); + if (!_owner->IsCreature()) + _owner->AtEngage(GetAnyTarget()); + + if (Player* player = _owner->ToPlayer()) + sScriptMgr->OnPlayerEnterCombat(player, GetAnyTarget()); + } + else + { + _owner->RemoveUnitFlag(UNIT_FLAG_IN_COMBAT); + _owner->AtExitCombat(); + if (!_owner->IsCreature()) + _owner->AtDisengage(); + } + + if (Unit* master = _owner->GetCharmerOrOwner()) + master->UpdatePetCombatState(); + + return true; +} + +void CombatManager::SetEvadeState(EvadeState state) +{ + if (_evadeState == state) + return; + + if (state == EVADE_STATE_NONE) + _evadeTimer = 0; + + _evadeState = state; + + // Propagate evade state to controlled units (pets, guardians, etc.) + if (!_owner->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) + { + for (Unit* controlled : _owner->m_Controlled) + controlled->GetCombatManager().SetEvadeState(state); + } +} + +void CombatManager::StopEvade() +{ + _evadeTimer = 0; + SetEvadeState(EVADE_STATE_NONE); +} diff --git a/src/server/game/Combat/CombatManager.h b/src/server/game/Combat/CombatManager.h new file mode 100644 index 000000000..f8e3aef00 --- /dev/null +++ b/src/server/game/Combat/CombatManager.h @@ -0,0 +1,167 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef ACORE_COMBATMANAGER_H +#define ACORE_COMBATMANAGER_H + +#include "Common.h" +#include "ObjectGuid.h" +#include "Position.h" +#include + +class Unit; + +enum EvadeState : uint8 +{ + EVADE_STATE_NONE = 0, // Not evading + EVADE_STATE_COMBAT = 1, // In combat evade (target unreachable in dungeon) + EVADE_STATE_HOME = 2, // Running home after combat ends +}; + +/********************************************************************************************************************************************************\ + * DEV DOCUMENTATION: COMBAT SYSTEM * + * (future devs: please keep this up-to-date if you change the system) * + * CombatManager maintains a list of dynamically allocated CombatReference entries. Each entry represents a combat state between two distinct units. * + * A unit is "in combat" iff it has one or more non-suppressed CombatReference entries in its CombatManager. No exceptions. * + * * + * A CombatReference object carries the following implicit guarantees by existing: * + * - Both CombatReference.first and CombatReference.second are valid Units, distinct, not nullptr and currently in the world. * + * - If the CombatReference was retrieved from the CombatManager of Unit* A, then exactly one of .first and .second is equal to A. * + * - Note: Use CombatReference::GetOther to quickly get the other unit for a given reference. * + * - Both .first and .second are currently in combat (IsInCombat will always be true) if either of the following hold: * + * - IsSuppressedFor returns false for the respective unit * + * * + * To end combat between two units, find their CombatReference and call EndCombat. * + * - Keep in mind that this modifies the CombatRefs maps on both ends, which may cause iterators to be invalidated. * + * * + * To put two units in combat with each other, call SetInCombatWith. Note that this is not guaranteed to succeed. * + * - The return value of SetInCombatWith is the new combat state between the units (identical to calling IsInCombatWith at that time). * + * * + * Note that (threat => combat) is a strong guarantee provided in conjunction with ThreatManager. Thus: * + * - Ending combat between two units will also delete any threat references that may exist between them. * + * - Adding threat will also create a combat reference between the units if one doesn't exist yet. * +\********************************************************************************************************************************************************/ + +struct AC_GAME_API CombatReference +{ + Unit* const first; + Unit* const second; + bool const _isPvP; + Unit* GetOther(Unit const* me) const { return (first == me) ? second : first; } + + void EndCombat(); + + // suppressed combat refs do not generate a combat state for one side of the relation + // (used by: vanish, feign death and launched out of combat but not yet landed spell missiles) + void SuppressFor(Unit* who); + bool IsSuppressedFor(Unit const* who) const { return (who == first) ? _suppressFirst : _suppressSecond; } + + CombatReference(CombatReference const&) = delete; + CombatReference& operator=(CombatReference const&) = delete; + +protected: + CombatReference(Unit* a, Unit* b, bool pvp = false) : first(a), second(b), _isPvP(pvp) { } + + void Refresh(); + void Suppress(Unit* who) { (who == first ? _suppressFirst : _suppressSecond) = true; } + + bool _suppressFirst = false; + bool _suppressSecond = false; + + friend class CombatManager; +}; + +struct AC_GAME_API PvPCombatReference : public CombatReference +{ + static const uint32 PVP_COMBAT_TIMEOUT = 5 * IN_MILLISECONDS; + +private: + PvPCombatReference(Unit* first, Unit* second) : CombatReference(first, second, true) { } + + bool Update(uint32 tdiff); + void RefreshTimer(); + + uint32 _combatTimer = PVP_COMBAT_TIMEOUT; + + friend class CombatManager; +}; + +class AC_GAME_API CombatManager +{ +public: + static bool CanBeginCombat(Unit const* a, Unit const* b); + + static constexpr uint32 EVADE_TIMER_DURATION = 10 * IN_MILLISECONDS; + static constexpr uint32 EVADE_REGEN_DELAY = 5 * IN_MILLISECONDS; + + CombatManager(Unit* owner) : _owner(owner), _evadeState(EVADE_STATE_NONE), _evadeTimer(0) { } + ~CombatManager(); + void Update(uint32 tdiff); // called from Unit::Update + + Unit* GetOwner() const { return _owner; } + + // Evade state handling + EvadeState GetEvadeState() const { return _evadeState; } + void SetEvadeState(EvadeState state); + bool IsEvadingHome() const { return _evadeState == EVADE_STATE_HOME; } + bool IsInEvadeMode() const { return _evadeTimer > 0 || _evadeState != EVADE_STATE_NONE; } + bool IsEvadeRegen() const { return (_evadeTimer > 0 && _evadeTimer <= EVADE_REGEN_DELAY) || _evadeState != EVADE_STATE_NONE; } + void StartEvadeTimer() { _evadeTimer = EVADE_TIMER_DURATION; } + void ContinueEvadeRegen() { _evadeTimer = EVADE_REGEN_DELAY; } + void StopEvade(); + + bool HasCombat() const { return HasPvECombat() || HasPvPCombat(); } + bool HasPvECombat() const; + bool HasPvECombatWithPlayers() const; + std::unordered_map const& GetPvECombatRefs() const { return _pveRefs; } + bool HasPvPCombat() const; + std::unordered_map const& GetPvPCombatRefs() const { return _pvpRefs; } + // If the Unit is in combat, returns an arbitrary Unit that it's in combat with. Otherwise, returns nullptr. + Unit* GetAnyTarget() const; + + // return value is the same as calling IsInCombatWith immediately after this returns + bool SetInCombatWith(Unit* who, bool addSecondUnitSuppressed = false); + bool IsInCombatWith(ObjectGuid const& who) const; + bool IsInCombatWith(Unit const* who) const; + void InheritCombatStatesFrom(Unit const* who); + void EndCombatBeyondRange(float range, bool includingPvP = false); + // flags any pvp refs for suppression on owner's side - these refs will not generate combat until refreshed + void SuppressPvPCombat(); + void EndAllPvECombat(); + void RevalidateCombat(); + void EndAllPvPCombat(); + void EndAllCombat() { EndAllPvECombat(); EndAllPvPCombat(); } + + CombatManager(CombatManager const&) = delete; + CombatManager& operator=(CombatManager const&) = delete; + +private: + static void NotifyAICombat(Unit* me, Unit* other); + void PutReference(ObjectGuid const& guid, CombatReference* ref); + void PurgeReference(ObjectGuid const& guid, bool pvp); + bool UpdateOwnerCombatState() const; + Unit* const _owner; + EvadeState _evadeState; + uint32 _evadeTimer; + std::unordered_map _pveRefs; + std::unordered_map _pvpRefs; + + friend struct CombatReference; + friend struct PvPCombatReference; +}; + +#endif diff --git a/src/server/game/Combat/HostileRefMgr.cpp b/src/server/game/Combat/HostileRefMgr.cpp deleted file mode 100644 index e8a1ebebe..000000000 --- a/src/server/game/Combat/HostileRefMgr.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * 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 2 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 . - */ - -#include "HostileRefMgr.h" -#include "CreatureAI.h" -#include "SpellInfo.h" -#include "SpellMgr.h" -#include "ThreatMgr.h" -#include "Unit.h" - -HostileRefMgr::~HostileRefMgr() -{ - deleteReferences(); -} - -//================================================= -// send threat to all my hateres for the victim -// The victim is hated than by them as well -// use for buffs and healing threat functionality - -void HostileRefMgr::threatAssist(Unit* victim, float baseThreat, SpellInfo const* threatSpell) -{ - if (getSize() == 0) - return; - - HostileReference* ref = getFirst(); - float threat = ThreatCalcHelper::calcThreat(victim, baseThreat, (threatSpell ? threatSpell->GetSchoolMask() : SPELL_SCHOOL_MASK_NORMAL), threatSpell); - threat /= getSize(); - while (ref) - { - Unit* refOwner = ref->GetSource()->GetOwner(); - if (ThreatCalcHelper::isValidProcess(victim, refOwner, threatSpell)) - { - if (Creature* hatingCreature = refOwner->ToCreature()) - { - if (hatingCreature->IsAIEnabled) - { - hatingCreature->AI()->CalculateThreat(victim, threat, threatSpell); - } - } - - ref->GetSource()->DoAddThreat(victim, threat); - } - - ref = ref->next(); - } -} - -//================================================= - -void HostileRefMgr::addTempThreat(float threat, bool apply) -{ - HostileReference* ref = getFirst(); - - while (ref) - { - if (apply) - { - if (ref->getTempThreatModifier() == 0.0f) - ref->addTempThreat(threat); - } - else - ref->resetTempThreat(); - - ref = ref->next(); - } -} - -//================================================= - -void HostileRefMgr::addThreatPercent(int32 percent) -{ - HostileReference* ref = getFirst(); - while (ref) - { - ref->addThreatPercent(percent); - ref = ref->next(); - } -} - -//================================================= -// The online / offline status is given to the method. The calculation has to be done before - -void HostileRefMgr::setOnlineOfflineState(bool isOnline) -{ - HostileReference* ref = getFirst(); - while (ref) - { - ref->setOnlineOfflineState(isOnline); - ref = ref->next(); - } -} - -//================================================= -// The online / offline status is calculated and set - -void HostileRefMgr::updateThreatTables() -{ - HostileReference* ref = getFirst(); - while (ref) - { - ref->updateOnlineStatus(); - ref = ref->next(); - } -} - -//================================================= -// The references are not needed anymore -// tell the source to remove them from the list and free the mem - -void HostileRefMgr::deleteReferences(bool removeFromMap /*= false*/) -{ - std::vector creaturesToEvade; - - HostileReference* ref = getFirst(); - while (ref) - { - HostileReference* nextRef = ref->next(); - ref->removeReference(); - - if (removeFromMap) - { - if (ThreatMgr const* threatMgr = ref->GetSource()) - { - if (threatMgr->areThreatListsEmpty()) - { - if (Creature* creature = threatMgr->GetOwner()->ToCreature()) - { - creaturesToEvade.push_back(creature); - } - } - } - } - - delete ref; - ref = nextRef; - } - - for (Creature* creature : creaturesToEvade) - { - creature->AI()->EnterEvadeMode(); - } -} - -//================================================= -// delete one reference, defined by faction - -void HostileRefMgr::deleteReferencesForFaction(uint32 faction) -{ - HostileReference* ref = getFirst(); - while (ref) - { - HostileReference* nextRef = ref->next(); - if (ref->GetSource()->GetOwner()->GetFactionTemplateEntry()->faction == faction) - { - ref->removeReference(); - delete ref; - } - ref = nextRef; - } -} - -//================================================= -// delete one reference, defined by Unit - -void HostileRefMgr::deleteReference(Unit* creature) -{ - HostileReference* ref = getFirst(); - while (ref) - { - HostileReference* nextRef = ref->next(); - if (ref->GetSource()->GetOwner() == creature) - { - ref->removeReference(); - delete ref; - break; - } - ref = nextRef; - } -} - -//================================================= -// delete all references out of specified range - -void HostileRefMgr::deleteReferencesOutOfRange(float range) -{ - HostileReference* ref = getFirst(); - range = range * range; - while (ref) - { - HostileReference* nextRef = ref->next(); - Unit* owner = ref->GetSource()->GetOwner(); - if (!owner->isActiveObject() && owner->GetExactDist2dSq(GetOwner()) > range) - { - ref->removeReference(); - delete ref; - } - ref = nextRef; - } -} - -//================================================= -// set state for one reference, defined by Unit - -void HostileRefMgr::setOnlineOfflineState(Unit* creature, bool isOnline) -{ - HostileReference* ref = getFirst(); - while (ref) - { - HostileReference* nextRef = ref->next(); - if (ref->GetSource()->GetOwner() == creature) - { - ref->setOnlineOfflineState(isOnline); - break; - } - ref = nextRef; - } -} - -//================================================= - -void HostileRefMgr::UpdateVisibility(bool checkThreat) -{ - HostileReference* ref = getFirst(); - while (ref) - { - HostileReference* nextRef = ref->next(); - if ((!checkThreat || ref->GetSource()->GetThreatListSize() <= 1)) - { - nextRef = ref->next(); - ref->removeReference(); - delete ref; - } - ref = nextRef; - } -} diff --git a/src/server/game/Combat/HostileRefMgr.h b/src/server/game/Combat/HostileRefMgr.h deleted file mode 100644 index 728f201bb..000000000 --- a/src/server/game/Combat/HostileRefMgr.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * 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 2 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 . - */ - -#ifndef _HOSTILEREFMANAGER -#define _HOSTILEREFMANAGER - -#include "RefMgr.h" - -class Unit; -class ThreatMgr; -class HostileReference; -class SpellInfo; - -//================================================= - -class HostileRefMgr : public RefMgr -{ -private: - Unit* iOwner; -public: - explicit HostileRefMgr(Unit* owner) { iOwner = owner; } - ~HostileRefMgr() override; - - Unit* GetOwner() { return iOwner; } - - // send threat to all my hateres for the victim - // The victim is hated than by them as well - // use for buffs and healing threat functionality - void threatAssist(Unit* victim, float baseThreat, SpellInfo const* threatSpell = nullptr); - - void addTempThreat(float threat, bool apply); - - void addThreatPercent(int32 percent); - - // The references are not needed anymore - // tell the source to remove them from the list and free the mem - void deleteReferences(bool removeFromMap = false); - - // Remove specific faction references - void deleteReferencesForFaction(uint32 faction); - - // pussywizard: for combat bugs - void deleteReferencesOutOfRange(float range); - - HostileReference* getFirst() { return ((HostileReference*) RefMgr::getFirst()); } - - void updateThreatTables(); - - void setOnlineOfflineState(bool isOnline); - - // set state for one reference, defined by Unit - void setOnlineOfflineState(Unit* creature, bool isOnline); - - // delete one reference, defined by Unit - void deleteReference(Unit* creature); - - void UpdateVisibility(bool checkThreat); -}; -//================================================= -#endif diff --git a/src/server/game/Combat/ThreatManager.cpp b/src/server/game/Combat/ThreatManager.cpp new file mode 100644 index 000000000..ce37f961c --- /dev/null +++ b/src/server/game/Combat/ThreatManager.cpp @@ -0,0 +1,947 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +/* + * Ported from TrinityCore with modifications for AzerothCore + * Original authors: + * Treeston - Combat/Threat system rewrite (PR #19930) + * Shauren + * Spp + */ + +#include "ThreatManager.h" +#include "CombatManager.h" +#include "Containers.h" +#include "Creature.h" +#include "CreatureAI.h" +#include "CreatureGroups.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "SpellAuraEffects.h" +#include "SpellInfo.h" +#include "SpellMgr.h" +#include "TemporarySummon.h" +#include "Unit.h" +#include "UnitAI.h" +#include "WorldPacket.h" +#include +#include + +const CompareThreatLessThan ThreatManager::CompareThreat; + +class ThreatManager::Heap : public boost::heap::fibonacci_heap> +{ +}; + +void ThreatReference::AddThreat(float amount) +{ + if (amount == 0.0f) + return; + _baseAmount = std::max(_baseAmount + amount, 0.0f); + if (amount > 0.0f) + HeapNotifyIncreased(); + else + HeapNotifyDecreased(); + _mgr._needClientUpdate = true; +} + +void ThreatReference::ScaleThreat(float factor) +{ + if (factor == 1.0f) + return; + _baseAmount *= factor; + if (factor > 1.0f) + HeapNotifyIncreased(); + else + HeapNotifyDecreased(); + _mgr._needClientUpdate = true; +} + +void ThreatReference::UpdateOffline() +{ + bool const shouldBeOffline = ShouldBeOffline(); + if (shouldBeOffline == IsOffline()) + return; + + if (shouldBeOffline) + { + _online = ONLINE_STATE_OFFLINE; + HeapNotifyDecreased(); + _mgr.SendRemoveToClients(_victim); + } + else + { + _online = ShouldBeSuppressed() ? ONLINE_STATE_SUPPRESSED : ONLINE_STATE_ONLINE; + HeapNotifyIncreased(); + _mgr.RegisterForAIUpdate(GetVictim()->GetGUID()); + } +} + +/*static*/ bool ThreatReference::FlagsAllowFighting(Unit const* a, Unit const* b) +{ + if (a->IsCreature() && a->ToCreature()->IsTrigger()) + return false; + if (a->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) + { + if (b->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC)) + return false; + } + else + { + if (b->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC)) + return false; + } + return true; +} + +bool ThreatReference::ShouldBeOffline() const +{ + if (!_owner->CanSeeOrDetect(_victim)) + return true; + if (!_owner->_IsTargetAcceptable(_victim) || !_owner->CanCreatureAttack(_victim)) + return true; + if (!FlagsAllowFighting(_owner, _victim) || !FlagsAllowFighting(_victim, _owner)) + return true; + return false; +} + +bool ThreatReference::ShouldBeSuppressed() const +{ + if (IsTaunting()) // a taunting victim can never be suppressed + return false; + if (_victim->IsImmunedToDamage(_owner->GetMeleeDamageSchoolMask())) + return true; + if (_victim->HasAuraType(SPELL_AURA_MOD_CONFUSE)) + return true; + if (_victim->HasBreakableByDamageAuraType(SPELL_AURA_MOD_STUN)) + return true; + return false; +} + +void ThreatReference::UpdateTauntState(TauntState state) +{ + // Check for SPELL_AURA_MOD_DETAUNT (applied from owner to victim) + if (state < TAUNT_STATE_TAUNT && _victim->HasAuraTypeWithCaster(SPELL_AURA_MOD_DETAUNT, _owner->GetGUID())) + state = TAUNT_STATE_DETAUNT; + + if (state == _taunted) + return; + + std::swap(state, _taunted); + + if (_taunted < state) + HeapNotifyDecreased(); + else + HeapNotifyIncreased(); + + _mgr._needClientUpdate = true; +} + +void ThreatReference::ClearThreat() +{ + _mgr.ClearThreat(this); +} + +void ThreatReference::UnregisterAndFree() +{ + _owner->GetThreatMgr().PurgeThreatListRef(_victim->GetGUID()); + _victim->GetThreatMgr().PurgeThreatenedByMeRef(_owner->GetGUID()); + delete this; +} + +class ThreatReferenceImpl : public ThreatReference +{ +public: + explicit ThreatReferenceImpl(ThreatManager* mgr, Unit* victim) : ThreatReference(mgr, victim) + { + // Only creatures can have threat lists (verified by CanHaveThreatList) + ASSERT(mgr->_owner->ToCreature()); + } + + ThreatManager::Heap::handle_type _handle; +}; + +void ThreatReference::HeapNotifyIncreased() +{ + _mgr._sortedThreatList->increase(static_cast(this)->_handle); +} + +void ThreatReference::HeapNotifyDecreased() +{ + _mgr._sortedThreatList->decrease(static_cast(this)->_handle); +} + +/*static*/ bool ThreatManager::CanHaveThreatList(Unit const* who) +{ + if (!who) + return false; + + Creature const* cWho = who->ToCreature(); + // only creatures can have threat list + if (!cWho) + return false; + + // pets, totems and triggers cannot have threat list + if (cWho->IsPet() || cWho->IsTotem() || cWho->IsTrigger()) + return false; + + // summons cannot have a threat list if they were summoned by a player + if (cWho->HasUnitTypeMask(UNIT_MASK_MINION | UNIT_MASK_GUARDIAN)) + if (TempSummon const* tWho = cWho->ToTempSummon()) + if (tWho->GetSummonerGUID().IsPlayer()) + return false; + + return true; +} + +ThreatManager::ThreatManager(Unit* owner) : _owner(owner), _ownerCanHaveThreatList(false), _needClientUpdate(false), _updateTimer(THREAT_UPDATE_INTERVAL), + _sortedThreatList(std::make_unique()), _currentVictimRef(nullptr), _fixateRef(nullptr) +{ + for (int8 i = 0; i < MAX_SPELL_SCHOOL; ++i) + _singleSchoolModifiers[i] = 1.0f; +} + +ThreatManager::~ThreatManager() +{ + ASSERT(_myThreatListEntries.empty(), "ThreatManager::~ThreatManager - %s: we still have %zu things threatening us, one of them is %s.", _owner->GetGUID().ToString().c_str(), _myThreatListEntries.size(), _myThreatListEntries.begin()->first.ToString().c_str()); + ASSERT(_sortedThreatList->empty(), "ThreatManager::~ThreatManager - %s: we still have %zu things threatening us, one of them is %s.", _owner->GetGUID().ToString().c_str(), _sortedThreatList->size(), (*_sortedThreatList->begin())->GetVictim()->GetGUID().ToString().c_str()); + ASSERT(_threatenedByMe.empty(), "ThreatManager::~ThreatManager - %s: we are still threatening %zu things, one of them is %s.", _owner->GetGUID().ToString().c_str(), _threatenedByMe.size(), _threatenedByMe.begin()->first.ToString().c_str()); +} + +void ThreatManager::Initialize() +{ + _ownerCanHaveThreatList = ThreatManager::CanHaveThreatList(_owner); +} + +void ThreatManager::Update(uint32 tdiff) +{ + if (!CanHaveThreatList() || IsThreatListEmpty(true)) + return; + if (_updateTimer <= tdiff) + { + UpdateVictim(); + _updateTimer = THREAT_UPDATE_INTERVAL; + } + else + _updateTimer -= tdiff; +} + +Unit* ThreatManager::GetCurrentVictim() +{ + if (!_currentVictimRef || _currentVictimRef->ShouldBeOffline()) + UpdateVictim(); + ASSERT(!_currentVictimRef || _currentVictimRef->IsAvailable()); + return _currentVictimRef ? _currentVictimRef->GetVictim() : nullptr; +} + +Unit* ThreatManager::GetLastVictim() const +{ + if (_currentVictimRef && !_currentVictimRef->ShouldBeOffline()) + return _currentVictimRef->GetVictim(); + return nullptr; +} + +Unit* ThreatManager::GetAnyTarget() const +{ + for (ThreatReference const* ref : *_sortedThreatList) + if (!ref->IsOffline()) + return ref->GetVictim(); + return nullptr; +} + +bool ThreatManager::IsThreatListEmpty(bool includeOffline) const +{ + if (includeOffline) + return _sortedThreatList->empty(); + for (ThreatReference const* ref : *_sortedThreatList) + if (ref->IsAvailable()) + return false; + return true; +} + +bool ThreatManager::IsThreatenedBy(ObjectGuid const& who, bool includeOffline) const +{ + auto it = _myThreatListEntries.find(who); + if (it == _myThreatListEntries.end()) + return false; + return (includeOffline || it->second->IsAvailable()); +} +bool ThreatManager::IsThreatenedBy(Unit const* who, bool includeOffline) const { return IsThreatenedBy(who->GetGUID(), includeOffline); } + +float ThreatManager::GetThreat(Unit const* who, bool includeOffline) const +{ + auto it = _myThreatListEntries.find(who->GetGUID()); + if (it == _myThreatListEntries.end()) + return 0.0f; + return (includeOffline || it->second->IsAvailable()) ? it->second->GetThreat() : 0.0f; +} + +size_t ThreatManager::GetThreatListSize() const +{ + return _sortedThreatList->size(); +} + +uint32 ThreatManager::GetThreatListPlayerCount(bool includeOffline/* = false*/) const +{ + uint32 returnValue = 0; + for (ThreatReference const* ref : *_sortedThreatList) + { + if (!includeOffline && !ref->IsAvailable()) + continue; + if (ref->GetVictim()->IsPlayer()) + ++returnValue; + } + return returnValue; +} + +Acore::IteratorPair ThreatManager::GetUnsortedThreatList() const +{ + auto itr = _myThreatListEntries.begin(); + auto end = _myThreatListEntries.end(); + std::function generator = [itr, end]() mutable -> ThreatReference const* + { + if (itr == end) + return nullptr; + + return (itr++)->second; + }; + std::function endGenerator = []() -> ThreatReference const* { return nullptr; }; + return { ThreatListIterator{ std::move(generator) }, ThreatListIterator{ std::move(endGenerator) } }; +} + +Acore::IteratorPair ThreatManager::GetSortedThreatList() const +{ + auto itr = _sortedThreatList->ordered_begin(); + auto end = _sortedThreatList->ordered_end(); + std::function generator = [itr, end]() mutable -> ThreatReference const* + { + if (itr == end) + return nullptr; + + return *(itr++); + }; + std::function endGenerator = []() -> ThreatReference const* { return nullptr; }; + return { ThreatListIterator{ std::move(generator) }, ThreatListIterator{ std::move(endGenerator) } }; +} + +std::vector ThreatManager::GetModifiableThreatList() +{ + std::vector list; + list.reserve(_myThreatListEntries.size()); + for (auto it = _sortedThreatList->ordered_begin(), end = _sortedThreatList->ordered_end(); it != end; ++it) + list.push_back(const_cast(*it)); + return list; +} + +bool ThreatManager::IsThreateningAnyone(bool includeOffline) const +{ + if (includeOffline) + return !_threatenedByMe.empty(); + for (auto const& pair : _threatenedByMe) + if (pair.second->IsAvailable()) + return true; + return false; +} + +bool ThreatManager::IsThreateningTo(ObjectGuid const& who, bool includeOffline) const +{ + auto it = _threatenedByMe.find(who); + if (it == _threatenedByMe.end()) + return false; + return (includeOffline || it->second->IsAvailable()); +} +bool ThreatManager::IsThreateningTo(Unit const* who, bool includeOffline) const { return IsThreateningTo(who->GetGUID(), includeOffline); } + +void ThreatManager::EvaluateSuppressed(bool canExpire) +{ + for (auto const& pair : _threatenedByMe) + { + bool const shouldBeSuppressed = pair.second->ShouldBeSuppressed(); + if (pair.second->IsOnline() && shouldBeSuppressed) + { + pair.second->_online = ThreatReference::ONLINE_STATE_SUPPRESSED; + pair.second->HeapNotifyDecreased(); + } + else if (canExpire && pair.second->IsSuppressed() && !shouldBeSuppressed) + { + pair.second->_online = ThreatReference::ONLINE_STATE_ONLINE; + pair.second->HeapNotifyIncreased(); + } + } +} + +void ThreatManager::AddThreat(Unit* target, float amount, SpellInfo const* spell, bool ignoreModifiers, bool ignoreRedirects) +{ + // step 1: we can shortcut if the spell has one of the NO_THREAT attrs set - nothing will happen + if (spell) + { + if (spell->HasAttribute(SPELL_ATTR1_NO_THREAT)) + return; + if (!_owner->IsEngaged() && spell->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS)) + return; + } + + // while riding a vehicle, all threat goes to the vehicle, not the pilot + if (Unit* vehicle = target->GetVehicleBase()) + { + AddThreat(vehicle, amount, spell, ignoreModifiers, ignoreRedirects); + if (target->HasUnitTypeMask(UNIT_MASK_ACCESSORY)) // accessories are fully treated as components of the parent and cannot have threat + return; + 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 + for (auto const& pair : target->GetThreatMgr()._redirectInfo) + if (!combatMgr.IsInCombatWith(pair.first)) + if (Unit* redirTarget = ObjectAccessor::GetUnit(*_owner, pair.first)) + combatMgr.SetInCombatWith(redirTarget); + return; + } + + // apply threat modifiers to the amount + if (!ignoreModifiers) + amount = CalculateModifiedThreat(amount, target, spell); + + // if we're increasing threat, send some/all of it to redirection targets instead if applicable + if (!ignoreRedirects && amount > 0.0f) + { + auto const& redirInfo = target->GetThreatMgr()._redirectInfo; + if (!redirInfo.empty()) + { + float const 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 (size_t i = 0; i < redirInfo.size(); ++i) + { + auto const pair = redirInfo[i]; // (victim,pct) + Unit* redirTarget = nullptr; + auto it = _myThreatListEntries.find(pair.first); // try to look it up in our threat list first (faster) + if (it != _myThreatListEntries.end()) + redirTarget = it->second->_victim; + else + redirTarget = ObjectAccessor::GetUnit(*_owner, pair.first); + + if (redirTarget) + { + float amountRedirected = CalculatePct(origAmount, pair.second); + AddThreat(redirTarget, amountRedirected, spell, true, true); + amount -= amountRedirected; + } + } + } + } + + // ensure we're in combat (threat implies combat!) + if (!_owner->GetCombatManager().SetInCombatWith(target)) // if this returns false, we're not actually in combat, and thus cannot have threat! + return; // typical causes: bad scripts trying to add threat to GMs, dead targets etc + + // ok, now we actually apply threat + // check if we already have an entry - if we do, just increase threat for that entry and we're done + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + { + ThreatReference* const ref = it->second; + + // SUPPRESSED threat states don't go back to ONLINE until threat is caused by them (retail behavior) + if (ref->GetOnlineState() == ThreatReference::ONLINE_STATE_SUPPRESSED) + if (!ref->ShouldBeSuppressed()) + { + ref->_online = ThreatReference::ONLINE_STATE_ONLINE; + ref->HeapNotifyIncreased(); + } + + if (ref->IsOnline()) + ref->AddThreat(amount); + + return; + } + + // ok, we're now in combat - create the threat list reference and push it to the respective managers + ThreatReference* ref = new ThreatReferenceImpl(this, target); + PutThreatListRef(target->GetGUID(), ref); + target->GetThreatMgr().PutThreatenedByMeRef(_owner->GetGUID(), ref); + + ref->UpdateOffline(); + if (ref->IsOnline()) // we only add the threat if the ref is currently available + ref->AddThreat(amount); + // Note: AI update registration is handled inside UpdateOffline() when transitioning from OFFLINE + + if (!_currentVictimRef) + UpdateVictim(); + else + ProcessAIUpdates(); +} + +void ThreatManager::ScaleThreat(Unit* target, float factor) +{ + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + it->second->ScaleThreat(std::max(factor, 0.0f)); +} + +void ThreatManager::MatchUnitThreatToHighestThreat(Unit* target) +{ + if (_sortedThreatList->empty()) + return; + + auto it = _sortedThreatList->ordered_begin(), end = _sortedThreatList->ordered_end(); + ThreatReference const* highest = *it; + if (!highest->IsAvailable()) + return; + + if (highest->IsTaunting() && ((++it) != end)) // might need to skip this - max threat could be the preceding element (there is only one taunt element) + { + ThreatReference const* a = *it; + if (a->IsAvailable() && a->GetThreat() > highest->GetThreat()) + highest = a; + } + + AddThreat(target, highest->GetThreat() - GetThreat(target, true), nullptr, true, true); +} + +void ThreatManager::TauntUpdate() +{ + Unit::AuraEffectList const& tauntEffects = _owner->GetAuraEffectsByType(SPELL_AURA_MOD_TAUNT); + + uint32 state = ThreatReference::TAUNT_STATE_TAUNT; + std::unordered_map tauntStates; + // Only the last taunt effect applied by something still on our threat list is considered + for (auto it = tauntEffects.begin(), end = tauntEffects.end(); it != end; ++it) + tauntStates[(*it)->GetCasterGUID()] = ThreatReference::TauntState(state++); + + for (auto const& pair : _myThreatListEntries) + { + auto it = tauntStates.find(pair.first); + if (it != tauntStates.end()) + pair.second->UpdateTauntState(it->second); + else + pair.second->UpdateTauntState(); + } + + // taunt aura update also re-evaluates all suppressed states (retail behavior) + EvaluateSuppressed(true); +} + +void ThreatManager::SetTauntStateForTesting( + Unit* target, uint32 state) +{ + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + it->second->UpdateTauntState( + static_cast(state)); +} + +void ThreatManager::ResetAllThreat() +{ + for (auto const& pair : _myThreatListEntries) + pair.second->ScaleThreat(0.0f); +} + +void ThreatManager::ClearThreat(Unit* target) +{ + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + ClearThreat(it->second); +} + +void ThreatManager::ClearThreat(ThreatReference* ref) +{ + SendRemoveToClients(ref->_victim); + ref->UnregisterAndFree(); + if (!_currentVictimRef) + UpdateVictim(); +} + +void ThreatManager::ClearAllThreat() +{ + if (!_myThreatListEntries.empty()) + { + SendClearAllThreatToClients(); + do + _myThreatListEntries.begin()->second->UnregisterAndFree(); + while (!_myThreatListEntries.empty()); + } +} + +void ThreatManager::FixateTarget(Unit* target) +{ + if (target) + { + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + { + _fixateRef = it->second; + return; + } + } + _fixateRef = nullptr; +} + +Unit* ThreatManager::GetFixateTarget() const +{ + if (_fixateRef) + return _fixateRef->GetVictim(); + else + return nullptr; +} + +void ThreatManager::UpdateVictim() +{ + ThreatReference const* const newVictim = ReselectVictim(); + bool const newHighest = newVictim && (newVictim != _currentVictimRef); + + _currentVictimRef = newVictim; + if (newHighest || _needClientUpdate) + { + SendThreatListToClients(newHighest); + _needClientUpdate = false; + } + + ProcessAIUpdates(); +} + +ThreatReference const* ThreatManager::ReselectVictim() +{ + if (_sortedThreatList->empty()) + return nullptr; + + for (auto const& pair : _myThreatListEntries) + pair.second->UpdateOffline(); // AI notifies are processed in ::UpdateVictim caller + + // fixated target is always preferred + if (_fixateRef && _fixateRef->IsAvailable()) + return _fixateRef; + + ThreatReference const* oldVictimRef = _currentVictimRef; + if (oldVictimRef && oldVictimRef->IsOffline()) + oldVictimRef = nullptr; + // in 99% of cases - we won't need to actually look at anything beyond the first element + ThreatReference const* highest = _sortedThreatList->top(); + + // if the highest reference is offline, the entire list is offline, and we indicate this + if (!highest->IsAvailable()) + return nullptr; + // if we have no old victim, or old victim is still highest, then highest is our target and we're done + if (!oldVictimRef || 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 (!ThreatManager::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 (ThreatManager::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->_victim)) + 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 + auto it = _sortedThreatList->ordered_begin(), end = _sortedThreatList->ordered_end(); + while (it != end) + { + ThreatReference const* next = *it; + // 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 (!ThreatManager::CompareReferencesLT(oldVictimRef, next, 1.1f)) + return oldVictimRef; + // if next is melee, he's above 110% and our new victim + if (_owner->IsWithinMeleeRange(next->_victim)) + return next; + // otherwise the next highest target may still be a melee above 110% and we need to look further + ++it; + } + // we should have found the old victim at some point in the loop above + ASSERT(false && "Current victim not found in sorted threat list even though it has a reference - manager desync!"); + return nullptr; +} + +void ThreatManager::ProcessAIUpdates() +{ + CreatureAI* ai = ASSERT_NOTNULL(_owner->ToCreature())->AI(); + std::vector v(std::move(_needsAIUpdate)); // _needsAIUpdate is now empty in case this triggers a recursive call + if (!ai) + return; + for (ObjectGuid const& guid : v) + if (ThreatReference const* ref = Acore::Containers::MapGetValuePtr(_myThreatListEntries, guid)) + ai->JustStartedThreateningMe(ref->GetVictim()); +} + +// returns true if a is LOWER on the threat list than b +/*static*/ bool ThreatManager::CompareReferencesLT(ThreatReference const* a, ThreatReference const* b, float aWeight) +{ + if (a->_online != b->_online) // online state precedence (ONLINE > SUPPRESSED > OFFLINE) + return a->_online < b->_online; + if (a->_taunted != b->_taunted) // taunt state precedence (TAUNT > NONE > DETAUNT) + return a->_taunted < b->_taunted; + return (a->GetThreat() * aWeight < b->GetThreat()); +} + +/*static*/ float ThreatManager::CalculateModifiedThreat(float threat, Unit const* victim, SpellInfo const* spell) +{ + // modifiers by spell + if (spell) + { + if (SpellThreatEntry const* threatEntry = sSpellMgr->GetSpellThreatEntry(spell->Id)) + if (threatEntry->pctMod != 1.0f) // flat/AP modifiers handled in Spell::HandleThreatSpells + threat *= threatEntry->pctMod; + + if (Player* modOwner = victim->GetSpellModOwner()) + modOwner->ApplySpellMod(spell->Id, SPELLMOD_THREAT, threat); + } + + // modifiers by effect school + ThreatManager const& victimMgr = victim->GetThreatMgr(); + SpellSchoolMask const mask = spell ? spell->GetSchoolMask() : SPELL_SCHOOL_MASK_NORMAL; + switch (mask) + { + case SPELL_SCHOOL_MASK_NORMAL: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_NORMAL]; + break; + case SPELL_SCHOOL_MASK_HOLY: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_HOLY]; + break; + case SPELL_SCHOOL_MASK_FIRE: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_FIRE]; + break; + case SPELL_SCHOOL_MASK_NATURE: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_NATURE]; + break; + case SPELL_SCHOOL_MASK_FROST: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_FROST]; + break; + case SPELL_SCHOOL_MASK_SHADOW: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_SHADOW]; + break; + case SPELL_SCHOOL_MASK_ARCANE: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_ARCANE]; + break; + default: + { + auto it = victimMgr._multiSchoolModifiers.find(mask); + if (it != victimMgr._multiSchoolModifiers.end()) + { + threat *= it->second; + break; + } + float mod = victim->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_THREAT, mask); + victimMgr._multiSchoolModifiers[mask] = mod; + threat *= mod; + break; + } + } + return threat; +} + +void ThreatManager::ForwardThreatForAssistingMe(Unit* assistant, float baseAmount, SpellInfo const* spell, bool ignoreModifiers) +{ + if (spell && spell->HasAttribute(SPELL_ATTR1_NO_THREAT)) // shortcut, none of the calls would do anything + return; + if (_threatenedByMe.empty()) + return; + + std::vector canBeThreatened, cannotBeThreatened; + for (auto const& pair : _threatenedByMe) + { + Creature* owner = pair.second->GetOwner(); + if (!owner->HasUnitState(UNIT_STATE_CONTROLLED)) + canBeThreatened.push_back(owner); + else + cannotBeThreatened.push_back(owner); + } + + if (!canBeThreatened.empty()) // targets under CC cannot gain assist threat - split evenly among the rest + { + float const perTarget = baseAmount / canBeThreatened.size(); + for (Creature* threatened : canBeThreatened) + threatened->GetThreatMgr().AddThreat(assistant, perTarget, spell, ignoreModifiers); + } + + for (Creature* threatened : cannotBeThreatened) + threatened->GetThreatMgr().AddThreat(assistant, 0.0f, spell, true); +} + +void ThreatManager::ResetAllMyThreatOnOthers() +{ + for (auto const& pair : _threatenedByMe) + pair.second->ScaleThreat(0.0f); +} + +void ThreatManager::RemoveMeFromThreatLists() +{ + while (!_threatenedByMe.empty()) + { + auto& ref = _threatenedByMe.begin()->second; + ref->_mgr.ClearThreat(_owner); + } +} + +void ThreatManager::UpdateMyTempModifiers() +{ + int32 mod = 0; + for (AuraEffect const* eff : _owner->GetAuraEffectsByType(SPELL_AURA_MOD_TOTAL_THREAT)) + mod += eff->GetAmount(); + + if (_threatenedByMe.empty()) + return; + + auto it = _threatenedByMe.begin(); + bool const isIncrease = (it->second->_tempModifier < mod); + do + { + it->second->_tempModifier = mod; + if (isIncrease) + it->second->HeapNotifyIncreased(); + else + it->second->HeapNotifyDecreased(); + } while ((++it) != _threatenedByMe.end()); +} + +void ThreatManager::UpdateMySpellSchoolModifiers() +{ + for (uint8 i = 0; i < MAX_SPELL_SCHOOL; ++i) + _singleSchoolModifiers[i] = _owner->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_THREAT, 1 << i); + _multiSchoolModifiers.clear(); +} + +void ThreatManager::RegisterRedirectThreat(uint32 spellId, ObjectGuid const& victim, uint32 pct) +{ + _redirectRegistry[spellId][victim] = pct; + UpdateRedirectInfo(); +} + +void ThreatManager::UnregisterRedirectThreat(uint32 spellId) +{ + auto it = _redirectRegistry.find(spellId); + if (it == _redirectRegistry.end()) + return; + _redirectRegistry.erase(it); + UpdateRedirectInfo(); +} + +void ThreatManager::UnregisterRedirectThreat(uint32 spellId, ObjectGuid const& victim) +{ + auto it = _redirectRegistry.find(spellId); + if (it == _redirectRegistry.end()) + return; + auto& victimMap = it->second; + auto it2 = victimMap.find(victim); + if (it2 == victimMap.end()) + return; + victimMap.erase(it2); + if (victimMap.empty()) + _redirectRegistry.erase(it); + UpdateRedirectInfo(); +} + +void ThreatManager::SendClearAllThreatToClients() const +{ + WorldPacket data(SMSG_THREAT_CLEAR, 8); + data << _owner->GetPackGUID(); + _owner->SendMessageToSet(&data, false); +} + +void ThreatManager::SendRemoveToClients(Unit const* victim) const +{ + WorldPacket data(SMSG_THREAT_REMOVE, 16); + data << _owner->GetPackGUID(); + data << victim->GetPackGUID(); + _owner->SendMessageToSet(&data, false); +} + +void ThreatManager::SendThreatListToClients(bool newHighest) const +{ + WorldPacket data(newHighest ? SMSG_HIGHEST_THREAT_UPDATE : SMSG_THREAT_UPDATE, (_sortedThreatList->size() + 2) * 8); // guess + data << _owner->GetPackGUID(); + if (newHighest) + data << _currentVictimRef->GetVictim()->GetPackGUID(); + size_t countPos = data.wpos(); + data << uint32(0); // placeholder + uint32 count = 0; + for (ThreatReference const* ref : *_sortedThreatList) + { + if (!ref->IsAvailable()) + continue; + data << ref->GetVictim()->GetPackGUID(); + data << uint32(ref->GetThreat() * 100); + ++count; + } + data.put(countPos, count); + _owner->SendMessageToSet(&data, false); +} + +void ThreatManager::PutThreatListRef(ObjectGuid const& guid, ThreatReference* ref) +{ + _needClientUpdate = true; + auto& inMap = _myThreatListEntries[guid]; + ASSERT(!inMap, "Duplicate threat reference at %p being inserted on %s for %s - memory leak!", (void*)ref, _owner->GetGUID().ToString().c_str(), guid.ToString().c_str()); + inMap = ref; + static_cast(ref)->_handle = _sortedThreatList->push(ref); +} + +void ThreatManager::PurgeThreatListRef(ObjectGuid const& guid) +{ + auto it = _myThreatListEntries.find(guid); + if (it == _myThreatListEntries.end()) + return; + ThreatReference* ref = it->second; + _myThreatListEntries.erase(it); + _sortedThreatList->erase(static_cast(ref)->_handle); + + if (_fixateRef == ref) + _fixateRef = nullptr; + if (_currentVictimRef == ref) + _currentVictimRef = nullptr; +} + +void ThreatManager::PutThreatenedByMeRef(ObjectGuid const& guid, ThreatReference* ref) +{ + auto& inMap = _threatenedByMe[guid]; + ASSERT(!inMap, "Duplicate threatened-by-me reference at %p being inserted on %s for %s - memory leak!", (void*)ref, _owner->GetGUID().ToString().c_str(), guid.ToString().c_str()); + inMap = ref; +} + +void ThreatManager::PurgeThreatenedByMeRef(ObjectGuid const& guid) +{ + auto it = _threatenedByMe.find(guid); + if (it != _threatenedByMe.end()) + _threatenedByMe.erase(it); +} + +void ThreatManager::UpdateRedirectInfo() +{ + _redirectInfo.clear(); + uint32 totalPct = 0; + for (auto const& pair : _redirectRegistry) // (spellid, victim -> pct) + for (auto const& victimPair : pair.second) // (victim,pct) + { + uint32 thisPct = std::min(100 - totalPct, victimPair.second); + if (thisPct > 0) + { + _redirectInfo.push_back({ victimPair.first, thisPct }); + totalPct += thisPct; + ASSERT(totalPct <= 100); + if (totalPct == 100) + return; + } + } +} diff --git a/src/server/game/Combat/ThreatManager.h b/src/server/game/Combat/ThreatManager.h new file mode 100644 index 000000000..261679371 --- /dev/null +++ b/src/server/game/Combat/ThreatManager.h @@ -0,0 +1,329 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +/* + * Ported from TrinityCore with modifications for AzerothCore + * Original authors: + * Treeston - Combat/Threat system rewrite (PR #19930) + * Shauren + * Spp + */ + +#ifndef ACORE_THREATMANAGER_H +#define ACORE_THREATMANAGER_H + +#include "Common.h" +#include "IteratorPair.h" +#include "ObjectGuid.h" +#include "SharedDefines.h" +#include +#include +#include +#include +#include + +class Creature; +class Unit; +class SpellInfo; + +/********************************************************************************************************************************************************\ + * DEV DOCUMENTATION: THREAT SYSTEM * + * (future devs: please keep this up-to-date if you change the system) * + * The threat system works based on dynamically allocated threat list entries. * + * * + * Each such entry is a ThreatReference object, which is always stored in exactly three places: * + * - The threatened unit's (from now: reference "owner") sorted and unsorted threat lists * + * - The threatening unit's (from now: reference "victim") threatened-by-me list * + * A ThreatReference object carries the following implicit guarantees: * + * - Both owner and victim are valid units, which are currently in the world. Neither can be nullptr. * + * - There is an active combat reference between owner and victim. * + * * + * Note that (threat => combat) is a strong guarantee provided in conjunction with CombatManager. Thus: * + * - Adding threat will also create a combat reference between the units if one doesn't exist yet (even if the owner can't have a threat list!) * + * - Ending combat between two units will also delete any threat references that may exist between them. * + * * + * To manage a creature's threat list, ThreatManager maintains a heap of threat reference const pointers. * + * This heap is kept well-structured in all methods that modify ThreatReference, and is used to select the next target. * + * * + * Selection uses the following properties on ThreatReference, in order: * + * - Online state (one of ONLINE, SUPPRESSED, OFFLINE): * + * - ONLINE: Normal threat state, target is valid and attackable * + * - SUPPRESSED: Target is attackable, but inopportune. This is used for targets under immunity effects and damage-breaking CC. * + * Targets with SUPPRESSED threat can still be valid targets, but any target with ONLINE threat will be preferred. * + * - OFFLINE: The target is, for whatever reason, not valid at this time (for example, IMMUNE_TO_X flags or game master state). * + * These targets can never be selected, and GetCurrentVictim will return nullptr if all targets are OFFLINE (typically causing evade). * + * - Related methods: GetOnlineState, IsOnline, IsAvailable, IsOffline * + * - Taunt state (one of TAUNT, NONE, DETAUNT), the names speak for themselves * + * - Related methods: GetTauntState, IsTaunting, IsDetaunted * + * - Actual threat value (GetThreat) * + * * + * The current (= last selected) victim can be accessed using GetCurrentVictim. * + * Beyond that, ThreatManager has a variety of helpers and notifiers, which are documented inline below. * + * * + * SPECIAL NOTE: Please be aware that any iterator may be invalidated if you modify a ThreatReference. The heap holds const pointers for a reason, but * + * that doesn't mean you're scot free. A variety of actions (casting spells, teleporting units, and so forth) can cause changes to * + * the threat list. Use with care - or default to GetModifiableThreatList(), which inherently copies entries. * +\********************************************************************************************************************************************************/ + +class ThreatReference; +struct CompareThreatLessThan +{ + CompareThreatLessThan() {} + bool operator()(ThreatReference const* a, ThreatReference const* b) const; +}; + +class AC_GAME_API ThreatManager +{ +public: + class Heap; + class ThreatListIterator; + static const uint32 THREAT_UPDATE_INTERVAL = 1000u; + + static bool CanHaveThreatList(Unit const* who); + + ThreatManager(Unit* owner); + ~ThreatManager(); + + // called from ::Create methods just after construction (once all fields on owner have been populated) + void Initialize(); + // called from Creature::Update (only creatures can have their own threat list) + void Update(uint32 tdiff); + + // never nullptr + Unit* GetOwner() const { return _owner; } + // can our owner have a threat list? + bool CanHaveThreatList() const { return _ownerCanHaveThreatList; } + // returns the current victim - this can be nullptr if owner's threat list is empty, or has only offline targets + Unit* GetCurrentVictim(); + // const-friendly version that doesn't update, returns cached victim + Unit* GetLastVictim() const; + // returns an arbitrary non-offline victim from owner's threat list if one exists, nullptr otherwise + Unit* GetAnyTarget() const; + + // are there any entries in owner's threat list? + bool IsThreatListEmpty(bool includeOffline = false) const; + // is there a threat list entry on owner's threat list with victim == who? + bool IsThreatenedBy(ObjectGuid const& who, bool includeOffline = false) const; + // is there a threat list entry on owner's threat list with victim == who? + bool IsThreatenedBy(Unit const* who, bool includeOffline = false) const; + // returns ThreatReference amount if a ref exists, 0.0f otherwise + float GetThreat(Unit const* who, bool includeOffline = false) const; + size_t GetThreatListSize() const; + uint32 GetThreatListPlayerCount(bool includeOffline = false) const; + + // fastest of the three threat list getters - gets the threat list in "arbitrary" order + Acore::IteratorPair GetUnsortedThreatList() const; + // slightly slower than GetUnsorted, but sorted - only use it if you need the sorted property + Acore::IteratorPair GetSortedThreatList() const; + // slowest of the three threat list getters (by far), but lets you modify the threat references - this is also sorted + std::vector GetModifiableThreatList(); + + // does any unit have a threat list entry with victim == this.owner? + bool IsThreateningAnyone(bool includeOffline = false) const; + // is there a threat list entry on who's threat list for this.owner? + bool IsThreateningTo(ObjectGuid const& who, bool includeOffline = false) const; + // is there a threat list entry on who's threat list for this.owner? + bool IsThreateningTo(Unit const* who, bool includeOffline = false) const; + auto const& GetThreatenedByMeList() const { return _threatenedByMe; } + + // Notify the ThreatManager that its owner may now be suppressed on others' threat lists + void EvaluateSuppressed(bool canExpire = false); + + ///== AFFECT MY THREAT LIST == + void AddThreat(Unit* target, float amount, SpellInfo const* spell = nullptr, bool ignoreModifiers = false, bool ignoreRedirects = false); + void ScaleThreat(Unit* target, float factor); + // Modify target's threat by +percent% + void ModifyThreatByPercent(Unit* target, int32 percent) { if (percent) ScaleThreat(target, 0.01f * float(100 + percent)); } + // Resets the specified unit's threat to zero + void ResetThreat(Unit* target) { ScaleThreat(target, 0.0f); } + // Sets the specified unit's threat to be equal to the highest entry on the threat list + void MatchUnitThreatToHighestThreat(Unit* target); + // Notify the ThreatManager that we have a new taunt aura (or a taunt aura expired) + void TauntUpdate(); + // Sets all threat refs in owner's threat list to have zero threat + void ResetAllThreat(); + // Removes specified target from the threat list + void ClearThreat(Unit* target); + void ClearThreat(ThreatReference* ref); + // Removes all targets from the threat list (will cause evade in UpdateVictim if called) + void ClearAllThreat(); + + // Fixate on the passed target; this target will always be selected until the fixate is cleared + void FixateTarget(Unit* target); + void ClearFixate() { FixateTarget(nullptr); } + Unit* GetFixateTarget() const; + + ///== AFFECT OTHERS' THREAT LISTS == + // call AddThreat on everything that's threatened by us with the specified params + void ForwardThreatForAssistingMe(Unit* assistant, float baseAmount, SpellInfo const* spell = nullptr, bool ignoreModifiers = false); + // zeros this unit's threat on every creature's threat list where it appears (used for Feign Death, Sanctuary) + void ResetAllMyThreatOnOthers(); + // delete all ThreatReferences with victim == owner + void RemoveMeFromThreatLists(); + // re-calculates the temporary threat modifier from auras on myself + void UpdateMyTempModifiers(); + // re-calculate SPELL_AURA_MOD_THREAT modifiers + void UpdateMySpellSchoolModifiers(); + + // Test-only: directly set taunt state on a threat ref + // Uses uint32 to avoid incomplete-type issue with ThreatReference + void SetTauntStateForTesting(Unit* target, uint32 state); + + ///== REDIRECT SYSTEM == + void RegisterRedirectThreat(uint32 spellId, ObjectGuid const& victim, uint32 pct); + void UnregisterRedirectThreat(uint32 spellId); + void UnregisterRedirectThreat(uint32 spellId, ObjectGuid const& victim); + bool HasRedirects() const { return !_redirectRegistry.empty(); } + void ResetAllRedirects() { _redirectRegistry.clear(); UpdateRedirectInfo(); } + +private: + Unit* const _owner; + bool _ownerCanHaveThreatList; + + static const CompareThreatLessThan CompareThreat; + static bool CompareReferencesLT(ThreatReference const* a, ThreatReference const* b, float aWeight); + static float CalculateModifiedThreat(float threat, Unit const* victim, SpellInfo const* spell); + + void SendClearAllThreatToClients() const; + void SendRemoveToClients(Unit const* victim) const; + void SendThreatListToClients(bool newHighest) const; + + ///== MY THREAT LIST == + void PutThreatListRef(ObjectGuid const& guid, ThreatReference* ref); + void PurgeThreatListRef(ObjectGuid const& guid); + + bool _needClientUpdate; + uint32 _updateTimer; + std::unique_ptr _sortedThreatList; + std::unordered_map _myThreatListEntries; + + void ProcessAIUpdates(); + void RegisterForAIUpdate(ObjectGuid const& guid) { _needsAIUpdate.push_back(guid); } + std::vector _needsAIUpdate; + + void UpdateVictim(); + ThreatReference const* ReselectVictim(); + ThreatReference const* _currentVictimRef; + ThreatReference const* _fixateRef; + + ///== OTHERS' THREAT LISTS == + void PutThreatenedByMeRef(ObjectGuid const& guid, ThreatReference* ref); + void PurgeThreatenedByMeRef(ObjectGuid const& guid); + std::unordered_map _threatenedByMe; + std::array _singleSchoolModifiers; + mutable std::unordered_map::type, float> _multiSchoolModifiers; + + void UpdateRedirectInfo(); + std::vector> _redirectInfo; + std::unordered_map> _redirectRegistry; + +public: + ThreatManager(ThreatManager const&) = delete; + ThreatManager& operator=(ThreatManager const&) = delete; + + class ThreatListIterator + { + private: + std::function _generator; + ThreatReference const* _current; + + friend ThreatManager; + explicit ThreatListIterator(std::function&& generator) + : _generator(std::move(generator)), _current(_generator()) {} + + public: + ThreatReference const* operator*() const { return _current; } + ThreatReference const* operator->() const { return _current; } + ThreatListIterator& operator++() { _current = _generator(); return *this; } + bool operator==(ThreatListIterator const& o) const { return _current == o._current; } + bool operator!=(ThreatListIterator const& o) const { return _current != o._current; } + bool operator==(std::nullptr_t) const { return _current == nullptr; } + bool operator!=(std::nullptr_t) const { return _current != nullptr; } + }; + + friend class ThreatReference; + friend class ThreatReferenceImpl; + friend struct CompareThreatLessThan; + friend class debug_commandscript; +}; + +class AC_GAME_API ThreatReference +{ +public: + enum TauntState : uint32 { TAUNT_STATE_DETAUNT = 0, TAUNT_STATE_NONE = 1, TAUNT_STATE_TAUNT = 2 }; + enum OnlineState { ONLINE_STATE_ONLINE = 2, ONLINE_STATE_SUPPRESSED = 1, ONLINE_STATE_OFFLINE = 0 }; + + Creature* GetOwner() const { return _owner; } + Unit* GetVictim() const { return _victim; } + float GetThreat() const { return std::max(_baseAmount + static_cast(_tempModifier), 0.0f); } + OnlineState GetOnlineState() const { return _online; } + bool IsOnline() const { return (_online >= ONLINE_STATE_ONLINE); } + bool IsAvailable() const { return (_online > ONLINE_STATE_OFFLINE); } + bool IsSuppressed() const { return (_online == ONLINE_STATE_SUPPRESSED); } + bool IsOffline() const { return (_online <= ONLINE_STATE_OFFLINE); } + TauntState GetTauntState() const { return IsTaunting() ? TAUNT_STATE_TAUNT : _taunted; } + bool IsTaunting() const { return _taunted >= TAUNT_STATE_TAUNT; } + bool IsDetaunted() const { return _taunted == TAUNT_STATE_DETAUNT; } + + void AddThreat(float amount); + void ScaleThreat(float factor); + void ModifyThreatByPercent(int32 percent) { if (percent) ScaleThreat(0.01f * float(100 + percent)); } + void UpdateOffline(); + + void ClearThreat(); // dealloc's this + +protected: + static bool FlagsAllowFighting(Unit const* a, Unit const* b); + + explicit ThreatReference(ThreatManager* mgr, Unit* victim) : + _owner(reinterpret_cast(mgr->_owner)), _mgr(*mgr), _victim(victim), + _baseAmount(0.0f), _tempModifier(0), _taunted(TAUNT_STATE_NONE) + { + _online = ONLINE_STATE_OFFLINE; + } + + virtual ~ThreatReference() = default; + + void UnregisterAndFree(); + + bool ShouldBeOffline() const; + bool ShouldBeSuppressed() const; + void UpdateTauntState(TauntState state = TAUNT_STATE_NONE); + Creature* const _owner; + ThreatManager& _mgr; + void HeapNotifyIncreased(); + void HeapNotifyDecreased(); + Unit* const _victim; + OnlineState _online; + float _baseAmount; + int32 _tempModifier; + TauntState _taunted; + +public: + ThreatReference(ThreatReference const&) = delete; + ThreatReference& operator=(ThreatReference const&) = delete; + + friend class ThreatManager; + friend struct CompareThreatLessThan; +}; + +inline bool CompareThreatLessThan::operator()(ThreatReference const* a, ThreatReference const* b) const +{ + return ThreatManager::CompareReferencesLT(a, b, 1.0f); +} + +#endif diff --git a/src/server/game/Combat/ThreatMgr.cpp b/src/server/game/Combat/ThreatMgr.cpp deleted file mode 100644 index 432c0b396..000000000 --- a/src/server/game/Combat/ThreatMgr.cpp +++ /dev/null @@ -1,733 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * 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 2 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 . - */ - -#include "ThreatMgr.h" -#include "Creature.h" -#include "CreatureAI.h" -#include "Map.h" -#include "ObjectAccessor.h" -#include "Player.h" -#include "SpellInfo.h" -#include "SpellMgr.h" -#include "Unit.h" -#include "UnitEvents.h" - -//============================================================== -//================= ThreatCalcHelper =========================== -//============================================================== - -// The hatingUnit is not used yet -float ThreatCalcHelper::calcThreat(Unit* hatedUnit, float threat, SpellSchoolMask schoolMask, SpellInfo const* threatSpell) -{ - if (threatSpell) - { - if (SpellThreatEntry const* threatEntry = sSpellMgr->GetSpellThreatEntry(threatSpell->Id)) - if (threatEntry->pctMod != 1.0f) - threat *= threatEntry->pctMod; - - // Energize is not affected by Mods - for (uint8 i = 0; i < MAX_SPELL_EFFECTS; i++) - if (threatSpell->Effects[i].Effect == SPELL_EFFECT_ENERGIZE || threatSpell->Effects[i].ApplyAuraName == SPELL_AURA_PERIODIC_ENERGIZE) - return threat; - - if (Player* modOwner = hatedUnit->GetSpellModOwner()) - modOwner->ApplySpellMod(threatSpell->Id, SPELLMOD_THREAT, threat); - } - - return hatedUnit->ApplyTotalThreatModifier(threat, schoolMask); -} - -bool ThreatCalcHelper::isValidProcess(Unit* hatedUnit, Unit* hatingUnit, SpellInfo const* threatSpell) -{ - //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 || !hatingUnit) - return false; - - // not to self - if (hatedUnit == hatingUnit) - return false; - - // not to GM - if (hatedUnit->IsPlayer() && 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->InSamePhase(hatingUnit)) - return false; - - // spell not causing threat - if (threatSpell && threatSpell->HasAttribute(SPELL_ATTR1_NO_THREAT)) - return false; - - ASSERT(hatingUnit->IsCreature()); - - return true; -} - -//============================================================ -//================= HostileReference ========================== -//============================================================ - -HostileReference::HostileReference(Unit* refUnit, ThreatMgr* threatMgr, float threat) -{ - iThreat = threat; - iTempThreatModifier = 0.0f; - link(refUnit, threatMgr); - iUnitGuid = refUnit->GetGUID(); - iOnline = true; -} - -//============================================================ -// Tell our refTo (target) object that we have a link -void HostileReference::targetObjectBuildLink() -{ - getTarget()->addHatedBy(this); -} - -//============================================================ -// Tell our refTo (taget) object, that the link is cut -void HostileReference::targetObjectDestroyLink() -{ - getTarget()->removeHatedBy(this); -} - -//============================================================ -// Tell our refFrom (source) object, that the link is cut (Target destroyed) - -void HostileReference::sourceObjectDestroyLink() -{ - setOnlineOfflineState(false); -} - -//============================================================ -// Inform the source, that the status of the reference changed - -void HostileReference::fireStatusChanged(ThreatRefStatusChangeEvent& threatRefStatusChangeEvent) -{ - if (GetSource()) - GetSource()->processThreatEvent(&threatRefStatusChangeEvent); -} - -// -- compatibility layer for combat rewrite -Unit* HostileReference::GetOwner() const { return GetSource()->GetOwner(); } - -//============================================================ - -void HostileReference::AddThreat(float modThreat) -{ - 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(); - if (modThreat != 0.0f) - { - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_THREAT_CHANGE, this, modThreat); - fireStatusChanged(event); - } - - if (isValid() && modThreat >= 0.0f) - { - Unit* target = getTarget(); - if (target->GetEntry() != NPC_EYE_OF_KILROGG) // Excluded Eye of Kilrogg - { - Unit* victimOwner = target->GetCharmerOrOwner(); - if (victimOwner && victimOwner->IsAlive()) - { - GetSource()->AddThreat(victimOwner, 0.0f); // create a threat to the owner of a pet, if the pet attacks - } - } - } -} - -void HostileReference::addThreatPercent(int32 percent) -{ - // Xinef: Do not allow to modify threat by percent if threat is negative (forced to big value < 0 by spells adding temporary threat) - // Xinef: When the temporary effect ends, temporary threat is added back which results in huge additional amount of threat - if (iThreat <= 0) - return; - - float tmpThreat = iThreat; - AddPct(tmpThreat, percent); - AddThreat(tmpThreat - iThreat); -} - -//============================================================ -// check, if source can reach target and set the status - -void HostileReference::updateOnlineStatus() -{ - bool online = false; - - if (!isValid()) - if (Unit* target = ObjectAccessor::GetUnit(*GetSourceUnit(), getUnitGuid())) - 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()->IsPlayer() || !getTarget()->ToPlayer()->IsGameMaster()) - && !getTarget()->IsInFlight() - && getTarget()->IsInMap(GetSourceUnit()) - && getTarget()->InSamePhase(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 - } - } - - setOnlineOfflineState(online); -} - -//============================================================ -// set the status and fire the event on status change - -void HostileReference::setOnlineOfflineState(bool isOnline) -{ - if (iOnline != isOnline) - { - iOnline = isOnline; - - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_ONLINE_STATUS, this, isOnline); - fireStatusChanged(event); - } -} - -//============================================================ -// prepare the reference for deleting -// this is called be the target - -void HostileReference::removeReference() -{ - invalidate(); - - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_REMOVE_FROM_LIST, this, false); - fireStatusChanged(event); -} - -//============================================================ - -Unit* HostileReference::GetSourceUnit() -{ - return (GetSource()->GetOwner()); -} - -//============================================================ -//================ ThreatContainer =========================== -//============================================================ - -void ThreatContainer::clearReferences() -{ - for (ThreatContainer::StorageType::const_iterator i = iThreatList.begin(); i != iThreatList.end(); ++i) - { - (*i)->unlink(); - delete (*i); - } - - iThreatList.clear(); -} - -//============================================================ -// Return the HostileReference of nullptr, if not found -HostileReference* ThreatContainer::getReferenceByTarget(Unit const* victim) const -{ - if (!victim) - return nullptr; - - return getReferenceByTarget(victim->GetGUID()); -} - -HostileReference* ThreatContainer::getReferenceByTarget(ObjectGuid const& guid) const -{ - for (ThreatContainer::StorageType::const_iterator i = iThreatList.begin(); i != iThreatList.end(); ++i) - { - HostileReference* ref = (*i); - if (ref && ref->getUnitGuid() == guid) - { - return ref; - } - } - - return nullptr; -} - -//============================================================ -// Add the threat, if we find the reference - -HostileReference* ThreatContainer::AddThreat(Unit* victim, float threat) -{ - HostileReference* ref = getReferenceByTarget(victim); - if (ref) - ref->AddThreat(threat); - return ref; -} - -//============================================================ - -void ThreatContainer::ModifyThreatByPercent(Unit* victim, int32 percent) -{ - if (HostileReference* ref = getReferenceByTarget(victim)) - ref->addThreatPercent(percent); -} - -//============================================================ -// Check if the list is dirty and sort if necessary - -void ThreatContainer::update() -{ - if (iDirty && iThreatList.size() > 1) - iThreatList.sort(Acore::ThreatOrderPred()); - - iDirty = false; -} - -//============================================================ -// return the next best victim -// could be the current victim - -HostileReference* ThreatContainer::SelectNextVictim(Creature* attacker, HostileReference* currentVictim) const -{ - // pussywizard: pretty much remade this whole function - - HostileReference* currentRef = nullptr; - bool found = false; - bool noPriorityTargetFound = false; - - // pussywizard: currentVictim is needed to compare if threat was exceeded by 10%/30% for melee/range targets (only then switching current target) - if (currentVictim) - { - Unit* cvUnit = currentVictim->getTarget(); - if (!attacker->CanCreatureAttack(cvUnit)) // pussywizard: if currentVictim is not valid => don't compare the threat with it, just take the highest threat valid target - currentVictim = nullptr; - else if (!IsPreferredTarget(attacker, cvUnit)) // pussywizard: no 10%/30% if currentVictim is immune to damage or has auras breakable by damage - currentVictim = nullptr; - } - - ThreatContainer::StorageType::const_iterator lastRef = iThreatList.end(); - --lastRef; - - // pussywizard: iterate from highest to lowest threat - for (ThreatContainer::StorageType::const_iterator iter = iThreatList.begin(); iter != iThreatList.end() && !found;) - { - currentRef = (*iter); - - Unit* target = currentRef->getTarget(); - ASSERT(target); // if the ref has status online the target must be there ! - - // pussywizard: don't go to threat comparison if this ref is immune to damage or has aura breakable on damage (second choice target) - // pussywizard: if this is the last entry on the threat list, then all targets are second choice, set bool to true and loop threat list again, ignoring this section - if (!noPriorityTargetFound && !IsPreferredTarget(attacker, target)) - { - if (iter != lastRef) - { - ++iter; - continue; - } - else - { - noPriorityTargetFound = true; - iter = iThreatList.begin(); - continue; - } - } - - // pussywizard: skip not valid targets - if (attacker->CanCreatureAttack(target)) - { - if (currentVictim) // pussywizard: if not nullptr then target must have 10%/30% more threat - { - if (currentVictim == currentRef) // pussywizard: nothing found previously was good and enough, currentRef passed all necessary tests, so end now - { - found = true; - break; - } - - // pussywizard: implement 110% threat rule for targets in melee range and 130% rule for targets in ranged distances - if (currentRef->GetThreat() > 1.3f * currentVictim->GetThreat()) // pussywizard: enough in all cases, end - { - found = true; - break; - } - else if (currentRef->GetThreat() > 1.1f * currentVictim->GetThreat()) // pussywizard: enought only if target in melee range - { - if (attacker->IsWithinMeleeRange(target)) - { - found = true; - break; - } - } - else // pussywizard: nothing found previously was good and enough, this and next entries on the list have less than 110% threat, and currentVictim is present and valid as checked before the loop (otherwise it's nullptr), so end now - { - currentRef = currentVictim; - found = true; - break; - } - } - else // pussywizard: no currentVictim, first passing all checks is chosen (highest threat, list is sorted) - { - // tie-breaker if multiple targets have the same threat: select closest target - currentRef = SelectNextVictimTieBreaker(attacker, iter, noPriorityTargetFound); - found = true; - break; - } - } - ++iter; - } - if (!found) - currentRef = nullptr; - - return currentRef; -} - -// Helper for Tie-breakers -HostileReference* ThreatContainer::SelectNextVictimTieBreaker(Creature* attacker, ThreatContainer::StorageType::const_iterator currentIter, bool noPriorityTargetFound) const -{ - HostileReference* bestRef = *currentIter; - float bestThreat = bestRef->GetThreat(); - float shortestDistSq = attacker->GetExactDistSq(bestRef->getTarget()); - - auto tieIter = std::next(currentIter); - - while (tieIter != iThreatList.end()) - { - HostileReference* nextRef = *tieIter; - - if (!nextRef) - { - ++tieIter; - continue; - } - - // Threatlist is sorted, so we can stop as soon as we find a lower threat - if (bestThreat - nextRef->GetThreat() > 0.01f) - break; - - Unit* target = nextRef->getTarget(); - - if (!target) - { - ++tieIter; - continue; - } - - if (attacker->CanCreatureAttack(target) && (noPriorityTargetFound || IsPreferredTarget(attacker, target))) - { - float distSq = attacker->GetExactDistSq(target); - if (distSq < shortestDistSq) - { - bestRef = nextRef; - shortestDistSq = distSq; - } - } - ++tieIter; - } - return bestRef; -} - -// Helper for checking if a target is preferred (not immune, not confused, etc) -bool ThreatContainer::IsPreferredTarget(Creature* attacker, Unit* target) const -{ - if (target->IsImmunedToDamageOrSchool(attacker->GetMeleeDamageSchoolMask())) - return false; - - if (target->HasNegativeAuraWithInterruptFlag(AURA_INTERRUPT_FLAG_TAKE_DAMAGE)) - return false; - - if (target->HasUnitState(UNIT_STATE_CONFUSED)) - return false; - - if (target->HasAuraTypeWithCaster(SPELL_AURA_IGNORED, attacker->GetGUID())) - return false; - - if (attacker->HasUnitState(UNIT_STATE_ROOT) && attacker->IsCombatMovementAllowed() && !attacker->IsWithinMeleeRange(target)) - return false; - - return true; -} - -//============================================================ -//=================== ThreatMgr ========================== -//============================================================ - -ThreatMgr::ThreatMgr(Unit* owner) : iCurrentVictim(nullptr), iOwner(owner), iUpdateTimer(THREAT_UPDATE_INTERVAL) -{ -} - -void ThreatMgr::ClearAllThreat() -{ - if (iOwner->CanHaveThreatList(true) && !isThreatListEmpty()) - iOwner->SendClearThreatListOpcode(); - clearReferences(); -} - -//============================================================ - -void ThreatMgr::clearReferences() -{ - iThreatContainer.clearReferences(); - iThreatOfflineContainer.clearReferences(); - iCurrentVictim = nullptr; - iUpdateTimer = THREAT_UPDATE_INTERVAL; -} - -//============================================================ - -void ThreatMgr::AddThreat(Unit* victim, float threat, SpellSchoolMask schoolMask, SpellInfo const* threatSpell) -{ - if (!ThreatCalcHelper::isValidProcess(victim, iOwner, threatSpell)) - return; - - threat = ThreatCalcHelper::calcThreat(victim, threat, schoolMask, threatSpell); - if (Creature* hatingCreature = iOwner->ToCreature()) - { - if (hatingCreature->IsAIEnabled) - { - hatingCreature->AI()->CalculateThreat(victim, threat, threatSpell); - } - } - - DoAddThreat(victim, threat); -} - -void ThreatMgr::DoAddThreat(Unit* victim, float threat) -{ - uint32 redirectThreadPct = victim->GetRedirectThreatPercent(); - Unit* redirectTarget = victim->GetRedirectThreatTarget(); - - // Personal Spawns from same summoner can aggro each other - if (TempSummon* tempSummonVictim = victim->ToTempSummon()) - { - if (tempSummonVictim->IsVisibleBySummonerOnly()) - { - if (!GetOwner()->ToTempSummon() || - !GetOwner()->ToTempSummon()->IsVisibleBySummonerOnly() || - tempSummonVictim->GetSummonerGUID() != GetOwner()->ToTempSummon()->GetSummonerGUID()) - { - redirectThreadPct = 100; - redirectTarget = tempSummonVictim->GetSummonerUnit(); - } - } - } - - // must check > 0.0f, otherwise dead loop - if (threat > 0.0f && redirectThreadPct) - { - if (redirectTarget) - { - float redirectThreat = CalculatePct(threat, redirectThreadPct); - threat -= redirectThreat; - if (ThreatCalcHelper::isValidProcess(redirectTarget, GetOwner())) - _addThreat(redirectTarget, redirectThreat); - } - } - - _addThreat(victim, threat); -} - -void ThreatMgr::_addThreat(Unit* victim, float threat) -{ - HostileReference* ref = iThreatContainer.AddThreat(victim, threat); - // Ref is not in the online refs, search the offline refs next - if (!ref) - ref = iThreatOfflineContainer.AddThreat(victim, threat); - - if (!ref) // there was no ref => create a new one - { - // threat has to be 0 here - HostileReference* hostileRef = new HostileReference(victim, this, 0); - iThreatContainer.addReference(hostileRef); - hostileRef->AddThreat(threat); // now we add the real threat - if (victim->IsPlayer() && victim->ToPlayer()->IsGameMaster()) - hostileRef->setOnlineOfflineState(false); // GM is always offline - } -} - -//============================================================ - -void ThreatMgr::ModifyThreatByPercent(Unit* victim, int32 percent) -{ - iThreatContainer.ModifyThreatByPercent(victim, percent); -} - -//============================================================ - -Unit* ThreatMgr::getHostileTarget() -{ - iThreatContainer.update(); - HostileReference* nextVictim = iThreatContainer.SelectNextVictim(GetOwner()->ToCreature(), getCurrentVictim()); - setCurrentVictim(nextVictim); - return getCurrentVictim() != nullptr ? getCurrentVictim()->getTarget() : nullptr; -} - -//============================================================ - -float ThreatMgr::GetThreat(Unit* victim, bool alsoSearchOfflineList) -{ - float threat = 0.0f; - HostileReference* ref = iThreatContainer.getReferenceByTarget(victim); - if (!ref && alsoSearchOfflineList) - ref = iThreatOfflineContainer.getReferenceByTarget(victim); - if (ref) - threat = ref->GetThreat(); - return threat; -} - -//============================================================ - -float ThreatMgr::getThreatWithoutTemp(Unit* victim, bool alsoSearchOfflineList) -{ - float threat = 0.0f; - HostileReference* ref = iThreatContainer.getReferenceByTarget(victim); - if (!ref && alsoSearchOfflineList) - ref = iThreatOfflineContainer.getReferenceByTarget(victim); - if (ref) - threat = ref->GetThreat() - ref->getTempThreatModifier(); - return threat; -} - -//============================================================ - -void ThreatMgr::tauntApply(Unit* taunter) -{ - HostileReference* ref = iThreatContainer.getReferenceByTarget(taunter); - if (getCurrentVictim() && ref && (ref->GetThreat() < getCurrentVictim()->GetThreat())) - { - if (ref->getTempThreatModifier() == 0.0f) // Ok, temp threat is unused - ref->setTempThreat(getCurrentVictim()->GetThreat()); - } -} - -//============================================================ - -void ThreatMgr::tauntFadeOut(Unit* taunter) -{ - HostileReference* ref = iThreatContainer.getReferenceByTarget(taunter); - if (ref) - ref->resetTempThreat(); -} - -//============================================================ - -void ThreatMgr::setCurrentVictim(HostileReference* pHostileReference) -{ - if (pHostileReference && pHostileReference != iCurrentVictim) - { - iOwner->SendChangeCurrentVictimOpcode(pHostileReference); - } - iCurrentVictim = pHostileReference; -} - -//============================================================ -// The hated unit is gone, dead or deleted -// return true, if the event is consumed - -void ThreatMgr::processThreatEvent(ThreatRefStatusChangeEvent* threatRefStatusChangeEvent) -{ - threatRefStatusChangeEvent->setThreatMgr(this); // now we can set the threat manager - - HostileReference* hostileRef = threatRefStatusChangeEvent->getReference(); - - switch (threatRefStatusChangeEvent->getType()) - { - case UEV_THREAT_REF_THREAT_CHANGE: - if ((getCurrentVictim() == hostileRef && threatRefStatusChangeEvent->getFValue() < 0.0f) || - (getCurrentVictim() != hostileRef && threatRefStatusChangeEvent->getFValue() > 0.0f)) - setDirty(true); // the order in the threat list might have changed - break; - case UEV_THREAT_REF_ONLINE_STATUS: - if (!hostileRef->IsOnline()) - { - if (hostileRef == getCurrentVictim()) - { - setCurrentVictim(nullptr); - setDirty(true); - } - if (GetOwner() && GetOwner()->IsInWorld()) - if (Unit* target = ObjectAccessor::GetUnit(*GetOwner(), hostileRef->getUnitGuid())) - if (GetOwner()->IsInMap(target)) - GetOwner()->SendRemoveFromThreatListOpcode(hostileRef); - iThreatContainer.remove(hostileRef); - iThreatOfflineContainer.addReference(hostileRef); - } - else - { - if (getCurrentVictim() && hostileRef->GetThreat() > (1.1f * getCurrentVictim()->GetThreat())) - setDirty(true); - iThreatContainer.addReference(hostileRef); - iThreatOfflineContainer.remove(hostileRef); - } - break; - case UEV_THREAT_REF_REMOVE_FROM_LIST: - if (hostileRef == getCurrentVictim()) - { - setCurrentVictim(nullptr); - setDirty(true); - } - iOwner->SendRemoveFromThreatListOpcode(hostileRef); - if (hostileRef->IsOnline()) - iThreatContainer.remove(hostileRef); - else - iThreatOfflineContainer.remove(hostileRef); - break; - } -} - -bool ThreatMgr::isNeedUpdateToClient(uint32 time) -{ - if (isThreatListEmpty()) - return false; - - if (time >= iUpdateTimer) - { - iUpdateTimer = THREAT_UPDATE_INTERVAL; - return true; - } - iUpdateTimer -= time; - return false; -} - -// Reset all aggro without modifying the threatlist. -void ThreatMgr::ResetAllThreat() -{ - ThreatContainer::StorageType& threatList = iThreatContainer.iThreatList; - if (threatList.empty()) - return; - - for (HostileReference* ref : threatList) - { - // Reset temp threat before setting threat back to 0. - ref->resetTempThreat(); - ref->SetThreat(0.f); - } - - setDirty(true); -} diff --git a/src/server/game/Combat/ThreatMgr.h b/src/server/game/Combat/ThreatMgr.h deleted file mode 100644 index 105c405a9..000000000 --- a/src/server/game/Combat/ThreatMgr.h +++ /dev/null @@ -1,335 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * 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 2 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 . - */ - -#ifndef _THREATMANAGER -#define _THREATMANAGER - -#include "IteratorPair.h" -#include "ObjectGuid.h" -#include "Reference.h" -#include "SharedDefines.h" -#include "UnitEvents.h" -#include - -//============================================================== - -class Unit; -class Creature; -class ThreatMgr; -class SpellInfo; - -#define THREAT_UPDATE_INTERVAL 2 * IN_MILLISECONDS // Server should send threat update to client periodically each second - -//============================================================== -// Class to calculate the real threat based - -struct ThreatCalcHelper -{ - static float calcThreat(Unit* hatedUnit, float threat, SpellSchoolMask schoolMask = SPELL_SCHOOL_MASK_NORMAL, SpellInfo const* threatSpell = nullptr); - static bool isValidProcess(Unit* hatedUnit, Unit* hatingUnit, SpellInfo const* threatSpell = nullptr); -}; - -//============================================================== -class HostileReference : public Reference -{ -public: - HostileReference(Unit* refUnit, ThreatMgr* threatMgr, float threat); - - Unit* GetOwner() const; - Unit* GetVictim() const { return getTarget(); } - - //================================================= - void AddThreat(float modThreat); - - void SetThreat(float threat) { AddThreat(threat - GetThreat()); } - - void addThreatPercent(int32 percent); - - [[nodiscard]] float GetThreat() const { return iThreat; } - - void ClearThreat() { removeReference(); } - - [[nodiscard]] bool IsOnline() const { return iOnline; } - [[nodiscard]] bool IsAvailable() const { return iOnline; } // unused for now - [[nodiscard]] bool IsOffline() const { return !iOnline; } // unused for now - - // used for temporary setting a threat and reducting it later again. - // the threat modification is stored - void setTempThreat(float threat) - { - addTempThreat(threat - GetThreat()); - } - - void addTempThreat(float threat) - { - iTempThreatModifier = threat; - if (iTempThreatModifier != 0.0f) - AddThreat(iTempThreatModifier); - } - - void resetTempThreat() - { - if (iTempThreatModifier != 0.0f) - { - AddThreat(-iTempThreatModifier); - iTempThreatModifier = 0.0f; - } - } - - float getTempThreatModifier() { return iTempThreatModifier; } - - //================================================= - // check, if source can reach target and set the status - void updateOnlineStatus(); - - void setOnlineOfflineState(bool isOnline); - //================================================= - - bool operator == (const HostileReference& hostileRef) const { return hostileRef.getUnitGuid() == getUnitGuid(); } - - //================================================= - - [[nodiscard]] ObjectGuid getUnitGuid() const { return iUnitGuid; } - - //================================================= - // reference is not needed anymore. realy delete it ! - - void removeReference(); - - //================================================= - - HostileReference* next() { return ((HostileReference*) Reference::next()); } - - //================================================= - - // Tell our refTo (target) object that we have a link - void targetObjectBuildLink() override; - - // Tell our refTo (taget) object, that the link is cut - void targetObjectDestroyLink() override; - - // Tell our refFrom (source) object, that the link is cut (Target destroyed) - void sourceObjectDestroyLink() override; -private: - // Inform the source, that the status of that reference was changed - void fireStatusChanged(ThreatRefStatusChangeEvent& threatRefStatusChangeEvent); - - Unit* GetSourceUnit(); -private: - float iThreat; - float iTempThreatModifier; // used for taunt - ObjectGuid iUnitGuid; - bool iOnline; -}; - -//============================================================== -class ThreatMgr; - -class ThreatContainer -{ - friend class ThreatMgr; - -public: - typedef std::list StorageType; - - ThreatContainer() = default; - - ~ThreatContainer() { clearReferences(); } - - HostileReference* AddThreat(Unit* victim, float threat); - - void ModifyThreatByPercent(Unit* victim, int32 percent); - - HostileReference* SelectNextVictim(Creature* attacker, HostileReference* currentVictim) const; - - HostileReference* SelectNextVictimTieBreaker(Creature* attacker, ThreatContainer::StorageType::const_iterator currentIter, bool noPriorityTargetFound) const; - - bool IsPreferredTarget(Creature* attacker, Unit* target) const; - - void setDirty(bool isDirty) { iDirty = isDirty; } - - [[nodiscard]] bool isDirty() const { return iDirty; } - - [[nodiscard]] bool empty() const - { - return iThreatList.empty(); - } - - [[nodiscard]] HostileReference* getMostHated() const - { - return iThreatList.empty() ? nullptr : iThreatList.front(); - } - - HostileReference* getReferenceByTarget(Unit const* victim) const; - HostileReference* getReferenceByTarget(ObjectGuid const& guid) const; - - [[nodiscard]] StorageType const& GetThreatList() const { return iThreatList; } - -private: - void remove(HostileReference* hostileRef) - { - iThreatList.remove(hostileRef); - } - - void addReference(HostileReference* hostileRef) - { - iThreatList.push_back(hostileRef); - } - - void clearReferences(); - - // Sort the list if necessary - void update(); - - StorageType iThreatList; - bool iDirty{false}; -}; - -//================================================= - -typedef HostileReference ThreatReference; - -class ThreatMgr -{ -public: - friend class HostileReference; - - explicit ThreatMgr(Unit* owner); - - ~ThreatMgr() { clearReferences(); } - - Unit* SelectVictim() { return getHostileTarget(); } - Unit* GetCurrentVictim() const { if (ThreatReference* ref = getCurrentVictim()) return ref->GetVictim(); else return nullptr; } - Unit* GetAnyTarget() const { auto const& list = GetThreatList(); if (!list.empty()) return list.front()->getTarget(); return nullptr; } - - void clearReferences(); - - void AddThreat(Unit* victim, float threat, SpellSchoolMask schoolMask = SPELL_SCHOOL_MASK_NORMAL, SpellInfo const* threatSpell = nullptr); - void DoAddThreat(Unit* victim, float threat); - void ModifyThreatByPercent(Unit* victim, int32 percent); - float GetThreat(Unit* victim, bool alsoSearchOfflineList = false); - float GetThreatListSize() const { return GetThreatList().size(); } - float getThreatWithoutTemp(Unit* victim, bool alsoSearchOfflineList = false); - - [[nodiscard]] bool isThreatListEmpty() const { return iThreatContainer.empty(); } - [[nodiscard]] bool areThreatListsEmpty() const { return iThreatContainer.empty() && iThreatOfflineContainer.empty(); } - - Acore::IteratorPair::const_iterator> GetSortedThreatList() const { auto& list = iThreatContainer.GetThreatList(); return { list.cbegin(), list.cend() }; } - Acore::IteratorPair::const_iterator> GetUnsortedThreatList() const { return GetSortedThreatList(); } - - void processThreatEvent(ThreatRefStatusChangeEvent* threatRefStatusChangeEvent); - - bool isNeedUpdateToClient(uint32 time); - - [[nodiscard]] HostileReference* getCurrentVictim() const { return iCurrentVictim; } - - [[nodiscard]] Unit* GetOwner() const { return iOwner; } - - Unit* getHostileTarget(); - - void tauntApply(Unit* taunter); - void tauntFadeOut(Unit* taunter); - - void setCurrentVictim(HostileReference* hostileRef); - - void setDirty(bool isDirty) { iThreatContainer.setDirty(isDirty); } - - // Reset all aggro without modifying the threadlist. - void ResetThreat(Unit const* who) { if (auto* ref = FindReference(who, true)) ref->SetThreat(0.0f); } - void ResetAllThreat(); - - void ClearThreat(Unit const* who) { if (auto* ref = FindReference(who, true)) ref->removeReference(); } - void ClearAllThreat(); - - // Reset all aggro of unit in threadlist satisfying the predicate. - template void resetAggro(PREDICATE predicate) - { - ThreatContainer::StorageType& threatList = iThreatContainer.iThreatList; - if (threatList.empty()) - return; - - for (auto& ref : threatList) - { - if (predicate(ref->getTarget())) - { - ref->SetThreat(0); - setDirty(true); - } - } - } - - // methods to access the lists from the outside to do some dirty manipulation (scriping and such) - // I hope they are used as little as possible. - [[nodiscard]] ThreatContainer::StorageType const& GetThreatList() const { return iThreatContainer.GetThreatList(); } - [[nodiscard]] ThreatContainer::StorageType const& GetOfflineThreatList() const { return iThreatOfflineContainer.GetThreatList(); } - ThreatContainer& GetOnlineContainer() { return iThreatContainer; } - ThreatContainer& GetOfflineContainer() { return iThreatOfflineContainer; } - -private: - HostileReference* FindReference(Unit const* who, bool includeOffline) const { if (auto* ref = iThreatContainer.getReferenceByTarget(who)) return ref; if (includeOffline) if (auto* ref = iThreatOfflineContainer.getReferenceByTarget(who)) return ref; return nullptr; } - - void _addThreat(Unit* victim, float threat); - - HostileReference* iCurrentVictim; - Unit* iOwner; - uint32 iUpdateTimer; - ThreatContainer iThreatContainer; - ThreatContainer iThreatOfflineContainer; -}; - -//================================================= - -struct RedirectThreatInfo -{ - RedirectThreatInfo() = default; - ObjectGuid _targetGUID; - uint32 _threatPct{ 0 }; - - [[nodiscard]] ObjectGuid GetTargetGUID() const { return _targetGUID; } - [[nodiscard]] uint32 GetThreatPct() const { return _threatPct; } - - void Set(ObjectGuid guid, uint32 pct) - { - _targetGUID = guid; - _threatPct = pct; - } - - void ModifyThreatPct(int32 amount) - { - amount += _threatPct; - _threatPct = uint32(std::max(0, amount)); - } -}; - -//================================================= - -namespace Acore -{ - // Binary predicate for sorting HostileReferences based on threat value - class ThreatOrderPred - { - public: - ThreatOrderPred(bool ascending = false) : m_ascending(ascending) {} - bool operator() (HostileReference const* a, HostileReference const* b) const - { - return m_ascending ? a->GetThreat() < b->GetThreat() : a->GetThreat() > b->GetThreat(); - } - private: - const bool m_ascending; - }; -} -#endif diff --git a/src/server/game/Combat/UnitEvents.h b/src/server/game/Combat/UnitEvents.h deleted file mode 100644 index da2c1a965..000000000 --- a/src/server/game/Combat/UnitEvents.h +++ /dev/null @@ -1,134 +0,0 @@ -/* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * 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 2 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 . - */ - -#ifndef _UNITEVENTS -#define _UNITEVENTS - -#include "Common.h" - -class ThreatContainer; -class ThreatMgr; -class HostileReference; - -//============================================================== -//============================================================== - -enum UNIT_EVENT_TYPE -{ - // Player/Pet changed on/offline status - UEV_THREAT_REF_ONLINE_STATUS = 1 << 0, - - // Threat for Player/Pet changed - UEV_THREAT_REF_THREAT_CHANGE = 1 << 1, - - // Player/Pet will be removed from list (dead) [for internal use] - UEV_THREAT_REF_REMOVE_FROM_LIST = 1 << 2, - - // Player/Pet entered/left water or some other place where it is/was not accessible for the creature - UEV_THREAT_REF_ASSECCIBLE_STATUS = 1 << 3, - - // Threat list is going to be sorted (if dirty flag is set) - UEV_THREAT_SORT_LIST = 1 << 4, - - // New target should be fetched, could tbe the current target as well - UEV_THREAT_SET_NEXT_TARGET = 1 << 5, - - // A new victim (target) was set. Could be nullptr - UEV_THREAT_VICTIM_CHANGED = 1 << 6, - - // Future use - //UEV_UNIT_KILLED = 1<<7, - - //Future use - //UEV_UNIT_HEALTH_CHANGE = 1<<8, -}; - -#define UEV_THREAT_REF_EVENT_MASK (UEV_THREAT_REF_ONLINE_STATUS | UEV_THREAT_REF_THREAT_CHANGE | UEV_THREAT_REF_REMOVE_FROM_LIST | UEV_THREAT_REF_ASSECCIBLE_STATUS) -#define UEV_THREAT_MANAGER_EVENT_MASK (UEV_THREAT_SORT_LIST | UEV_THREAT_SET_NEXT_TARGET | UEV_THREAT_VICTIM_CHANGED) -#define UEV_ALL_EVENT_MASK (0xffffffff) - -// Future use -//#define UEV_UNIT_EVENT_MASK (UEV_UNIT_KILLED | UEV_UNIT_HEALTH_CHANGE) - -//============================================================== - -class UnitBaseEvent -{ -private: - uint32 iType; -public: - UnitBaseEvent(uint32 pType) { iType = pType; } - [[nodiscard]] uint32 getType() const { return iType; } - [[nodiscard]] bool matchesTypeMask(uint32 pMask) const { return iType & pMask; } - - void setType(uint32 pType) { iType = pType; } -}; - -//============================================================== - -class ThreatRefStatusChangeEvent : public UnitBaseEvent -{ -private: - HostileReference* iHostileReference; - union - { - float iFValue; - int32 iIValue; - bool iBValue; - }; - ThreatMgr* iThreatMgr; -public: - ThreatRefStatusChangeEvent(uint32 pType) : UnitBaseEvent(pType), iThreatMgr(nullptr) { iHostileReference = nullptr; } - - ThreatRefStatusChangeEvent(uint32 pType, HostileReference* pHostileReference) : UnitBaseEvent(pType), iThreatMgr(nullptr) { iHostileReference = pHostileReference; } - - ThreatRefStatusChangeEvent(uint32 pType, HostileReference* pHostileReference, float pValue) : UnitBaseEvent(pType), iThreatMgr(nullptr) { iHostileReference = pHostileReference; iFValue = pValue; } - - ThreatRefStatusChangeEvent(uint32 pType, HostileReference* pHostileReference, bool pValue) : UnitBaseEvent(pType), iThreatMgr(nullptr) { iHostileReference = pHostileReference; iBValue = pValue; } - - [[nodiscard]] int32 getIValue() const { return iIValue; } - - [[nodiscard]] float getFValue() const { return iFValue; } - - [[nodiscard]] bool getBValue() const { return iBValue; } - - void setBValue(bool pValue) { iBValue = pValue; } - - [[nodiscard]] HostileReference* getReference() const { return iHostileReference; } - - void setThreatMgr(ThreatMgr* pThreatMgr) { iThreatMgr = pThreatMgr; } - - [[nodiscard]] ThreatMgr* GetThreatMgr() const { return iThreatMgr; } -}; - -//============================================================== - -class ThreatMgrEvent : public ThreatRefStatusChangeEvent -{ -private: - ThreatContainer* iThreatContainer; -public: - ThreatMgrEvent(uint32 pType) : ThreatRefStatusChangeEvent(pType), iThreatContainer(nullptr) {} - ThreatMgrEvent(uint32 pType, HostileReference* pHostileReference) : ThreatRefStatusChangeEvent(pType, pHostileReference), iThreatContainer(nullptr) {} - - void setThreatContainer(ThreatContainer* pThreatContainer) { iThreatContainer = pThreatContainer; } - - [[nodiscard]] ThreatContainer* getThreatContainer() const { return iThreatContainer; } -}; - -//============================================================== -#endif diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 90352ae04..a670abd8b 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -221,16 +221,12 @@ bool AssistDelayEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/) if (assistant && assistant->CanAssistTo(m_owner, victim)) { assistant->SetNoCallAssistance(true); - assistant->CombatStart(victim); - if (assistant->IsAIEnabled) - { - assistant->AI()->AttackStart(victim); + assistant->EngageWithTarget(victim); - // When nearby mobs aggro from another mob's initial call for assistance - // their leash timers become linked and attacking one will keep the rest from evading. - if (assistant->GetVictim()) - assistant->SetLastLeashExtensionTimePtr(m_owner->GetLastLeashExtensionTimePtr()); - } + // When nearby mobs aggro from another mob's initial call for assistance + // their leash timers become linked and attacking one will keep the rest from evading. + if (assistant->GetVictim()) + assistant->SetLastLeashExtensionTimePtr(m_owner->GetLastLeashExtensionTimePtr()); } } } @@ -267,7 +263,7 @@ Creature::Creature(): Unit(), MovableMapObject(), m_groupLootTimer(0), lootingGr m_transportCheckTimer(1000), lootPickPocketRestoreTime(0), m_combatPulseTime(0), m_combatPulseDelay(0), m_reactState(REACT_AGGRESSIVE), m_defaultMovementType(IDLE_MOTION_TYPE), m_spawnId(0), m_equipmentId(0), m_originalEquipmentId(0), m_alreadyCallForHelp(false), m_AlreadyCallAssistance(false), m_AlreadySearchedAssistance(false), m_regenHealth(true), m_regenPower(true), m_AI_locked(false), m_meleeDamageSchoolMask(SPELL_SCHOOL_MASK_NORMAL), m_originalEntry(0), _gossipMenuId(0), m_moveInLineOfSightDisabled(false), m_moveInLineOfSightStrictlyDisabled(false), - m_homePosition(), m_transportHomePosition(), m_creatureInfo(nullptr), m_creatureData(nullptr), m_detectionDistance(20.0f),_sparringPct(0.0f), m_waypointID(0), m_path_id(0), m_formation(nullptr), m_lastLeashExtensionTime(nullptr), m_cannotReachTimer(0), + m_homePosition(), m_transportHomePosition(), m_creatureInfo(nullptr), m_creatureData(nullptr), m_detectionDistance(20.0f),_sparringPct(0.0f), m_waypointID(0), m_path_id(0), m_formation(nullptr), m_lastLeashExtensionTime(nullptr), _isMissingSwimmingFlagOutOfCombat(false), m_assistanceTimer(0), _playerDamageReq(0), _damagedByPlayer(false), _isCombatMovementAllowed(true) { m_regenTimer = CREATURE_REGEN_INTERVAL; @@ -529,6 +525,8 @@ bool Creature::InitEntry(uint32 Entry, const CreatureData* data) for (uint8 i = 0; i < MAX_CREATURE_SPELLS; ++i) m_spells[i] = GetCreatureTemplate()->spells[i]; + GetThreatMgr().Initialize(); + return true; } @@ -722,6 +720,8 @@ void Creature::Update(uint32 diff) if (!IsAlive()) break; + GetThreatMgr().Update(diff); + // if creature is charmed, switch to charmed AI if (NeedChangeAI) { @@ -865,45 +865,7 @@ void Creature::Update(uint32 diff) m_regenTimer += CREATURE_REGEN_INTERVAL; } - if (CanNotReachTarget() && !IsInEvadeMode()) - { - m_cannotReachTimer += diff; - if (m_cannotReachTimer >= (sWorld->getIntConfig(CONFIG_NPC_EVADE_IF_NOT_REACHABLE) * IN_MILLISECONDS)) - { - Player* cannotReachPlayer = ObjectAccessor::GetPlayer(*this, m_cannotReachTarget); - if (cannotReachPlayer && IsEngagedBy(cannotReachPlayer) && IsAIEnabled && AI()->OnTeleportUnreacheablePlayer(cannotReachPlayer)) - { - SetCannotReachTarget(); - } - else if (!GetMap()->IsRaid()) - { - auto EnterEvade = [&]() - { - if (CreatureAI* ai = AI()) - { - ai->EnterEvadeMode(CreatureAI::EvadeReason::EVADE_REASON_NO_PATH); - } - }; - - if (GetThreatMgr().GetThreatListSize() <= 1) - { - EnterEvade(); - } - else - { - if (HostileReference* ref = GetThreatMgr().GetOnlineContainer().getReferenceByTarget(m_cannotReachTarget)) - { - ref->removeReference(); - SetCannotReachTarget(); - } - else - { - EnterEvade(); - } - } - } - } - } + // Evade timer is now handled by CombatManager::Update() which calls UnitAI::EvadeTimerExpired() break; } default: @@ -2792,6 +2754,80 @@ void Creature::SetInCombatWithZone() AI()->DoZoneInCombat(); } +void Creature::AtEngage(Unit* target) +{ + Unit::AtEngage(target); + + // If we somehow engage in combat with a player while immune, remove immunity so they can fight back + if (target && IsImmuneToPC() && target->GetCharmerOrOwnerPlayerOrPlayerItself()) + SetImmuneToPC(false); + + if (!IsStandState()) + SetStandState(UNIT_STAND_STATE_STAND); + + if (!(GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_ALLOW_MOUNTED_COMBAT)) + Dismount(); + + RefreshSwimmingFlag(); + + if (IsPet() || IsGuardian()) // update pets' speed for catchup OOC speed + { + UpdateSpeed(MOVE_RUN, true); + UpdateSpeed(MOVE_SWIM, true); + UpdateSpeed(MOVE_FLIGHT, true); + } + + MovementGeneratorType const movetype = GetMotionMaster()->GetCurrentMovementGeneratorType(); + if (movetype == WAYPOINT_MOTION_TYPE || movetype == ESCORT_MOTION_TYPE || (IsAIEnabled && AI()->IsEscorted())) + { + SetHomePosition(GetPosition()); + + // if its a vehicle, set the home position of every creature passenger at engage + // so that they are in combat range if hostile + if (Vehicle* vehicle = GetVehicleKit()) + { + for (auto& seat : vehicle->Seats) + if (Unit* passenger = ObjectAccessor::GetUnit(*this, seat.second.Passenger.Guid)) + if (Creature* creature = passenger->ToCreature()) + creature->SetHomePosition(GetPosition()); + } + } + + if (CreatureAI* ai = AI()) + { + ai->JustEngagedWith(target); + UpdateLeashExtensionTime(); + + if (CreatureGroup* formation = GetFormation()) + formation->MemberEngagingTarget(this, target); + + sScriptMgr->OnUnitEnterCombat(this, target); + } +} + +void Creature::AtDisengage() +{ + Unit::AtDisengage(); + + ClearUnitState(UNIT_STATE_ATTACK_PLAYER); + if (IsAlive() && HasDynamicFlag(UNIT_DYNFLAG_TAPPED)) + ReplaceAllDynamicFlags(GetCreatureTemplate()->dynamicflags); + + if (IsPet() || IsGuardian()) // update pets' speed for catchup OOC speed + { + UpdateSpeed(MOVE_RUN, true); + UpdateSpeed(MOVE_SWIM, true); + UpdateSpeed(MOVE_FLIGHT, true); + } +} + +bool Creature::IsEngaged() const +{ + if (CreatureAI const* ai = AI()) + return ai->IsEngaged(); + return false; +} + void Creature::ProhibitSpellSchool(SpellSchoolMask idSchoolMask, uint32 unTimeMs) { for (uint8 i = SPELL_SCHOOL_NORMAL; i < MAX_SPELL_SCHOOL; ++i) @@ -3393,7 +3429,9 @@ void Creature::SetDisplayFromModel(uint32 modelIdx) void Creature::SetTarget(ObjectGuid guid) { - if (!_focusSpell) + if (_focusSpell) + _spellFocusTarget = guid; + else SetGuidValue(UNIT_FIELD_TARGET, guid); } @@ -3404,6 +3442,7 @@ void Creature::FocusTarget(Spell const* focusSpell, WorldObject const* target) return; _focusSpell = focusSpell; + _spellFocusTarget = GetGuidValue(UNIT_FIELD_TARGET); SetGuidValue(UNIT_FIELD_TARGET, this == target ? ObjectGuid::Empty : target->GetGUID()); if (focusSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR5_AI_DOESNT_FACE_TARGET)) @@ -3433,10 +3472,8 @@ void Creature::ReleaseFocus(Spell const* focusSpell) return; _focusSpell = nullptr; - if (Unit* victim = GetVictim()) - SetGuidValue(UNIT_FIELD_TARGET, victim->GetGUID()); - else - SetGuidValue(UNIT_FIELD_TARGET, ObjectGuid::Empty); + SetGuidValue(UNIT_FIELD_TARGET, _spellFocusTarget); + _spellFocusTarget.Clear(); if (focusSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR5_AI_DOESNT_FACE_TARGET)) ClearUnitState(UNIT_STATE_ROTATING); @@ -3509,16 +3546,19 @@ bool Creature::IsMovementPreventedByCasting() const void Creature::SetCannotReachTarget(ObjectGuid const& cannotReach) { if (cannotReach == m_cannotReachTarget) - { return; - } m_cannotReachTarget = cannotReach; - m_cannotReachTimer = 0; if (cannotReach) { - LOG_DEBUG("entities.unit", "Creature::SetCannotReachTarget() called with true. Details: {}", GetDebugInfo()); + LOG_DEBUG("entities.unit", "Creature::SetCannotReachTarget() called with target {}. Details: {}", cannotReach.ToString(), GetDebugInfo()); + if (!GetCombatManager().IsInEvadeMode()) + GetCombatManager().StartEvadeTimer(); + } + else + { + GetCombatManager().StopEvade(); } } @@ -3529,12 +3569,7 @@ bool Creature::CanNotReachTarget() const bool Creature::IsNotReachableAndNeedRegen() const { - if (CanNotReachTarget()) - { - return m_cannotReachTimer >= (sWorld->getIntConfig(CONFIG_NPC_REGEN_TIME_IF_NOT_REACHABLE_IN_RAID) * IN_MILLISECONDS); - } - - return false; + return CanNotReachTarget() && GetCombatManager().IsEvadeRegen(); } std::shared_ptr const& Creature::GetLastLeashExtensionTimePtr() const diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 3c8617da1..2b33a764a 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -313,6 +313,11 @@ public: void SetInCombatWithZone(); + // Engagement callbacks (called from CreatureAI::EngagementStart/EngagementOver) + void AtEngage(Unit* target) override; + void AtDisengage() override; + [[nodiscard]] bool IsEngaged() const override; + [[nodiscard]] bool hasQuest(uint32 quest_id) const override; [[nodiscard]] bool hasInvolvedQuest(uint32 quest_id) const override; @@ -330,6 +335,7 @@ public: void SetCannotReachTarget(ObjectGuid const& target = ObjectGuid::Empty); [[nodiscard]] bool CanNotReachTarget() const; + [[nodiscard]] ObjectGuid const& GetCannotReachTarget() const { return m_cannotReachTarget; } [[nodiscard]] bool IsNotReachableAndNeedRegen() const; void SetPosition(float x, float y, float z, float o); @@ -521,9 +527,9 @@ private: mutable std::shared_ptr m_lastLeashExtensionTime; ObjectGuid m_cannotReachTarget; - uint32 m_cannotReachTimer; Spell const* _focusSpell; ///> Locks the target during spell cast for proper facing + ObjectGuid _spellFocusTarget; ///> Saved target during spell focus for restoration CreatureTextRepeatGroup m_textRepeat; diff --git a/src/server/game/Entities/Creature/CreatureGroups.cpp b/src/server/game/Entities/Creature/CreatureGroups.cpp index e3cb1fd3c..b970f03cf 100644 --- a/src/server/game/Entities/Creature/CreatureGroups.cpp +++ b/src/server/game/Entities/Creature/CreatureGroups.cpp @@ -227,9 +227,9 @@ void CreatureGroup::MemberEngagingTarget(Creature* member, Unit* target) continue; } - if (pMember->IsValidAttackTarget(target) && pMember->AI()) + if (pMember->IsValidAttackTarget(target)) { - pMember->AI()->AttackStart(target); + pMember->EngageWithTarget(target); } } } diff --git a/src/server/game/Entities/Pet/Pet.cpp b/src/server/game/Entities/Pet/Pet.cpp index 55ebf72af..95e4d878d 100644 --- a/src/server/game/Entities/Pet/Pet.cpp +++ b/src/server/game/Entities/Pet/Pet.cpp @@ -2318,6 +2318,7 @@ bool Pet::Create(ObjectGuid::LowType guidlow, Map* map, uint32 phaseMask, uint32 // Force regen flag for player pets, just like we do for players themselves SetUnitFlag2(UNIT_FLAG2_REGENERATE_POWER); SetSheath(SHEATH_STATE_MELEE); + GetThreatMgr().Initialize(); return true; } diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 2486bcb36..8f1ac6058 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -2161,7 +2161,7 @@ void Player::SetInWater(bool apply) // remove auras that need water/land RemoveAurasWithInterruptFlags(apply ? AURA_INTERRUPT_FLAG_NOT_ABOVEWATER : AURA_INTERRUPT_FLAG_NOT_UNDERWATER); - getHostileRefMgr().updateThreatTables(); + GetThreatMgr().EvaluateSuppressed(); if (InstanceScript* instance = GetInstanceScript()) instance->OnPlayerInWaterStateUpdate(this, apply); @@ -2203,7 +2203,6 @@ void Player::SetGameMaster(bool on) { if (GetSession()->IsGMAccount()) pet->SetFaction(FACTION_FRIENDLY); - pet->getHostileRefMgr().setOnlineOfflineState(false); } if (HasByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_FFA_PVP)) { @@ -2212,7 +2211,6 @@ void Player::SetGameMaster(bool on) } ResetContestedPvP(); - getHostileRefMgr().setOnlineOfflineState(false); CombatStopWithPets(); SetPhaseMask(uint32(PHASEMASK_ANYWHERE), false); // see and visible in all phases @@ -2234,10 +2232,7 @@ void Player::SetGameMaster(bool on) RemoveUnitFlag2(UNIT_FLAG2_ALLOW_CHEAT_SPELLS); if (Pet* pet = GetPet()) - { pet->SetFaction(GetFaction()); - pet->getHostileRefMgr().setOnlineOfflineState(true); - } // restore FFA PvP Server state if (sWorld->IsFFAPvPRealm()) @@ -2251,7 +2246,6 @@ void Player::SetGameMaster(bool on) // restore FFA PvP area state, remove not allowed for GM mounts UpdateArea(m_areaUpdateId); - getHostileRefMgr().setOnlineOfflineState(true); SetServerSideVisibilityDetect(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER); } @@ -2268,7 +2262,6 @@ void Player::SetGMVisible(bool on) m_ExtraFlags &= ~PLAYER_EXTRA_GM_INVISIBLE; SetServerSideVisibility(SERVERSIDE_VISIBILITY_GM, SEC_PLAYER); - getHostileRefMgr().setOnlineOfflineState(false); CombatStopWithPets(); } else @@ -10380,7 +10373,6 @@ void Player::CleanupAfterTaxiFlight() m_taxi.ClearTaxiDestinations(); // not destinations, clear source node Dismount(); RemoveUnitFlag(UNIT_FLAG_DISABLE_MOVE | UNIT_FLAG_TAXI_FLIGHT); - getHostileRefMgr().setOnlineOfflineState(true); } void Player::ContinueTaxiFlight() diff --git a/src/server/game/Entities/Player/PlayerUpdates.cpp b/src/server/game/Entities/Player/PlayerUpdates.cpp index 4b0bd93a4..504ff8fcf 100644 --- a/src/server/game/Entities/Player/PlayerUpdates.cpp +++ b/src/server/game/Entities/Player/PlayerUpdates.cpp @@ -402,13 +402,11 @@ void Player::Update(uint32 p_time) // != GetCharmGUID()))) RemovePet(pet, PET_SAVE_NOT_IN_SLOT, true); - // pussywizard: if (m_hostileReferenceCheckTimer <= p_time) { m_hostileReferenceCheckTimer = 15000; if (!GetMap()->IsDungeon()) - getHostileRefMgr().deleteReferencesOutOfRange( - GetVisibilityRange()); + GetCombatManager().EndCombatBeyondRange(GetVisibilityRange(), true); } else m_hostileReferenceCheckTimer -= p_time; @@ -1238,7 +1236,8 @@ void Player::UpdateArea(uint32 newArea) { SetByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_SANCTUARY); pvpInfo.IsInNoPvPArea = true; - CombatStopWithPets(); + if (!duel && GetCombatManager().HasPvPCombat()) + CombatStopWithPets(); } else RemoveByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_SANCTUARY); diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index ab71b9ac8..754025045 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -329,11 +329,11 @@ Unit::Unit() : WorldObject(), m_removedAurasCount(0), i_motionMaster(new MotionMaster(this)), m_regenTimer(0), - m_ThreatMgr(this), + m_threatManager(this), + m_combatManager(this), m_vehicle(nullptr), m_vehicleKit(nullptr), m_unitTypeMask(UNIT_MASK_NONE), - m_HostileRefMgr(this), m_comboTarget(nullptr), m_comboPoints(0) { @@ -406,16 +406,11 @@ Unit::Unit() : WorldObject(), m_CombatTimer = 0; m_lastManaUse = 0; - for (uint8 i = 0; i < MAX_SPELL_SCHOOL; ++i) - m_threatModifier[i] = 1.0f; - for (uint8 i = 0; i < MAX_MOVE_TYPE; ++i) m_speed_rate[i] = 1.0f; m_charmInfo = nullptr; - _redirectThreatInfo = RedirectThreatInfo(); - // remove aurastates allowing special moves for (uint8 i = 0; i < MAX_REACTIVE; ++i) m_reactiveTimer[i] = 0; @@ -439,6 +434,7 @@ Unit::Unit() : WorldObject(), _oldFactionId = 0; _isWalkingBeforeCharm = false; + _isCombatDisallowed = false; _lastExtraAttackSpell = 0; } @@ -529,16 +525,13 @@ void Unit::Update(uint32 p_time) _UpdateSpells( p_time ); - if (CanHaveThreatList() && GetThreatMgr().isNeedUpdateToClient(p_time)) - SendThreatListUpdate(); - // update combat timer only for players and pets (only pets with PetAI) if (IsInCombat() && (IsPlayer() || ((IsPet() || HasUnitTypeMask(UNIT_MASK_CONTROLLABLE_GUARDIAN)) && 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 (m_HostileRefMgr.IsEmpty()) + if (!GetCombatManager().HasCombat()) { // m_CombatTimer set at aura start and it will be freeze until aura removing if (m_CombatTimer <= p_time) @@ -548,6 +541,8 @@ void Unit::Update(uint32 p_time) } } + m_combatManager.Update(p_time); + _lastDamagedTargetGuid = ObjectGuid::Empty; if (_lastExtraAttackSpell) { @@ -1197,9 +1192,6 @@ uint32 Unit::DealDamage(Unit* attacker, Unit* victim, uint32 damage, CleanDamage if (attacker && attacker != victim) { - if (spellProto && victim->CanHaveThreatList() && !victim->HasUnitState(UNIT_STATE_EVADE) && !victim->IsInCombatWith(attacker)) - victim->CombatStart(attacker, !(spellProto->AttributesEx3 & SPELL_ATTR3_SUPPRESS_TARGET_PROCS)); - victim->AddThreat(attacker, float(damage), damageSchoolMask, spellProto); } } @@ -2701,7 +2693,7 @@ void Unit::AttackerStateUpdate(Unit* victim, WeaponAttackType attType /*= BASE_A // CombatStart puts the target into stand state, so we need to cache sit state here to know if we should crit later const bool sittingVictim = victim->IsPlayer() && (victim->IsSitState() || victim->getStandState() == UNIT_STAND_STATE_SLEEP); - CombatStart(victim); + AtTargetAttacked(victim, true); RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_MELEE_ATTACK); if (attType != BASE_ATTACK && attType != OFF_ATTACK) @@ -7348,14 +7340,10 @@ bool Unit::Attack(Unit* victim, bool meleeAttack) // set position before any AI calls/assistance //if (IsCreature()) // ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ()); - if (creature && !(IsControllableGuardian() && IsControlledByPlayer())) + if (creature && !IsControlledByPlayer()) { // should not let player enter combat by right clicking target - doesn't helps - SetInCombatWith(victim); - if (victim->IsPlayer()) - victim->SetInCombatWith(this); - - AddThreat(victim, 0.0f); + EngageWithTarget(victim); creature->SendAIReaction(AI_REACTION_HOSTILE); @@ -7415,7 +7403,7 @@ bool Unit::AttackStop() return true; } -void Unit::CombatStop(bool includingCast) +void Unit::CombatStop(bool includingCast, bool mutualPvP) { if (includingCast && IsNonMeleeSpellCast(false)) InterruptNonMeleeSpells(false); @@ -7426,7 +7414,14 @@ void Unit::CombatStop(bool includingCast) ToPlayer()->SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel if (Creature* pCreature = ToCreature()) pCreature->ClearLastLeashExtensionTimePtr(); - ClearInCombat(); + + if (mutualPvP) + ClearInCombat(); + else + { + GetCombatManager().EndAllPvECombat(); + GetCombatManager().SuppressPvPCombat(); + } // xinef: just in case if (IsPetInCombat() && !IsPlayer()) @@ -7441,6 +7436,16 @@ void Unit::CombatStopWithPets(bool includingCast) (*itr)->CombatStop(includingCast); } +void Unit::EngageWithTarget(Unit* who) +{ + if (!who) + return; + if (CanHaveThreatList()) + m_threatManager.AddThreat(who, 0.0f, nullptr, true, true); + else + SetInCombatWith(who); +} + bool Unit::isAttackingPlayer() const { if (HasUnitState(UNIT_STATE_ATTACK_PLAYER)) @@ -7460,6 +7465,38 @@ bool Unit::isAttackingPlayer() const return false; } +void Unit::UpdatePetCombatState() +{ + ASSERT(!IsPet()); // player pets do not use UNIT_FLAG_PET_IN_COMBAT for this purpose + + bool state = false; + for (Unit* minion : m_Controlled) + if (minion->IsInCombat()) + { + state = true; + break; + } + + if (state) + SetUnitFlag(UNIT_FLAG_PET_IN_COMBAT); + else + RemoveUnitFlag(UNIT_FLAG_PET_IN_COMBAT); +} + +void Unit::AtExitCombat() +{ + RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_LEAVE_COMBAT); +} + +void Unit::AtEngage(Unit* /*target*/) +{ + if (HasUnitState(UNIT_STATE_DISTRACTED)) + { + ClearUnitState(UNIT_STATE_DISTRACTED); + GetMotionMaster()->MovementExpiredOnSlot(MOTION_SLOT_CONTROLLED, false); + } +} + /** * @brief Remove all units in m_attackers list and send them AttackStop() */ @@ -8262,13 +8299,10 @@ void Unit::SendEnergizeSpellLog(Unit* victim, uint32 spellID, uint32 damage, Pow void Unit::EnergizeBySpell(Unit* victim, uint32 spellID, uint32 damage, Powers powerType) { - int32 gainedPower = victim->ModifyPower(powerType, damage, false); + victim->ModifyPower(powerType, damage, false); - if (powerType != POWER_HAPPINESS && gainedPower) - { - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellID); - victim->getHostileRefMgr().threatAssist(this, float(gainedPower) * 0.5f, spellInfo); - } + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellID)) + victim->GetThreatMgr().ForwardThreatForAssistingMe(this, float(damage) / 2.0f, spellInfo, true); SendEnergizeSpellLog(victim, spellID, damage, powerType); } @@ -10519,245 +10553,56 @@ void Unit::Dismount() } } -void Unit::SetInCombatWith(Unit* enemy, uint32 duration) -{ - // Xinef: Dont allow to start combat with triggers - if (enemy->IsCreature() && enemy->ToCreature()->IsTrigger()) - return; - - Unit* eOwner = enemy->GetCharmerOrOwnerOrSelf(); - if (eOwner->IsPvP() || eOwner->IsFFAPvP()) - { - SetInCombatState(true, enemy, duration); - return; - } - - // check for duel - if (eOwner->IsPlayer() && eOwner->ToPlayer()->duel) - { - Unit const* myOwner = GetCharmerOrOwnerOrSelf(); - if (((Player const*)eOwner)->duel->Opponent == myOwner) - { - SetInCombatState(true, enemy, duration); - return; - } - } - - SetInCombatState(false, enemy, duration); -} - void Unit::SetImmuneToPC(bool apply, bool keepCombat) { - (void)keepCombat; if (apply) + { SetUnitFlag(UNIT_FLAG_IMMUNE_TO_PC); + if (!keepCombat) + { + std::vector toEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (pair.second->GetOther(this)->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (auto const& pair : m_combatManager.GetPvPCombatRefs()) + toEnd.push_back(pair.second); + for (CombatReference* ref : toEnd) + ref->EndCombat(); + } + } else RemoveUnitFlag(UNIT_FLAG_IMMUNE_TO_PC); } void Unit::SetImmuneToNPC(bool apply, bool keepCombat) { - (void)keepCombat; if (apply) + { SetUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); + if (!keepCombat) + { + std::vector toEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (!pair.second->GetOther(this)->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) + toEnd.push_back(pair.second); + for (CombatReference* ref : toEnd) + ref->EndCombat(); + } + } else RemoveUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); } -void Unit::CombatStart(Unit* victim, bool initialAggro) -{ - // Xinef: Dont allow to start combat with triggers - if (victim->IsCreature() && victim->ToCreature()->IsTrigger()) - return; - - if (initialAggro) - { - // Make player victim stand up automatically - if (victim->getStandState() && victim->IsPlayer()) - { - victim->SetStandState(UNIT_STAND_STATE_STAND); - } - - if (!victim->IsInCombat() && !victim->IsPlayer() && !victim->ToCreature()->HasReactState(REACT_PASSIVE) && victim->ToCreature()->IsAIEnabled) - { - if (victim->IsPet()) - victim->ToCreature()->AI()->AttackedBy(this); // PetAI has special handler before AttackStart() - else - { - victim->ToCreature()->AI()->AttackStart(this); - // if the target is an NPC with a pet or minion, pet should react. - if (Unit* victimControlledUnit = victim->GetFirstControlled()) - { - victimControlledUnit->SetInCombatWith(this); - SetInCombatWith(victimControlledUnit); - victimControlledUnit->AddThreat(this, 0.0f); - } - } - - // if unit has an owner, put owner in combat. - if (Unit* victimOwner = victim->GetOwner()) - { - if (!(victimOwner->IsInCombatWith(this))) - { - /* warding off to not take over aggro for no reason - Using only AddThreat causes delay in attack */ - if (!victimOwner->IsInCombat() && victimOwner->IsAIEnabled) - { - victimOwner->ToCreature()->AI()->AttackStart(this); - } - victimOwner->SetInCombatWith(this); - SetInCombatWith(victimOwner); - victimOwner->AddThreat(this, 0.0f); - } - } - } - - bool alreadyInCombat = IsInCombat(); - - SetInCombatWith(victim); - victim->SetInCombatWith(this); - - // Update leash timer when attacking creatures - if (victim->IsCreature() && this != victim) - victim->ToCreature()->UpdateLeashExtensionTime(); - - // Xinef: If pet started combat - put owner in combat - if (!alreadyInCombat && IsInCombat()) - { - if (Unit* owner = GetOwner()) - { - owner->SetInCombatWith(victim); - victim->SetInCombatWith(owner); - } - } - } - - Unit* who = victim->GetCharmerOrOwnerOrSelf(); - if (who->IsPlayer()) - SetContestedPvP(who->ToPlayer()); - - Player* player = GetCharmerOrOwnerPlayerOrPlayerItself(); - if (player && who->IsPvP() && (!who->IsPlayer() || !player->duel || player->duel->Opponent != who)) - { - player->UpdatePvP(true); - player->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_ENTER_PVP_COMBAT); - } -} - -void Unit::CombatStartOnCast(Unit* target, bool initialAggro, uint32 duration) -{ - // Xinef: Dont allow to start combat with triggers - if (target->IsCreature() && target->ToCreature()->IsTrigger()) - return; - - if (initialAggro) - { - SetInCombatWith(target, duration); - - // Xinef: If pet started combat - put owner in combat - if (Unit* owner = GetOwner()) - owner->SetInCombatWith(target, duration); - - // Update leash timer when attacking creatures - if (target->IsCreature()) - target->ToCreature()->UpdateLeashExtensionTime(); - else if (ToCreature()) // Reset leash if it is a spell caster, else it may evade inbetween casts - ToCreature()->UpdateLeashExtensionTime(); - } - - Unit* who = target->GetCharmerOrOwnerOrSelf(); - if (who->IsPlayer()) - SetContestedPvP(who->ToPlayer()); - - Player* player = GetCharmerOrOwnerPlayerOrPlayerItself(); - if (player && who->IsPvP() && (!who->IsPlayer() || !player->duel || player->duel->Opponent != who)) - { - player->UpdatePvP(true); - player->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_ENTER_PVP_COMBAT); - } -} - -void Unit::SetInCombatState(bool PvP, Unit* enemy, uint32 duration) -{ - // only alive units can be in combat - if (!IsAlive()) - return; - - if (PvP) - m_CombatTimer = std::max(GetCombatTimer(), std::max(5500, duration)); - else if (duration) - m_CombatTimer = std::max(GetCombatTimer(), duration); - - if (HasUnitState(UNIT_STATE_EVADE) || GetCreatureType() == CREATURE_TYPE_NON_COMBAT_PET) - return; - - // xinef: if we somehow engage in combat (scripts, dunno) with player, remove this flag so he can fight back - if (IsCreature() && enemy && IsImmuneToPC() && enemy->GetCharmerOrOwnerPlayerOrPlayerItself()) - SetImmuneToPC(false); // unit has engaged in combat, remove immunity so players can fight back - - if (IsInCombat()) - return; - - SetUnitFlag(UNIT_FLAG_IN_COMBAT); - - if (Creature* creature = ToCreature()) - { - // Set home position at place of engaging combat for escorted creatures - if ((IsAIEnabled && creature->AI()->IsEscorted()) || - GetMotionMaster()->GetCurrentMovementGeneratorType() == WAYPOINT_MOTION_TYPE || - GetMotionMaster()->GetCurrentMovementGeneratorType() == ESCORT_MOTION_TYPE) - creature->SetHomePosition(GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation()); - - if (enemy) - { - creature->UpdateLeashExtensionTime(); - - if (IsAIEnabled) - creature->AI()->JustEngagedWith(enemy); - - if (creature->GetFormation()) - creature->GetFormation()->MemberEngagingTarget(creature, enemy); - - sScriptMgr->OnUnitEnterCombat(creature, enemy); - } - - creature->RefreshSwimmingFlag(); - - if (IsPet()) - { - UpdateSpeed(MOVE_RUN, true); - UpdateSpeed(MOVE_SWIM, true); - UpdateSpeed(MOVE_FLIGHT, true); - } - - if (!(creature->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_ALLOW_MOUNTED_COMBAT)) - Dismount(); - if (!IsStandState()) // pussywizard: already done in CombatStart(target, initialAggro) for the target, but when aggro'ing from MoveInLOS CombatStart is not called! - SetStandState(UNIT_STAND_STATE_STAND); - } - - for (Unit::ControlSet::iterator itr = m_Controlled.begin(); itr != m_Controlled.end();) - { - Unit* controlled = *itr; - ++itr; - - // Xinef: Dont set combat for passive units, they will evade in next update... - if (controlled->IsCreature() && controlled->ToCreature()->HasReactState(REACT_PASSIVE)) - continue; - - controlled->SetInCombatState(PvP, enemy, duration); - } - - if (Player* player = this->ToPlayer()) - { - sScriptMgr->OnPlayerEnterCombat(player, enemy); - } -} - void Unit::ClearInCombat() { m_CombatTimer = 0; - RemoveUnitFlag(UNIT_FLAG_IN_COMBAT); + // Let CombatManager handle flag removal through UpdateOwnerCombatState + // so that AtExitCombat/AtDisengage callbacks properly fire. + GetCombatManager().EndAllCombat(); + // If flag is still set (e.g. set by old code path without CombatReference), + // remove it directly. + if (IsInCombat()) + RemoveUnitFlag(UNIT_FLAG_IN_COMBAT); // Player's state will be cleared in Player::UpdateContestedPvP if (Creature* creature = ToCreature()) @@ -11454,8 +11299,6 @@ void Unit::setDeathState(DeathState s, bool despawn) if (s != DeathState::Alive && s != DeathState::JustRespawned) { CombatStop(); - GetThreatMgr().ClearAllThreat(); - getHostileRefMgr().deleteReferences(); ClearComboPointHolders(); // any combo points pointed to unit lost at it death if (IsNonMeleeSpellCast(false)) @@ -11508,118 +11351,56 @@ void Unit::setDeathState(DeathState s, bool despawn) ########################################*/ bool Unit::CanHaveThreatList(bool skipAliveCheck) const { - // only creatures can have threat list - if (!IsCreature()) + // Delegate to ThreatManager's cached value for consistency + if (!m_threatManager.CanHaveThreatList()) return false; // only alive units can have threat list if (!skipAliveCheck && !IsAlive()) return false; - // totems can not have threat list - if (ToCreature()->IsTotem()) - return false; - - // vehicles can not have threat list + // vehicles can not have threat list in battlegrounds if (ToCreature()->IsVehicle() && GetMap()->IsBattlegroundOrArena()) return false; - // summons can not have a threat list, unless they are controlled by a creature - if (HasUnitTypeMask(UNIT_MASK_MINION | UNIT_MASK_GUARDIAN | UNIT_MASK_CONTROLLABLE_GUARDIAN) && ((Pet*)this)->GetOwnerGUID().IsPlayer()) - return false; - return true; } //====================================================================== -float Unit::ApplyTotalThreatModifier(float fThreat, SpellSchoolMask schoolMask) -{ - if (!HasThreatAura() || fThreat < 0) - return fThreat; - - SpellSchools school = GetFirstSchoolInMask(schoolMask); - - return fThreat * m_threatModifier[school]; -} - -//====================================================================== - -void Unit::AddThreat(Unit* victim, float fThreat, SpellSchoolMask schoolMask, SpellInfo const* threatSpell) +void Unit::AddThreat(Unit* victim, float fThreat, SpellSchoolMask /*schoolMask*/, SpellInfo const* threatSpell) { // Only mobs can manage threat lists if (CanHaveThreatList() && !HasUnitState(UNIT_STATE_EVADE)) { - m_ThreatMgr.AddThreat(victim, fThreat, schoolMask, threatSpell); + m_threatManager.AddThreat(victim, fThreat, threatSpell); } } //====================================================================== -void Unit::TauntApply(Unit* taunter) +void Unit::AtTargetAttacked(Unit* target, bool canInitialAggro) { - ASSERT(IsCreature()); - - if (!taunter || (taunter->IsPlayer() && taunter->ToPlayer()->IsGameMaster())) + if (!target->IsEngaged() && !canInitialAggro) return; - if (!CanHaveThreatList()) - return; + target->EngageWithTarget(this); + if (Unit* targetOwner = target->GetCharmerOrOwner()) + targetOwner->EngageWithTarget(this); - Creature* creature = ToCreature(); + // Patch 3.0.8: All player spells which cause a creature to become aggressive + // to you will now also immediately cause the creature to be tapped. + if (Creature* creature = target->ToCreature()) + if (!creature->hasLootRecipient() && IsPlayer()) + creature->SetLootRecipient(this); - if (creature->HasReactState(REACT_PASSIVE)) - return; - - Unit* target = GetVictim(); - if (target && target == taunter) - return; - - SetInFront(taunter); - SetGuidValue(UNIT_FIELD_TARGET, taunter->GetGUID()); - - if (creature->IsAIEnabled) - creature->AI()->AttackStart(taunter); - - //m_ThreatMgr.tauntApply(taunter); -} - -//====================================================================== - -void Unit::TauntFadeOut(Unit* taunter) -{ - ASSERT(IsCreature()); - - if (!taunter || (taunter->IsPlayer() && taunter->ToPlayer()->IsGameMaster())) - return; - - if (!CanHaveThreatList()) - return; - - Creature* creature = ToCreature(); - - if (creature->HasReactState(REACT_PASSIVE)) - return; - - Unit* target = GetVictim(); - if (!target || target != taunter) - return; - - if (m_ThreatMgr.isThreatListEmpty()) + Player* myPlayerOwner = GetCharmerOrOwnerPlayerOrPlayerItself(); + Player* targetPlayerOwner = target->GetCharmerOrOwnerPlayerOrPlayerItself(); + if (myPlayerOwner && targetPlayerOwner && !(myPlayerOwner->duel && myPlayerOwner->duel->Opponent == targetPlayerOwner)) { - if (creature->IsAIEnabled) - creature->AI()->EnterEvadeMode(CreatureAI::EVADE_REASON_NO_HOSTILES); - return; - } - - target = creature->SelectVictim(); // might have more taunt auras remaining - - if (target && target != taunter) - { - SetGuidValue(UNIT_FIELD_TARGET, target->GetGUID()); - SetInFront(target); - if (creature->IsAIEnabled) - creature->AI()->AttackStart(target); + myPlayerOwner->UpdatePvP(true); + myPlayerOwner->SetContestedPvP(targetPlayerOwner); + myPlayerOwner->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_ENTER_PVP_COMBAT); } } @@ -11633,21 +11414,11 @@ Unit* Creature::SelectVictim() Unit* target = nullptr; - // First checking if we have some taunt on us - AuraEffectList const& tauntAuras = GetAuraEffectsByType(SPELL_AURA_MOD_TAUNT); - if (!tauntAuras.empty()) - for (Unit::AuraEffectList::const_reverse_iterator itr = tauntAuras.rbegin(); itr != tauntAuras.rend(); ++itr) - if (Unit* caster = (*itr)->GetCaster()) - if (CanCreatureAttack(caster) && !caster->HasAuraTypeWithCaster(SPELL_AURA_IGNORED, GetGUID())) - { - target = caster; - break; - } - + // Taunt handling is now done inside ThreatManager via TauntState if (CanHaveThreatList()) { - if (!target && !m_ThreatMgr.isThreatListEmpty()) - target = m_ThreatMgr.getHostileTarget(); + if (!m_threatManager.IsThreatListEmpty()) + target = m_threatManager.GetCurrentVictim(); } else if (!HasReactState(REACT_PASSIVE)) { @@ -11671,9 +11442,10 @@ Unit* Creature::SelectVictim() else return nullptr; - if (target && CanCreatureAttack(target)) + if (target && _IsTargetAcceptable(target) && CanCreatureAttack(target)) { - SetInFront(target); + if (!HasSpellFocus()) + SetInFront(target); return target; } @@ -12776,8 +12548,6 @@ void Unit::CleanupBeforeRemoveFromMap(bool finalCleanup) CombatStop(); ClearComboPoints(); ClearComboPointHolders(); - GetThreatMgr().ClearAllThreat(); - getHostileRefMgr().deleteReferences(); GetMotionMaster()->Clear(false); // remove different non-standard movement generators. } @@ -14290,7 +14060,6 @@ void Unit::Kill(Unit* killer, Unit* victim, bool durabilityLoss, WeaponAttackTyp // Stop attacks victim->CombatStop(); - victim->getHostileRefMgr().deleteReferences(); // restore for use at real death victim->SetUInt32Value(PLAYER_SELF_RES_SPELL, ressSpellId); @@ -14352,8 +14121,6 @@ void Unit::Kill(Unit* killer, Unit* victim, bool durabilityLoss, WeaponAttackTyp if (!creature->IsPet() && creature->GetLootMode() > 0) { - creature->GetThreatMgr().ClearAllThreat(); - // must be after setDeathState which resets dynamic flags if (!creature->loot.isLooted()) { @@ -14814,10 +14581,6 @@ bool Unit::SetCharmedBy(Unit* charmer, CharmType type, AuraApplication const* au CastStop(); AttackStop(); - // Xinef: dont reset threat and combat, put them on offline list, moved down after faction changes - // CombatStop(); /// @todo: CombatStop(true) may cause crash (interrupt spells) - // DeleteThreatList(); - Player* playerCharmer = charmer->ToPlayer(); // Charmer stop charming @@ -15027,6 +14790,10 @@ void Unit::RemoveCharmedBy(Unit* charmer) ASSERT(type != CHARM_TYPE_POSSESS || charmer->IsPlayer()); ASSERT(type != CHARM_TYPE_VEHICLE || (IsCreature() && IsVehicle())); + // Vehicle should not attack its passenger after he exits the seat + if (type != CHARM_TYPE_VEHICLE) + LastCharmerGUID = charmer->GetGUID(); + charmer->SetCharm(this, false); StopAttackingInvalidTarget(); @@ -15478,41 +15245,29 @@ void Unit::SetPhaseMask(uint32 newPhaseMask, bool update) if (!sScriptMgr->CanSetPhaseMask(this, newPhaseMask, update)) return; - // modify hostile references for new phasemask, some special cases deal with hostile references themselves - if (IsCreature() || (!ToPlayer()->IsGameMaster() && !ToPlayer()->GetSession()->PlayerLogout())) - { - HostileRefMgr& refMgr = getHostileRefMgr(); - HostileReference* ref = refMgr.getFirst(); - - while (ref) - { - if (Unit* unit = ref->GetSource()->GetOwner()) - if (Creature* creature = unit->ToCreature()) - refMgr.setOnlineOfflineState(creature, creature->InSamePhase(newPhaseMask)); - - ref = ref->next(); - } - - // modify threat lists for new phasemask - if (!IsPlayer()) - { - ThreatContainer::StorageType threatList = GetThreatMgr().GetThreatList(); - ThreatContainer::StorageType offlineThreatList = GetThreatMgr().GetOfflineThreatList(); - - // merge expects sorted lists - threatList.sort(); - offlineThreatList.sort(); - threatList.merge(offlineThreatList); - - for (ThreatContainer::StorageType::const_iterator itr = threatList.begin(); itr != threatList.end(); ++itr) - if (Unit* unit = (*itr)->getTarget()) - unit->getHostileRefMgr().setOnlineOfflineState(ToCreature(), unit->InSamePhase(newPhaseMask)); - } - } + // Phase-related threat updates are done AFTER the phase change below } WorldObject::SetPhaseMask(newPhaseMask, false); + // Now update threat online states with the new phase mask applied + if (IsCreature() || (IsPlayer() && !ToPlayer()->IsGameMaster() && !ToPlayer()->GetSession()->PlayerLogout())) + { + // Update online state for units that have me on their threat list + for (auto const& pair : GetThreatMgr().GetThreatenedByMeList()) + { + if (ThreatReference* ref = pair.second) + ref->UpdateOffline(); + } + + // Update online state for units on my threat list + if (!IsPlayer()) + { + for (ThreatReference* ref : GetThreatMgr().GetModifiableThreatList()) + ref->UpdateOffline(); + } + } + if (!IsInWorld()) { return; @@ -15655,11 +15410,6 @@ uint32 Unit::GetModelForForm(ShapeshiftForm form, uint32 spellId) return modelid; } -Unit* Unit::GetRedirectThreatTarget() const -{ - return _redirectThreatInfo.GetTargetGUID() ? ObjectAccessor::GetUnit(*this, _redirectThreatInfo.GetTargetGUID()) : nullptr; -} - bool Unit::IsAttackSpeedOverridenShapeShift() const { // Mirroring clientside gameplay logic @@ -16221,64 +15971,6 @@ void Unit::UpdateHeight(float newZ) GetVehicleKit()->RelocatePassengers(); } -void Unit::SendThreatListUpdate() -{ - if (!GetThreatMgr().isThreatListEmpty()) - { - uint32 count = GetThreatMgr().GetThreatList().size(); - - //LOG_DEBUG("entities.unit", "WORLD: Send SMSG_THREAT_UPDATE Message"); - WorldPacket data(SMSG_THREAT_UPDATE, 8 + count * 8); - data << GetPackGUID(); - data << uint32(count); - ThreatContainer::StorageType const& tlist = GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = tlist.begin(); itr != tlist.end(); ++itr) - { - data << (*itr)->getUnitGuid().WriteAsPacked(); - data << uint32((*itr)->GetThreat() * 100); - } - SendMessageToSet(&data, false); - } -} - -void Unit::SendChangeCurrentVictimOpcode(HostileReference* pHostileReference) -{ - if (!GetThreatMgr().isThreatListEmpty()) - { - uint32 count = GetThreatMgr().GetThreatList().size(); - - LOG_DEBUG("entities.unit", "WORLD: Send SMSG_HIGHEST_THREAT_UPDATE Message"); - WorldPacket data(SMSG_HIGHEST_THREAT_UPDATE, 8 + 8 + count * 8); - data << GetPackGUID(); - data << pHostileReference->getUnitGuid().WriteAsPacked(); - data << uint32(count); - ThreatContainer::StorageType const& tlist = GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = tlist.begin(); itr != tlist.end(); ++itr) - { - data << (*itr)->getUnitGuid().WriteAsPacked(); - data << uint32((*itr)->GetThreat() * 100); - } - SendMessageToSet(&data, false); - } -} - -void Unit::SendClearThreatListOpcode() -{ - LOG_DEBUG("entities.unit", "WORLD: Send SMSG_THREAT_CLEAR Message"); - WorldPacket data(SMSG_THREAT_CLEAR, 8); - data << GetPackGUID(); - SendMessageToSet(&data, false); -} - -void Unit::SendRemoveFromThreatListOpcode(HostileReference* pHostileReference) -{ - LOG_DEBUG("entities.unit", "WORLD: Send SMSG_THREAT_REMOVE Message"); - WorldPacket data(SMSG_THREAT_REMOVE, 8 + 8); - data << GetPackGUID(); - data << pHostileReference->getUnitGuid().WriteAsPacked(); - SendMessageToSet(&data, false); -} - void Unit::RewardRage(uint32 damage, uint32 weaponSpeedHitFactor, bool attacker) { // Rage formulae https://wowwiki-archive.fandom.com/wiki/Rage#Formulae @@ -16340,7 +16032,13 @@ void Unit::StopAttackFaction(uint32 faction_id) ++itr; } - getHostileRefMgr().deleteReferencesForFaction(faction_id); + // End combat and threat references with creatures in this faction + std::vector refsToEnd; + for (auto const& pair : m_combatManager.GetPvECombatRefs()) + if (pair.second->GetOther(this)->GetFactionTemplateEntry()->faction == faction_id) + refsToEnd.push_back(pair.second); + for (CombatReference* ref : refsToEnd) + ref->EndCombat(); for (ControlSet::const_iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) (*itr)->StopAttackFaction(faction_id); @@ -17300,20 +16998,14 @@ DisplayRace Unit::GetDisplayRaceFromModelId(uint32 modelId) const // Check if unit in combat with specific unit bool Unit::IsInCombatWith(Unit const* who) const { - // Check target exists if (!who) return false; - // Search in threat list - ObjectGuid guid = who->GetGUID(); - for (ThreatContainer::StorageType::const_iterator i = m_ThreatMgr.GetThreatList().begin(); i != m_ThreatMgr.GetThreatList().end(); ++i) - { - HostileReference* ref = (*i); - // Return true if the unit matches - if (ref && ref->getUnitGuid() == guid) - return true; - } - // Nothing found, false. - return false; + return GetCombatManager().IsInCombatWith(who); +} + +bool Unit::IsThreatened() const +{ + return !m_threatManager.IsThreatListEmpty(); } /** diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index dd4d8efc6..286548115 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -20,7 +20,7 @@ #include "EnumFlag.h" #include "EventProcessor.h" -#include "HostileRefMgr.h" +#include "CombatManager.h" #include "ItemTemplate.h" #include "MotionMaster.h" #include "Object.h" @@ -28,7 +28,7 @@ #include "SharedDefines.h" #include "SpellAuraDefines.h" #include "SpellDefines.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" #include "UnitDefines.h" #include "UnitUtils.h" #include @@ -902,7 +902,7 @@ public: [[nodiscard]] bool isAttackingPlayer() const; [[nodiscard]] Unit* GetVictim() const { return m_attacking; } - void CombatStop(bool includingCast = false); + void CombatStop(bool includingCast = false, bool mutualPvP = true); void CombatStopWithPets(bool includingCast = false); void StopAttackFaction(uint32 faction_id); void StopAttackingInvalidTarget(); @@ -928,17 +928,15 @@ public: void SetImmuneToNPC(bool apply, bool keepCombat = false); bool IsImmuneToNPC() const { return HasUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); } - bool IsEngaged() const { return IsInCombat(); } - bool IsEngagedBy(Unit const* who) const { return IsInCombatWith(who); } + virtual bool IsEngaged() const { return IsInCombat(); } + bool IsEngagedBy(Unit const* who) const { return CanHaveThreatList() ? IsThreatenedBy(who) : IsInCombatWith(who); } + void EngageWithTarget(Unit* who); [[nodiscard]] bool IsInCombat() const { return HasUnitFlag(UNIT_FLAG_IN_COMBAT); } bool IsInCombatWith(Unit const* who) const; [[nodiscard]] bool IsPetInCombat() const { return HasUnitFlag(UNIT_FLAG_PET_IN_COMBAT); } - void CombatStart(Unit* target, bool initialAggro = true); - void CombatStartOnCast(Unit* target, bool initialAggro = true, uint32 duration = 0); - void SetInCombatState(bool PvP, Unit* enemy = nullptr, uint32 duration = 0); - void SetInCombatWith(Unit* enemy, uint32 duration = 0); + void SetInCombatWith(Unit* enemy, bool addSecondUnitSuppressed = false) { if (enemy) m_combatManager.SetInCombatWith(enemy, addSecondUnitSuppressed); } void ClearInCombat(); void ClearInPetCombat(); [[nodiscard]] uint32 GetCombatTimer() const { return m_CombatTimer; } @@ -947,21 +945,28 @@ public: // Threat related methods [[nodiscard]] bool CanHaveThreatList(bool skipAliveCheck = false) const; void AddThreat(Unit* victim, float fThreat, SpellSchoolMask schoolMask = SPELL_SCHOOL_MASK_NORMAL, SpellInfo const* threatSpell = nullptr); - float ApplyTotalThreatModifier(float fThreat, SpellSchoolMask schoolMask = SPELL_SCHOOL_MASK_NORMAL); - void TauntApply(Unit* victim); - void TauntFadeOut(Unit* taunter); - ThreatMgr& GetThreatMgr() { return m_ThreatMgr; } - ThreatMgr const& GetThreatMgr() const { return m_ThreatMgr; } - void addHatedBy(HostileReference* pHostileReference) { m_HostileRefMgr.insertFirst(pHostileReference); }; - void removeHatedBy(HostileReference* /*pHostileReference*/) { /* nothing to do yet */ } - HostileRefMgr& getHostileRefMgr() { return m_HostileRefMgr; } + void AtTargetAttacked(Unit* target, bool canInitialAggro); - // Redirect Threat - void SetRedirectThreat(ObjectGuid guid, uint32 pct) { _redirectThreatInfo.Set(guid, pct); } - void ResetRedirectThreat() { SetRedirectThreat(ObjectGuid::Empty, 0); } - void ModifyRedirectThreat(int32 amount) { _redirectThreatInfo.ModifyThreatPct(amount); } - uint32 GetRedirectThreatPercent() { return _redirectThreatInfo.GetThreatPct(); } - [[nodiscard]] Unit* GetRedirectThreatTarget() const; + // ThreatManager/CombatManager accessors + ThreatManager& GetThreatMgr() { return m_threatManager; } + ThreatManager const& GetThreatMgr() const { return m_threatManager; } + CombatManager& GetCombatManager() { return m_combatManager; } + CombatManager const& GetCombatManager() const { return m_combatManager; } + + // Combat state methods + [[nodiscard]] bool IsThreatened() const; + [[nodiscard]] bool IsThreatenedBy(Unit const* who) const { return who && m_threatManager.IsThreatenedBy(who, true); } + void UpdatePetCombatState(); + + virtual void AtEnterCombat() {} + virtual void AtExitCombat(); + + // Engagement callbacks - override in Creature for creature-specific behavior + virtual void AtEngage(Unit* target); + virtual void AtDisengage() {} + + [[nodiscard]] bool IsCombatDisallowed() const { return _isCombatDisallowed; } + void SetIsCombatDisallowed(bool value) { _isCombatDisallowed = value; } void SetLastDamagedTargetGuid(ObjectGuid const& guid) { _lastDamagedTargetGuid = guid; } [[nodiscard]] ObjectGuid const& GetLastDamagedTargetGuid() const { return _lastDamagedTargetGuid; } @@ -1818,7 +1823,7 @@ public: [[nodiscard]] bool HasIgnoreTargetResistAura() const { return HasAuraType(SPELL_AURA_MOD_IGNORE_TARGET_RESIST); } [[nodiscard]] bool HasIncreaseMountedSpeedAura() const { return HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED); } [[nodiscard]] bool HasIncreaseMountedFlightSpeedAura() const { return HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED); } - [[nodiscard]] bool HasThreatAura() const { return HasAuraType(SPELL_AURA_MOD_THREAT); } + [[nodiscard]] bool HasAttackerSpellCritChanceAura() const { return HasAuraType(SPELL_AURA_MOD_ATTACKER_SPELL_CRIT_CHANCE); } [[nodiscard]] bool HasUnattackableAura() const { return HasAuraType(SPELL_AURA_MOD_UNATTACKABLE); } [[nodiscard]] bool HasHealthRegenInCombatAura() const { return HasAuraType(SPELL_AURA_MOD_HEALTH_REGEN_IN_COMBAT); } @@ -2044,10 +2049,6 @@ public: void SendMovementFeatherFall(Player* sendTo); void SendMovementHover(Player* sendTo); - void SendChangeCurrentVictimOpcode(HostileReference* pHostileReference); - void SendClearThreatListOpcode(); - void SendRemoveFromThreatListOpcode(HostileReference* pHostileReference); - void SendThreatListUpdate(); void SendClearTarget(); // Misc functions @@ -2077,7 +2078,6 @@ public: float m_modSpellHitChance; int32 m_baseSpellCritChance; - float m_threatModifier[MAX_SPELL_SCHOOL]; float m_modAttackSpeedPct[3]; SpellImmuneList m_spellImmune[MAX_SPELL_IMMUNITY]; @@ -2087,6 +2087,7 @@ public: typedef std::set PetAuraSet; PetAuraSet m_petAuras; + ObjectGuid LastCharmerGUID; bool IsAIEnabled; bool NeedChangeAI; @@ -2184,7 +2185,8 @@ protected: uint32 m_reactiveTimer[MAX_REACTIVE]; int32 m_regenTimer; - ThreatMgr m_ThreatMgr; + ThreatManager m_threatManager; + CombatManager m_combatManager; typedef std::map CharmThreatMap; CharmThreatMap _charmThreatInfo; @@ -2225,8 +2227,6 @@ private: //TimeTrackerSmall m_movesplineTimer; Diminishing m_Diminishing; - // Manage all Units that are threatened by us - HostileRefMgr m_HostileRefMgr; std::unordered_set m_followingMe; @@ -2234,13 +2234,12 @@ private: int8 m_comboPoints; std::unordered_set m_ComboPointHolders; - RedirectThreatInfo _redirectThreatInfo; - bool m_cleanupDone; // lock made to not add stuff after cleanup before delete bool m_duringRemoveFromWorld; // lock made to not add stuff after begining removing from world uint32 _oldFactionId; ///< faction before charm bool _isWalkingBeforeCharm; ///< Are we walking before we were charmed? + bool _isCombatDisallowed; uint32 _lastExtraAttackSpell; std::unordered_map extraAttacksTargets; diff --git a/src/server/game/Entities/Vehicle/Vehicle.cpp b/src/server/game/Entities/Vehicle/Vehicle.cpp index 5fbc91a4e..c4ce55658 100644 --- a/src/server/game/Entities/Vehicle/Vehicle.cpp +++ b/src/server/game/Entities/Vehicle/Vehicle.cpp @@ -445,6 +445,10 @@ bool Vehicle::AddPassenger(Unit* unit, int8 seatId) init.SetTransportEnter(); init.Launch(); + // Transfer threat from passenger to vehicle + for (auto const& [guid, threatRef] : unit->GetThreatMgr().GetThreatenedByMeList()) + threatRef->GetOwner()->GetThreatMgr().AddThreat(_me, threatRef->GetThreat(), nullptr, true, true); + if (_me->IsCreature()) { if (_me->ToCreature()->IsAIEnabled) diff --git a/src/server/game/Grids/Notifiers/GridNotifiers.h b/src/server/game/Grids/Notifiers/GridNotifiers.h index 34f0c09ad..e3271e305 100644 --- a/src/server/game/Grids/Notifiers/GridNotifiers.h +++ b/src/server/game/Grids/Notifiers/GridNotifiers.h @@ -1158,12 +1158,9 @@ namespace Acore if (!u->IsWithinLOSInMap(i_enemy)) return; - if (u->AI()) - { - u->SetNoCallForHelp(true); // avoid recursive call for help causing stack overflow - u->AI()->AttackStart(i_enemy); - u->SetNoCallForHelp(false); - } + u->SetNoCallForHelp(true); // avoid recursive call for help causing stack overflow + u->EngageWithTarget(i_enemy); + u->SetNoCallForHelp(false); } private: Unit* const i_funit; diff --git a/src/server/game/Handlers/MovementHandler.cpp b/src/server/game/Handlers/MovementHandler.cpp index 22bd4a012..296232a8a 100644 --- a/src/server/game/Handlers/MovementHandler.cpp +++ b/src/server/game/Handlers/MovementHandler.cpp @@ -135,8 +135,8 @@ void WorldSession::HandleMoveWorldportAck() _player->m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_ONTRANSPORT); } - if (!_player->getHostileRefMgr().IsEmpty()) - _player->getHostileRefMgr().deleteReferences(true); // pussywizard: multithreading crashfix + if (_player->GetThreatMgr().IsThreateningAnyone()) + _player->GetThreatMgr().RemoveMeFromThreatLists(); // pussywizard: multithreading crashfix CellCoord pair(Acore::ComputeCellCoord(GetPlayer()->GetPositionX(), GetPlayer()->GetPositionY())); Cell cell(pair); diff --git a/src/server/game/Maps/Map.cpp b/src/server/game/Maps/Map.cpp index 13b3ee3b5..63aad5ebb 100644 --- a/src/server/game/Maps/Map.cpp +++ b/src/server/game/Maps/Map.cpp @@ -702,7 +702,7 @@ void Map::RemovePlayerFromMap(Player* player, bool remove) { UpdatePlayerZoneStats(player->GetZoneId(), MAP_INVALID_ZONE); - player->getHostileRefMgr().deleteReferences(true); // pussywizard: multithreading crashfix + player->GetThreatMgr().RemoveMeFromThreatLists(); // pussywizard: multithreading crashfix player->RemoveFromWorld(); SendRemoveTransports(player); diff --git a/src/server/game/Movement/MovementGenerators/HomeMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/HomeMovementGenerator.cpp index d4928722a..29e234897 100644 --- a/src/server/game/Movement/MovementGenerators/HomeMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/HomeMovementGenerator.cpp @@ -16,6 +16,7 @@ */ #include "HomeMovementGenerator.h" +#include "CombatManager.h" #include "Creature.h" #include "CreatureAI.h" #include "DisableMgr.h" @@ -23,11 +24,13 @@ void HomeMovementGenerator::DoInitialize(Creature* owner) { + owner->GetCombatManager().SetEvadeState(EVADE_STATE_HOME); _setTargetLocation(owner); } void HomeMovementGenerator::DoFinalize(Creature* owner) { + owner->GetCombatManager().SetEvadeState(EVADE_STATE_NONE); owner->ClearUnitState(UNIT_STATE_EVADE); if (arrived) { diff --git a/src/server/game/Movement/MovementGenerators/TargetedMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/TargetedMovementGenerator.cpp index f67808892..457698f18 100644 --- a/src/server/game/Movement/MovementGenerators/TargetedMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/TargetedMovementGenerator.cpp @@ -255,10 +255,17 @@ bool ChaseMovementGenerator::DoUpdate(T* owner, uint32 time_diff) { i_recalculateTravel = false; i_path = nullptr; - if (cOwner) - cOwner->SetCannotReachTarget(); owner->ClearUnitState(UNIT_STATE_CHASE_MOVE); owner->SetInFront(target); + + if (cOwner) + { + if (cOwner->IsWithinMeleeRange(target)) + cOwner->SetCannotReachTarget(); + else + cOwner->SetCannotReachTarget(target->GetGUID()); + } + MovementInform(owner); } diff --git a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp index a8da788e5..47a83f5ae 100644 --- a/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/WaypointMovementGenerator.cpp @@ -414,7 +414,6 @@ void FlightPathMovementGenerator::DoFinalize(Player* player) if (player->m_taxi.empty()) { - player->getHostileRefMgr().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 @@ -443,7 +442,6 @@ void FlightPathMovementGenerator::DoReset(Player* player) if (player->pvpInfo.EndTimer) player->UpdatePvP(false, true); // PvP flag timer immediately ends when starting taxi - player->getHostileRefMgr().setOnlineOfflineState(false); player->AddUnitState(UNIT_STATE_IN_FLIGHT); player->SetUnitFlag(UNIT_FLAG_DISABLE_MOVE | UNIT_FLAG_TAXI_FLIGHT); diff --git a/src/server/game/Server/WorldSession.cpp b/src/server/game/Server/WorldSession.cpp index 237e79534..ef81cbb86 100644 --- a/src/server/game/Server/WorldSession.cpp +++ b/src/server/game/Server/WorldSession.cpp @@ -628,7 +628,7 @@ void WorldSession::LogoutPlayer(bool save) //FIXME: logout must be delayed in case lost connection with client in time of combat if (_player->GetDeathTimer()) { - _player->getHostileRefMgr().deleteReferences(true); + _player->GetThreatMgr().RemoveMeFromThreatLists(); _player->BuildPlayerRepop(); _player->RepopAtGraveyard(); } diff --git a/src/server/game/Spells/Auras/SpellAuraDefines.h b/src/server/game/Spells/Auras/SpellAuraDefines.h index 327cd044e..ff34ae883 100644 --- a/src/server/game/Spells/Auras/SpellAuraDefines.h +++ b/src/server/game/Spells/Auras/SpellAuraDefines.h @@ -281,7 +281,7 @@ enum AuraType SPELL_AURA_HASTE_RANGED = 218, SPELL_AURA_MOD_MANA_REGEN_FROM_STAT = 219, SPELL_AURA_MOD_RATING_FROM_STAT = 220, - SPELL_AURA_IGNORED = 221, + SPELL_AURA_MOD_DETAUNT = 221, SPELL_AURA_222 = 222, SPELL_AURA_RAID_PROC_FROM_CHARGE = 223, SPELL_AURA_224 = 224, diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index f8298d56d..e7c030cc3 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -283,7 +283,7 @@ pAuraEffectHandler AuraEffectHandler[TOTAL_AURAS] = &AuraEffect::HandleAuraModRangedHaste, //218 SPELL_AURA_HASTE_RANGED &AuraEffect::HandleModManaRegen, //219 SPELL_AURA_MOD_MANA_REGEN_FROM_STAT &AuraEffect::HandleModRatingFromStat, //220 SPELL_AURA_MOD_RATING_FROM_STAT - &AuraEffect::HandleNoImmediateEffect, //221 SPELL_AURA_IGNORED + &AuraEffect::HandleModDetaunt, //221 SPELL_AURA_MOD_DETAUNT &AuraEffect::HandleUnused, //222 unused (3.2.0) only for spell 44586 that not used in real spell cast &AuraEffect::HandleNoImmediateEffect, //223 SPELL_AURA_RAID_PROC_FROM_CHARGE &AuraEffect::HandleUnused, //224 unused (3.0.8a) @@ -2984,19 +2984,17 @@ void AuraEffect::HandleFeignDeath(AuraApplication const* aurApp, uint8 mode, boo } } - if (target->GetInstanceScript() && target->GetInstanceScript()->IsEncounterInProgress()) + for (auto& pair : target->GetThreatMgr().GetThreatenedByMeList()) + pair.second->ScaleThreat(0.0f); + + if (target->GetMap()->IsDungeon()) // feign death does not remove combat in dungeons { - // Xinef: replaced with CombatStop(false) target->AttackStop(); - target->RemoveAllAttackers(); - target->getHostileRefMgr().addThreatPercent(-100); - target->ToPlayer()->SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel + if (Player* targetPlayer = target->ToPlayer()) + targetPlayer->SendAttackSwingCancelAttack(); } else - { - target->CombatStop(); - target->getHostileRefMgr().deleteReferences(); - } + target->CombatStop(false, false); target->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_IMMUNE_OR_LOST_SELECTION); @@ -3551,26 +3549,16 @@ void AuraEffect::HandleForceMoveForward(AuraApplication const* aurApp, uint8 mod /*** THREAT ***/ /****************************/ -void AuraEffect::HandleModThreat(AuraApplication const* aurApp, uint8 mode, bool apply) const +void AuraEffect::HandleModThreat(AuraApplication const* aurApp, uint8 mode, bool /*apply*/) const { if (!(mode & AURA_EFFECT_HANDLE_CHANGE_AMOUNT_MASK)) return; Unit* target = aurApp->GetTarget(); - for (uint8 i = 0; i < MAX_SPELL_SCHOOL; ++i) - if (GetMiscValue() & (1 << i)) - { - if (apply) - AddPct(target->m_threatModifier[i], GetAmount()); - else - { - float amount = target->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_THREAT, 1 << i); - target->m_threatModifier[i] = amount; - } - } + target->GetThreatMgr().UpdateMySpellSchoolModifiers(); } -void AuraEffect::HandleAuraModTotalThreat(AuraApplication const* aurApp, uint8 mode, bool apply) const +void AuraEffect::HandleAuraModTotalThreat(AuraApplication const* aurApp, uint8 mode, bool /*apply*/) const { if (!(mode & AURA_EFFECT_HANDLE_CHANGE_AMOUNT_MASK)) return; @@ -3582,10 +3570,10 @@ void AuraEffect::HandleAuraModTotalThreat(AuraApplication const* aurApp, uint8 m Unit* caster = GetCaster(); if (caster && caster->IsAlive()) - target->getHostileRefMgr().addTempThreat((float)GetAmount(), apply); + caster->GetThreatMgr().UpdateMyTempModifiers(); } -void AuraEffect::HandleModTaunt(AuraApplication const* aurApp, uint8 mode, bool apply) const +void AuraEffect::HandleModTaunt(AuraApplication const* aurApp, uint8 mode, bool /*apply*/) const { if (!(mode & AURA_EFFECT_HANDLE_REAL)) return; @@ -3595,17 +3583,21 @@ void AuraEffect::HandleModTaunt(AuraApplication const* aurApp, uint8 mode, bool if (!target->IsAlive() || !target->CanHaveThreatList()) return; - Unit* caster = GetCaster(); - if (!caster || !caster->IsAlive()) + target->GetThreatMgr().TauntUpdate(); +} + +void AuraEffect::HandleModDetaunt(AuraApplication const* aurApp, uint8 mode, bool /*apply*/) const +{ + if (!(mode & AURA_EFFECT_HANDLE_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->GetThreatMgr().TauntUpdate(); } /*****************************/ @@ -3620,6 +3612,8 @@ void AuraEffect::HandleModConfuse(AuraApplication const* aurApp, uint8 mode, boo Unit* target = aurApp->GetTarget(); target->SetControlled(apply, UNIT_STATE_CONFUSED); + if (apply) + target->GetThreatMgr().EvaluateSuppressed(); } void AuraEffect::HandleModFear(AuraApplication const* aurApp, uint8 mode, bool apply) const @@ -3640,6 +3634,8 @@ void AuraEffect::HandleAuraModStun(AuraApplication const* aurApp, uint8 mode, bo Unit* target = aurApp->GetTarget(); target->SetControlled(apply, UNIT_STATE_STUNNED); + if (apply) + target->GetThreatMgr().EvaluateSuppressed(); } void AuraEffect::HandleAuraModRoot(AuraApplication const* aurApp, uint8 mode, bool apply) const @@ -4359,6 +4355,9 @@ void AuraEffect::HandleAuraModSchoolImmunity(AuraApplication const* aurApp, uint target->ApplySpellImmune(GetId(), IMMUNITY_SCHOOL, GetMiscValue(), (apply)); + if (apply) + target->GetThreatMgr().EvaluateSuppressed(); + if (GetSpellInfo()->Mechanic == MECHANIC_BANISH) { if (apply) @@ -4417,6 +4416,9 @@ void AuraEffect::HandleAuraModDmgImmunity(AuraApplication const* aurApp, uint8 m Unit* target = aurApp->GetTarget(); target->ApplySpellImmune(GetId(), IMMUNITY_DAMAGE, GetMiscValue(), apply); + + if (apply) + target->GetThreatMgr().EvaluateSuppressed(); } void AuraEffect::HandleAuraModDispelImmunity(AuraApplication const* aurApp, uint8 mode, bool apply) const @@ -6895,7 +6897,7 @@ void AuraEffect::HandlePeriodicHealthLeechAuraTick(Unit* target, Unit* caster) c float threat = float(caster->HealBySpell(healInfo)) * 0.5f; if (caster->IsClass(CLASS_PALADIN)) threat *= 0.5f; - caster->getHostileRefMgr().threatAssist(caster, threat, GetSpellInfo()); + caster->GetThreatMgr().ForwardThreatForAssistingMe(caster, threat, GetSpellInfo()); } void AuraEffect::HandlePeriodicHealthFunnelAuraTick(Unit* target, Unit* caster) const @@ -7030,8 +7032,7 @@ void AuraEffect::HandlePeriodicHealAurasTick(Unit* target, Unit* caster) const float threat = float(gain) * 0.5f; if (caster->IsClass(CLASS_PALADIN)) threat *= 0.5f; - - target->getHostileRefMgr().threatAssist(caster, threat, GetSpellInfo()); + target->GetThreatMgr().ForwardThreatForAssistingMe(caster, threat, GetSpellInfo()); } bool haveCastItem = GetBase()->GetCastItemGUID(); @@ -7117,8 +7118,6 @@ void AuraEffect::HandlePeriodicManaLeechAuraTick(Unit* target, Unit* caster) con target->AddThreat(caster, float(gainedAmount) * 0.5f, GetSpellInfo()->GetSchoolMask(), GetSpellInfo()); } - target->AddThreat(caster, float(gainedAmount) * 0.5f, GetSpellInfo()->GetSchoolMask(), GetSpellInfo()); - // remove CC auras target->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TAKE_DAMAGE); @@ -7169,7 +7168,7 @@ void AuraEffect::HandleObsModPowerAuraTick(Unit* target, Unit* caster) const int32 gain = target->ModifyPower(PowerType, amount); if (caster) - target->getHostileRefMgr().threatAssist(caster, float(gain) * 0.5f, GetSpellInfo()); + target->GetThreatMgr().ForwardThreatForAssistingMe(caster, float(gain) * 0.5f, GetSpellInfo(), true); } void AuraEffect::HandlePeriodicEnergizeAuraTick(Unit* target, Unit* caster) const @@ -7203,7 +7202,7 @@ void AuraEffect::HandlePeriodicEnergizeAuraTick(Unit* target, Unit* caster) cons int32 gain = target->ModifyPower(PowerType, amount); if (caster) - target->getHostileRefMgr().threatAssist(caster, float(gain) * 0.5f, GetSpellInfo()); + target->GetThreatMgr().ForwardThreatForAssistingMe(caster, float(gain) * 0.5f, GetSpellInfo(), true); } void AuraEffect::HandlePeriodicPowerBurnAuraTick(Unit* target, Unit* caster) const diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.h b/src/server/game/Spells/Auras/SpellAuraEffects.h index a4c8640c9..5acdedda4 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.h +++ b/src/server/game/Spells/Auras/SpellAuraEffects.h @@ -206,6 +206,7 @@ public: void HandleModThreat(AuraApplication const* aurApp, uint8 mode, bool apply) const; void HandleAuraModTotalThreat(AuraApplication const* aurApp, uint8 mode, bool apply) const; void HandleModTaunt(AuraApplication const* aurApp, uint8 mode, bool apply) const; + void HandleModDetaunt(AuraApplication const* aurApp, uint8 mode, bool apply) const; // control void HandleModConfuse(AuraApplication const* aurApp, uint8 mode, bool apply) const; void HandleModFear(AuraApplication const* aurApp, uint8 mode, bool apply) const; diff --git a/src/server/game/Spells/Auras/SpellAuras.h b/src/server/game/Spells/Auras/SpellAuras.h index 87b77ff25..0c6639584 100644 --- a/src/server/game/Spells/Auras/SpellAuras.h +++ b/src/server/game/Spells/Auras/SpellAuras.h @@ -237,6 +237,12 @@ public: AuraScript* GetScriptByName(std::string const& scriptName) const; + template + Script* GetScript(std::string const& scriptName) const + { + return dynamic_cast(GetScriptByName(scriptName)); + } + std::list m_loadedScripts; virtual std::string GetDebugInfo() const; diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 2018bb89b..63e2c5c0b 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -2751,8 +2751,7 @@ void Spell::DoAllEffectOnTarget(TargetInfo* target) float threat = float(gain) * 0.5f; if (caster->IsClass(CLASS_PALADIN)) threat *= 0.5f; - - unitTarget->getHostileRefMgr().threatAssist(caster, threat, m_spellInfo); + unitTarget->GetThreatMgr().ForwardThreatForAssistingMe(caster, threat, m_spellInfo); m_healing = gain; // Xinef: if heal actually healed something, add no overheal flag @@ -2877,8 +2876,7 @@ void Spell::DoAllEffectOnTarget(TargetInfo* target) if (missInfo == SPELL_MISS_RESIST && m_spellInfo->HasAttribute(SPELL_ATTR0_CU_PICKPOCKET) && unitTarget->IsCreature() && m_caster) { m_caster->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TALK); - if (unitTarget->ToCreature()->IsAIEnabled) - unitTarget->ToCreature()->AI()->AttackStart(m_caster); + unitTarget->ToCreature()->EngageWithTarget(m_caster); } } @@ -2887,19 +2885,7 @@ void Spell::DoAllEffectOnTarget(TargetInfo* target) if (missInfo != SPELL_MISS_EVADE && !m_caster->IsFriendlyTo(effectUnit) && (!m_spellInfo->IsPositive() || m_spellInfo->HasEffect(SPELL_EFFECT_DISPEL))) { if (!m_triggeredByAuraSpell.spellInfo || m_damage || (!(m_triggeredByAuraSpell.spellInfo->Effects[m_triggeredByAuraSpell.effectIndex].TriggerSpell == m_spellInfo->Id) && !(m_triggeredByAuraSpell.spellInfo->IsAuraEffectEqual(m_spellInfo)))) - m_caster->CombatStart(effectUnit, !(m_spellInfo->AttributesEx3 & SPELL_ATTR3_SUPPRESS_TARGET_PROCS)); - - // Patch 3.0.8: All player spells which cause a creature to become aggressive to you will now also immediately cause the creature to be tapped. - if (effectUnit->IsInCombatWith(m_caster)) - { - if (Creature* creature = effectUnit->ToCreature()) - { - if (!creature->hasLootRecipient() && m_caster->IsPlayer()) - { - creature->SetLootRecipient(m_caster); - } - } - } + m_caster->AtTargetAttacked(effectUnit, !(m_spellInfo->HasAttribute(SPELL_ATTR1_NO_THREAT) || m_spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS))); // Unsure if there are more spells that are not supposed to stop enemy from // regenerating HP from food, so for now it stays as an ID. @@ -3036,10 +3022,11 @@ SpellMissInfo Spell::DoSpellHitOnUnit(Unit* unit, uint32 effectMask, bool scaleA } // xinef: triggered spells should not prolong combat - if (unit->IsInCombat() && !m_spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS) && !m_triggeredByAuraSpell) + if (m_originalCaster && unit->IsInCombat() && !m_spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS) && !m_triggeredByAuraSpell) { - m_caster->SetInCombatState(unit->GetCombatTimer() > 0, unit); - unit->getHostileRefMgr().threatAssist(m_caster, 0.0f); + if (m_originalCaster->HasUnitFlag(UNIT_FLAG_PLAYER_CONTROLLED)) + m_originalCaster->GetCombatManager().InheritCombatStatesFrom(unit); + unit->GetThreatMgr().ForwardThreatForAssistingMe(m_originalCaster, 0.0f, nullptr, true); } } } @@ -4048,12 +4035,6 @@ void Spell::_cast(bool skipCheck) if (target->IsCreature()) m_caster->CastSpell(target, 32747, true); - // xinef: start combat at cast for delayed spells, only for explicit target - if (Unit* target = m_targets.GetUnitTarget()) - if (m_caster->IsPlayer() || (m_caster->IsPet() && m_caster->IsControlledByPlayer())) - if (GetDelayMoment() > 0 && !m_caster->IsFriendlyTo(target) && !m_spellInfo->HasAura(SPELL_AURA_BIND_SIGHT) && (!m_spellInfo->IsPositive() || m_spellInfo->HasEffect(SPELL_EFFECT_DISPEL))) - m_caster->CombatStartOnCast(target, !m_spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS), GetDelayMoment() + 500); // xinef: increase this time so we dont leave and enter combat in a moment - if (m_caster->IsPlayer()) if (m_caster->ToPlayer()->GetCommandStatus(CHEAT_COOLDOWN)) m_caster->ToPlayer()->RemoveSpellCooldown(m_spellInfo->Id, true); @@ -5558,11 +5539,13 @@ void Spell::HandleThreatSpells() if (m_spellInfo->HasAttribute(SPELL_ATTR1_NO_THREAT) || m_spellInfo->HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS)) return; + Unit* unitCaster = m_originalCaster ? m_originalCaster : m_caster; + float threat = 0.0f; if (SpellThreatEntry const* threatEntry = sSpellMgr->GetSpellThreatEntry(m_spellInfo->Id)) { if (threatEntry->apPctMod != 0.0f) - threat += threatEntry->apPctMod * m_caster->GetTotalAttackPowerValue(BASE_ATTACK); + threat += threatEntry->apPctMod * unitCaster->GetTotalAttackPowerValue(BASE_ATTACK); threat += threatEntry->flatMod; } @@ -5576,23 +5559,27 @@ void Spell::HandleThreatSpells() // since 2.0.1 threat from positive effects also is distributed among all targets, so the overall caused threat is at most the defined bonus threat /= m_UniqueTargetInfo.size(); - for (std::list::iterator ihit = m_UniqueTargetInfo.begin(); ihit != m_UniqueTargetInfo.end(); ++ihit) + for (auto& ihit : m_UniqueTargetInfo) { float threatToAdd = threat; - if (ihit->missCondition != SPELL_MISS_NONE) + if (ihit.missCondition != SPELL_MISS_NONE) threatToAdd = 0.0f; - Unit* target = ObjectAccessor::GetUnit(*m_caster, ihit->targetGUID); + Unit* target = ObjectAccessor::GetUnit(*unitCaster, ihit.targetGUID); if (!target) continue; - bool IsFriendly = m_caster->IsFriendlyTo(target); // positive spells distribute threat among all units that are in combat with target, like healing - if (m_spellInfo->_IsPositiveSpell() && IsFriendly) - target->getHostileRefMgr().threatAssist(m_caster, threatToAdd, m_spellInfo); + if (m_spellInfo->_IsPositiveSpell()) + target->GetThreatMgr().ForwardThreatForAssistingMe(unitCaster, threatToAdd, m_spellInfo); // for negative spells threat gets distributed among affected targets - else if (!m_spellInfo->_IsPositiveSpell() && !IsFriendly && target->CanHaveThreatList()) - target->AddThreat(m_caster, threatToAdd, m_spellInfo->GetSchoolMask(), m_spellInfo); + // ignoreModifiers=true because flat SpellThreatEntry threat should not have modifiers applied twice + else + { + if (!target->CanHaveThreatList()) + continue; + target->GetThreatMgr().AddThreat(unitCaster, threatToAdd, m_spellInfo, true); + } } LOG_DEBUG("spells.aura", "Spell {}, added an additional {} threat for {} {} target(s)", m_spellInfo->Id, threat, m_spellInfo->_IsPositiveSpell() ? "assisting" : "harming", uint32(m_UniqueTargetInfo.size())); } @@ -5772,8 +5759,7 @@ SpellCastResult Spell::CheckCast(bool strict) if (Unit* victim = member->GetVictim()) if (victim->IsInCombat() && m_caster->GetDistance(victim) < m_caster->GetVisibilityRange()) { - m_caster->CombatStart(victim); - victim->AddThreat(m_caster, 1.0f); + m_caster->EngageWithTarget(victim); break; } return SPELL_FAILED_TARGET_CANNOT_BE_RESURRECTED; diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp index 5e4bc73c6..7c47c9d41 100644 --- a/src/server/game/Spells/SpellEffects.cpp +++ b/src/server/game/Spells/SpellEffects.cpp @@ -2652,7 +2652,7 @@ void Spell::EffectDispel(SpellEffIndex effIndex) // put in combat if (unitTarget->IsFriendlyTo(m_caster)) - unitTarget->getHostileRefMgr().threatAssist(m_caster, 0.0f, m_spellInfo); + unitTarget->GetThreatMgr().ForwardThreatForAssistingMe(m_caster, 0.0f, m_spellInfo); if (success_list.empty()) return; @@ -3299,33 +3299,25 @@ void Spell::EffectTaunt(SpellEffIndex /*effIndex*/) if (!unitTarget) return; - // xinef: Hand of Reckoning, cast before checing canhavethreatlist. fixes damage against pets + // xinef: Hand of Reckoning, cast before checking canhavethreatlist. fixes damage against pets if (m_spellInfo->Id == 62124 && unitTarget->GetVictim() != m_caster) - { m_caster->CastSpell(unitTarget, 67485, true); - unitTarget->CombatStart(m_caster); - } - // 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->CanHaveThreatList() || (unitTarget->GetVictim() == m_caster && !m_spellInfo->HasAura(SPELL_AURA_MOD_TAUNT))) + if (!unitTarget->CanHaveThreatList()) { SendCastResult(SPELL_FAILED_DONT_REPORT); return; } - if (!unitTarget->GetThreatMgr().GetOnlineContainer().empty()) + ThreatManager& mgr = unitTarget->GetThreatMgr(); + 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->GetThreatMgr().GetThreat(m_caster); - float topThreat = unitTarget->GetThreatMgr().GetOnlineContainer().getMostHated()->GetThreat(); - if (topThreat > myThreat) - unitTarget->GetThreatMgr().DoAddThreat(m_caster, topThreat - myThreat); - - //Set aggro victim to caster - if (HostileReference* forcedVictim = unitTarget->GetThreatMgr().GetOnlineContainer().getReferenceByTarget(m_caster)) - unitTarget->GetThreatMgr().setCurrentVictim(forcedVictim); + SendCastResult(SPELL_FAILED_DONT_REPORT); + return; } + + if (!mgr.IsThreatListEmpty()) + mgr.MatchUnitThreatToHighestThreat(m_caster); } void Spell::EffectWeaponDmg(SpellEffIndex effIndex) @@ -3677,7 +3669,8 @@ void Spell::EffectThreat(SpellEffIndex /*effIndex*/) if (!unitTarget->CanHaveThreatList() || m_caster->IsFriendlyTo(unitTarget)) return; - unitTarget->AddThreat(m_caster, float(damage)); + // SPELL_EFFECT_THREAT adds flat threat that should not be modified by threat reduction + unitTarget->GetThreatMgr().AddThreat(m_caster, float(damage), m_spellInfo, true); } void Spell::EffectHealMaxHealth(SpellEffIndex /*effIndex*/) @@ -4002,24 +3995,18 @@ void Spell::EffectSanctuary(SpellEffIndex /*effIndex*/) if (!unitTarget) return; - if (unitTarget->GetInstanceScript() && unitTarget->GetInstanceScript()->IsEncounterInProgress()) + unitTarget->GetThreatMgr().EvaluateSuppressed(); + + if (unitTarget->IsPlayer() && !unitTarget->GetMap()->IsDungeon()) { - unitTarget->getHostileRefMgr().UpdateVisibility(true); - // Xinef: replaced with CombatStop(false) - unitTarget->AttackStop(); - unitTarget->RemoveAllAttackers(); - - // Night Elf: Shadowmeld only resets threat temporarily - if (m_spellInfo->Id != 59646) - unitTarget->getHostileRefMgr().addThreatPercent(-100); - - if (unitTarget->IsPlayer()) - unitTarget->ToPlayer()->SendAttackSwingCancelAttack(); // melee and ranged forced attack cancel + // stop all pve combat for players outside dungeons, suppress pvp combat + unitTarget->CombatStop(false, false); } else { - unitTarget->getHostileRefMgr().UpdateVisibility(m_spellInfo->Id == 59646); // Night Elf: Shadowmeld - unitTarget->CombatStop(true); + // in dungeons (or for nonplayers), reset this unit on all enemies' threat lists + for (auto const& pair : unitTarget->GetThreatMgr().GetThreatenedByMeList()) + pair.second->ScaleThreat(0.0f); } UnitList targets; @@ -5185,7 +5172,7 @@ void Spell::EffectDispelMechanic(SpellEffIndex effIndex) // put in combat if (unitTarget->IsFriendlyTo(m_caster)) - unitTarget->getHostileRefMgr().threatAssist(m_caster, 0.0f, m_spellInfo); + unitTarget->GetThreatMgr().ForwardThreatForAssistingMe(m_caster, 0.0f, m_spellInfo); } void Spell::EffectResurrectPet(SpellEffIndex /*effIndex*/) @@ -5903,7 +5890,7 @@ void Spell::EffectRedirectThreat(SpellEffIndex /*effIndex*/) return; if (unitTarget) - m_caster->SetRedirectThreat(unitTarget->GetGUID(), uint32(damage)); + m_caster->GetThreatMgr().RegisterRedirectThreat(m_spellInfo->Id, unitTarget->GetGUID(), uint32(damage)); } void Spell::EffectGameObjectDamage(SpellEffIndex /*effIndex*/) diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index f075b59d4..86eb2d4dd 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -1286,6 +1286,11 @@ bool SpellInfo::IsAutoRepeatRangedSpell() const return AttributesEx2 & SPELL_ATTR2_AUTO_REPEAT; } +bool SpellInfo::HasInitialAggro() const +{ + return !(HasAttribute(SPELL_ATTR1_NO_THREAT) || HasAttribute(SPELL_ATTR3_SUPPRESS_TARGET_PROCS)); +} + bool SpellInfo::IsAffected(uint32 familyName, flag96 const& familyFlags) const { if (!familyName) diff --git a/src/server/game/Spells/SpellInfo.h b/src/server/game/Spells/SpellInfo.h index 00462fe66..70f0097f7 100644 --- a/src/server/game/Spells/SpellInfo.h +++ b/src/server/game/Spells/SpellInfo.h @@ -466,6 +466,7 @@ public: bool IsBreakingStealth() const; bool IsRangedWeaponSpell() const; bool IsAutoRepeatRangedSpell() const; + bool HasInitialAggro() const; [[nodiscard]] bool IsAffected(uint32 familyName, flag96 const& familyFlags) const; diff --git a/src/server/scripts/Commands/cs_debug.cpp b/src/server/scripts/Commands/cs_debug.cpp index 5c440f8a8..cd1fc6702 100644 --- a/src/server/scripts/Commands/cs_debug.cpp +++ b/src/server/scripts/Commands/cs_debug.cpp @@ -27,6 +27,7 @@ #include "Log.h" #include "M2Stores.h" #include "MapMgr.h" +#include "ObjectAccessor.h" #include "ObjectMgr.h" #include "PoolMgr.h" #include "ScriptMgr.h" @@ -70,6 +71,8 @@ public: { { "setbit", HandleDebugSet32BitCommand, SEC_ADMINISTRATOR, Console::No }, { "threat", HandleDebugThreatListCommand, SEC_ADMINISTRATOR, Console::No }, + { "threatinfo", HandleDebugThreatInfoCommand, SEC_ADMINISTRATOR, Console::No }, + { "combat", HandleDebugCombatListCommand, SEC_ADMINISTRATOR, Console::No }, { "hostile", HandleDebugHostileRefListCommand, SEC_ADMINISTRATOR, Console::No }, { "anim", HandleDebugAnimCommand, SEC_ADMINISTRATOR, Console::No }, { "arena", HandleDebugArenaCommand, SEC_ADMINISTRATOR, Console::No }, @@ -816,43 +819,182 @@ public: static bool HandleDebugThreatListCommand(ChatHandler* handler) { - Creature* target = handler->getSelectedCreature(); - if (!target || target->IsTotem() || target->IsPet()) - return false; + Unit* target = handler->getSelectedUnit(); + if (!target) + target = handler->GetPlayer(); + + ThreatManager& mgr = target->GetThreatMgr(); + if (!target->IsAlive()) + { + handler->PSendSysMessage("{} ({}) is not alive.{}", target->GetName(), target->GetGUID().ToString(), target->IsEngaged() ? " (It is, however, engaged. Huh?)" : ""); + return true; + } - auto const& threatList = target->GetThreatMgr().GetThreatList(); - ThreatContainer::StorageType::const_iterator itr; uint32 count = 0; - - handler->PSendSysMessage("Threat list of {} ({})", target->GetName(), target->GetGUID().ToString()); - - for (itr = threatList.begin(); itr != threatList.end(); ++itr) + auto const& threatenedByMe = target->GetThreatMgr().GetThreatenedByMeList(); + if (threatenedByMe.empty()) + handler->PSendSysMessage("{} ({}) does not threaten any units.", target->GetName(), target->GetGUID().ToString()); + else { - Unit* unit = (*itr)->getTarget(); - if (!unit) + handler->PSendSysMessage("List of units threatened by {} ({})", target->GetName(), target->GetGUID().ToString()); + for (auto const& pair : threatenedByMe) { - handler->PSendSysMessage(" {}. No Unit - threat {}", ++count, (*itr)->GetThreat()); - continue; + Unit* unit = pair.second->GetOwner(); + handler->PSendSysMessage(" {}. {} ({}, SpawnID {}) - threat {}", ++count, unit->GetName(), unit->GetGUID().ToString(), unit->IsCreature() ? unit->ToCreature()->GetSpawnId() : 0, pair.second->GetThreat()); } - - handler->PSendSysMessage(" {}. {} ({}) - threat {}", ++count, unit->GetName(), unit->GetGUID().ToString(), (*itr)->GetThreat()); + handler->SendSysMessage("End of threatened-by-me list."); } - auto const& threatList2 = target->GetThreatMgr().GetOfflineThreatList(); - for (itr = threatList2.begin(); itr != threatList2.end(); ++itr) + if (mgr.CanHaveThreatList()) { - Unit* unit = (*itr)->getTarget(); - if (!unit) + if (!mgr.IsThreatListEmpty(true)) { - handler->PSendSysMessage(" {}. [offline] No Unit - threat {}", ++count, (*itr)->GetThreat()); - continue; - } + if (target->IsEngaged()) + handler->PSendSysMessage("Threat list of {} ({}, SpawnID {}):", target->GetName(), target->GetGUID().ToString(), target->IsCreature() ? target->ToCreature()->GetSpawnId() : 0); + else + handler->PSendSysMessage("{} ({}, SpawnID {}) is not engaged, but still has a threat list? Well, here it is:", target->GetName(), target->GetGUID().ToString(), target->IsCreature() ? target->ToCreature()->GetSpawnId() : 0); - handler->PSendSysMessage(" {}. [offline] {} ({}) - threat {}", ++count, unit->GetName(), unit->GetGUID().ToString(), (*itr)->GetThreat()); + count = 0; + Unit* fixateVictim = mgr.GetFixateTarget(); + for (ThreatReference const* ref : mgr.GetSortedThreatList()) + { + Unit* unit = ref->GetVictim(); + char const* onlineStr; + switch (ref->GetOnlineState()) + { + case ThreatReference::ONLINE_STATE_SUPPRESSED: + onlineStr = " [SUPPRESSED]"; + break; + case ThreatReference::ONLINE_STATE_OFFLINE: + onlineStr = " [OFFLINE]"; + break; + default: + onlineStr = ""; + } + char const* tauntStr; + if (unit == fixateVictim) + tauntStr = " [FIXATE]"; + else + switch (ref->GetTauntState()) + { + case ThreatReference::TAUNT_STATE_TAUNT: + tauntStr = " [TAUNT]"; + break; + case ThreatReference::TAUNT_STATE_DETAUNT: + tauntStr = " [DETAUNT]"; + break; + default: + tauntStr = ""; + } + handler->PSendSysMessage(" {}. {} ({}) - threat {}{}{}", ++count, unit->GetName(), unit->GetGUID().ToString(), ref->GetThreat(), tauntStr, onlineStr); + } + handler->SendSysMessage("End of threat list."); + } + else if (!target->IsEngaged()) + handler->PSendSysMessage("{} ({}, SpawnID {}) is not currently engaged.", target->GetName(), target->GetGUID().ToString(), target->IsCreature() ? target->ToCreature()->GetSpawnId() : 0); + else + handler->PSendSysMessage("{} ({}, SpawnID {}) seems to be engaged, but does not have a threat list??", target->GetName(), target->GetGUID().ToString(), target->IsCreature() ? target->ToCreature()->GetSpawnId() : 0); + } + else if (target->IsEngaged()) + handler->PSendSysMessage("{} ({}) is currently engaged. (This unit cannot have a threat list.)", target->GetName(), target->GetGUID().ToString()); + else + handler->PSendSysMessage("{} ({}) is not currently engaged. (This unit cannot have a threat list.)", target->GetName(), target->GetGUID().ToString()); + + return true; + } + + static bool HandleDebugThreatInfoCommand(ChatHandler* handler) + { + Unit* target = handler->getSelectedUnit(); + if (!target) + { + handler->SendSysMessage(LANG_SELECT_CHAR_OR_CREATURE); + handler->SetSentErrorMessage(true); + return false; } - handler->SendSysMessage("End of threat list."); + handler->PSendSysMessage("Threat info for {} ({}):", target->GetName(), target->GetGUID().ToString()); + ThreatManager const& mgr = target->GetThreatMgr(); + + // _singleSchoolModifiers + { + auto& mods = mgr._singleSchoolModifiers; + handler->SendSysMessage(" - Single-school threat modifiers:"); + handler->PSendSysMessage(" |-- Physical: {:.2f}%", mods[SPELL_SCHOOL_NORMAL] * 100.0f); + handler->PSendSysMessage(" |-- Holy : {:.2f}%", mods[SPELL_SCHOOL_HOLY] * 100.0f); + handler->PSendSysMessage(" |-- Fire : {:.2f}%", mods[SPELL_SCHOOL_FIRE] * 100.0f); + handler->PSendSysMessage(" |-- Nature : {:.2f}%", mods[SPELL_SCHOOL_NATURE] * 100.0f); + handler->PSendSysMessage(" |-- Frost : {:.2f}%", mods[SPELL_SCHOOL_FROST] * 100.0f); + handler->PSendSysMessage(" |-- Shadow : {:.2f}%", mods[SPELL_SCHOOL_SHADOW] * 100.0f); + handler->PSendSysMessage(" |-- Arcane : {:.2f}%", mods[SPELL_SCHOOL_ARCANE] * 100.0f); + } + + // _multiSchoolModifiers + { + auto& mods = mgr._multiSchoolModifiers; + handler->PSendSysMessage(" - Multi-school threat modifiers ({} entries):", mods.size()); + for (auto const& pair : mods) + handler->PSendSysMessage(" |-- Mask 0x{:x}: {:.2f}%", uint32(pair.first), pair.second * 100.0f); + } + + // _redirectInfo + { + auto const& redirectInfo = mgr._redirectInfo; + if (redirectInfo.empty()) + handler->SendSysMessage(" - No redirects being applied"); + else + { + handler->PSendSysMessage(" - {:02} redirects being applied:", redirectInfo.size()); + for (auto const& pair : redirectInfo) + { + Unit* unit = ObjectAccessor::GetUnit(*target, pair.first); + handler->PSendSysMessage(" |-- {:02}% to {}", pair.second, unit ? unit->GetName() : pair.first.ToString()); + } + } + } + + // _redirectRegistry + { + auto const& redirectRegistry = mgr._redirectRegistry; + if (redirectRegistry.empty()) + handler->SendSysMessage(" - No redirects are registered"); + else + { + handler->PSendSysMessage(" - {:02} spells may have redirects registered", redirectRegistry.size()); + for (auto const& outerPair : redirectRegistry) + { + SpellInfo const* const spell = sSpellMgr->GetSpellInfo(outerPair.first); + handler->PSendSysMessage(" |-- #{:06} {} ({} entries):", outerPair.first, spell ? spell->SpellName[0] : "", outerPair.second.size()); + for (auto const& innerPair : outerPair.second) + { + Unit* unit = ObjectAccessor::GetUnit(*target, innerPair.first); + handler->PSendSysMessage(" |-- {:02}% to {}", innerPair.second, unit ? unit->GetName() : innerPair.first.ToString()); + } + } + } + } + + return true; + } + + static bool HandleDebugCombatListCommand(ChatHandler* handler) + { + Unit* target = handler->getSelectedUnit(); + if (!target) + target = handler->GetPlayer(); + + handler->PSendSysMessage("Combat refs: (Combat state: {} | Manager state: {})", target->IsInCombat(), target->GetCombatManager().HasCombat()); + for (auto const& ref : target->GetCombatManager().GetPvPCombatRefs()) + { + Unit* unit = ref.second->GetOther(target); + handler->PSendSysMessage("[PvP] {} (SpawnID {})", unit->GetName(), unit->IsCreature() ? unit->ToCreature()->GetSpawnId() : 0); + } + for (auto const& ref : target->GetCombatManager().GetPvECombatRefs()) + { + Unit* unit = ref.second->GetOther(target); + handler->PSendSysMessage("[PvE] {} (SpawnID {})", unit->GetName(), unit->IsCreature() ? unit->ToCreature()->GetSpawnId() : 0); + } return true; } @@ -862,27 +1004,32 @@ public: if (!target) target = handler->GetSession()->GetPlayer(); - HostileReference* ref = target->getHostileRefMgr().getFirst(); uint32 count = 0; - handler->PSendSysMessage("Hostile reference list of {} ({})", target->GetName(), target->GetGUID().ToString()); + handler->PSendSysMessage("Threatened by me list of {} ({})", target->GetName(), target->GetGUID().ToString()); - while (ref) + for (auto const& pair : target->GetThreatMgr().GetThreatenedByMeList()) { - if (Unit* unit = ref->GetSource()->GetOwner()) + ThreatReference const* ref = pair.second; + Creature* owner = ref->GetOwner(); + if (owner) { - handler->PSendSysMessage(" {}. {} {} ({}) - threat {}", ++count, (ref->IsOnline() ? "" : "[offline]"), - unit->GetName(), unit->GetGUID().ToString(), ref->GetThreat()); + std::string stateStr = ""; + if (ref->IsOffline()) + stateStr = "[offline] "; + else if (ref->IsSuppressed()) + stateStr = "[suppressed] "; + + handler->PSendSysMessage(" {}. {}{} ({}) - threat {}", ++count, stateStr, + owner->GetName(), owner->GetGUID().ToString(), ref->GetThreat()); } else { handler->PSendSysMessage(" {}. No Owner - threat {}", ++count, ref->GetThreat()); } - - ref = ref->next(); } - handler->SendSysMessage("End of hostile reference list."); + handler->SendSysMessage("End of threatened by me list."); return true; } diff --git a/src/server/scripts/Commands/cs_misc.cpp b/src/server/scripts/Commands/cs_misc.cpp index 559b6eb67..cd543488a 100644 --- a/src/server/scripts/Commands/cs_misc.cpp +++ b/src/server/scripts/Commands/cs_misc.cpp @@ -2904,7 +2904,7 @@ public: } playerTarget->CombatStop(); - playerTarget->getHostileRefMgr().deleteReferences(); + playerTarget->GetThreatMgr().RemoveMeFromThreatLists(); return true; } diff --git a/src/server/scripts/Commands/cs_spellinfo.cpp b/src/server/scripts/Commands/cs_spellinfo.cpp index 46bf55b2e..4b7b562df 100644 --- a/src/server/scripts/Commands/cs_spellinfo.cpp +++ b/src/server/scripts/Commands/cs_spellinfo.cpp @@ -446,7 +446,7 @@ public: case SPELL_AURA_HASTE_RANGED: return "SPELL_AURA_HASTE_RANGED"; case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT: return "SPELL_AURA_MOD_MANA_REGEN_FROM_STAT"; case SPELL_AURA_MOD_RATING_FROM_STAT: return "SPELL_AURA_MOD_RATING_FROM_STAT"; - case SPELL_AURA_IGNORED: return "SPELL_AURA_IGNORED"; + case SPELL_AURA_MOD_DETAUNT: return "SPELL_AURA_MOD_DETAUNT"; case SPELL_AURA_222: return "SPELL_AURA_222"; case SPELL_AURA_RAID_PROC_FROM_CHARGE: return "SPELL_AURA_RAID_PROC_FROM_CHARGE"; case SPELL_AURA_224: return "SPELL_AURA_224"; diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_nefarian.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_nefarian.cpp index 4407416df..55d3d818e 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_nefarian.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_nefarian.cpp @@ -672,11 +672,12 @@ struct boss_nefarian : public BossAI case EVENT_CLASSCALL: if (classesPresent.empty()) { - for (auto& ref : me->GetThreatMgr().GetThreatList()) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (ref->getTarget() && ref->getTarget()->IsPlayer()) + if (Unit* victim = ref->GetVictim()) { - classesPresent.insert(ref->getTarget()->getClass()); + if (victim->IsPlayer()) + classesPresent.insert(victim->getClass()); } } } diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_razorgore.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_razorgore.cpp index ffbfe4ca0..c17fd24f2 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_razorgore.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/BlackwingLair/boss_razorgore.cpp @@ -172,7 +172,9 @@ public: if (Unit* charmer = ObjectAccessor::GetUnit(*me, _charmerGUID)) { charmer->RemoveAurasDueToSpell(SPELL_MINDCONTROL_VISUAL); - me->TauntApply(charmer); + me->EngageWithTarget(charmer); + me->AddThreat(charmer, 100.0f); + me->AI()->AttackStart(charmer); } } } diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp index a0d2f2b50..01145b41e 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp @@ -237,7 +237,7 @@ public: void EnterEvadeMode(EvadeReason why) override { - if (!me->GetThreatMgr().GetThreatList().empty()) + if (!me->GetThreatMgr().IsThreatListEmpty()) { if (!_processingMagmaBurst) { diff --git a/src/server/scripts/EasternKingdoms/Karazhan/boss_midnight.cpp b/src/server/scripts/EasternKingdoms/Karazhan/boss_midnight.cpp index d4e1bffaa..654ee2289 100644 --- a/src/server/scripts/EasternKingdoms/Karazhan/boss_midnight.cpp +++ b/src/server/scripts/EasternKingdoms/Karazhan/boss_midnight.cpp @@ -389,18 +389,13 @@ class spell_midnight_fixate : public AuraScript { Unit* target = GetTarget(); if (Unit* caster = GetCaster()) - { - caster->TauntApply(target); - } + caster->GetThreatMgr().FixateTarget(target); } void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) { - Unit* target = GetTarget(); if (Unit* caster = GetCaster()) - { - caster->TauntFadeOut(target); - } + caster->GetThreatMgr().ClearFixate(); } void Register() override diff --git a/src/server/scripts/EasternKingdoms/MagistersTerrace/boss_priestess_delrissa.cpp b/src/server/scripts/EasternKingdoms/MagistersTerrace/boss_priestess_delrissa.cpp index fcb89df18..883f71ca4 100644 --- a/src/server/scripts/EasternKingdoms/MagistersTerrace/boss_priestess_delrissa.cpp +++ b/src/server/scripts/EasternKingdoms/MagistersTerrace/boss_priestess_delrissa.cpp @@ -239,16 +239,14 @@ struct boss_priestess_lackey_commonAI : public ScriptedAI void RecalculateThreat() { - ThreatContainer::StorageType const& tList = me->GetThreatMgr().GetThreatList(); - for (auto const& ref : tList) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - Unit* pUnit = ObjectAccessor::GetUnit(*me, ref->getUnitGuid()); + Unit* pUnit = ref->GetVictim(); if (pUnit && pUnit->IsPlayer() && me->GetThreatMgr().GetThreat(pUnit)) { float threatMod = GetThreatMod(me->GetDistance2d(pUnit), (float)pUnit->GetArmor(), pUnit->GetHealth(), pUnit->GetMaxHealth(), pUnit); - me->GetThreatMgr().ModifyThreatByPercent(pUnit, -100); - if (HostileReference* ref = me->GetThreatMgr().GetOnlineContainer().getReferenceByTarget(pUnit)) - ref->AddThreat(10000000.0f * threatMod); + me->GetThreatMgr().ResetThreat(pUnit); + me->GetThreatMgr().AddThreat(pUnit, 10000000.0f * threatMod, nullptr, true, true); } } } diff --git a/src/server/scripts/EasternKingdoms/Scholomance/instance_scholomance.cpp b/src/server/scripts/EasternKingdoms/Scholomance/instance_scholomance.cpp index 9cd1f4830..49d74bdf7 100644 --- a/src/server/scripts/EasternKingdoms/Scholomance/instance_scholomance.cpp +++ b/src/server/scripts/EasternKingdoms/Scholomance/instance_scholomance.cpp @@ -254,14 +254,13 @@ class spell_scholomance_fixate_aura : public AuraScript { Unit* target = GetTarget(); if (Unit* caster = GetCaster()) - caster->TauntApply(target); + caster->GetThreatMgr().FixateTarget(target); } void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) { - Unit* target = GetTarget(); if (Unit* caster = GetCaster()) - caster->TauntFadeOut(target); + caster->GetThreatMgr().ClearFixate(); } void Register() override @@ -339,10 +338,9 @@ public: Unit* SelectUnitCasting() { - ThreatContainer::StorageType threatlist = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatlist.begin(); itr != threatlist.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* unit = ObjectAccessor::GetUnit(*me, (*itr)->getUnitGuid())) + if (Unit* unit = ref->GetVictim()) { if (unit->HasUnitState(UNIT_STATE_CASTING)) { diff --git a/src/server/scripts/EasternKingdoms/ZulGurub/boss_hazzarah.cpp b/src/server/scripts/EasternKingdoms/ZulGurub/boss_hazzarah.cpp index 819cfa493..c91d36291 100644 --- a/src/server/scripts/EasternKingdoms/ZulGurub/boss_hazzarah.cpp +++ b/src/server/scripts/EasternKingdoms/ZulGurub/boss_hazzarah.cpp @@ -83,14 +83,14 @@ struct boss_hazzarah : public BossAI { if (me->GetThreatMgr().GetThreatListSize() > 1) { - ThreatContainer::StorageType::const_iterator lastRef = me->GetThreatMgr().GetOnlineContainer().GetThreatList().end(); - --lastRef; - if (Unit* lastTarget = (*lastRef)->getTarget()) + // Check if target is the lowest threat (last in sorted list) + ThreatReference const* lowestRef = nullptr; + for (ThreatReference const* ref : me->GetThreatMgr().GetSortedThreatList()) + lowestRef = ref; // Last iteration will have the lowest threat target + + if (lowestRef && lowestRef->GetVictim() != target) { - if (lastTarget != target) - { - return !target->HasAura(SPELL_SLEEP); - } + return !target->HasAura(SPELL_SLEEP); } } diff --git a/src/server/scripts/EasternKingdoms/ZulGurub/boss_mandokir.cpp b/src/server/scripts/EasternKingdoms/ZulGurub/boss_mandokir.cpp index 14b430746..04cbfd4fd 100644 --- a/src/server/scripts/EasternKingdoms/ZulGurub/boss_mandokir.cpp +++ b/src/server/scripts/EasternKingdoms/ZulGurub/boss_mandokir.cpp @@ -470,10 +470,10 @@ public: case EVENT_CLEAVE: { std::list meleeRangeTargets; - auto i = me->GetThreatMgr().GetThreatList().begin(); - for (; i != me->GetThreatMgr().GetThreatList().end(); ++i) + auto i = me->GetThreatMgr().GetUnsortedThreatList().begin(); + for (; i != me->GetThreatMgr().GetUnsortedThreatList().end(); ++i) { - Unit* target = (*i)->getTarget(); + Unit* target = (*i)->GetVictim(); if (me->IsWithinMeleeRange(target)) { meleeRangeTargets.push_back(target); diff --git a/src/server/scripts/EasternKingdoms/ZulGurub/boss_renataki.cpp b/src/server/scripts/EasternKingdoms/ZulGurub/boss_renataki.cpp index 22319d57e..2b83f54cc 100644 --- a/src/server/scripts/EasternKingdoms/ZulGurub/boss_renataki.cpp +++ b/src/server/scripts/EasternKingdoms/ZulGurub/boss_renataki.cpp @@ -80,14 +80,14 @@ public: { if (me->GetThreatMgr().GetThreatListSize() > 1) { - ThreatContainer::StorageType::const_iterator lastRef = me->GetThreatMgr().GetOnlineContainer().GetThreatList().end(); - --lastRef; - if (Unit* lastTarget = (*lastRef)->getTarget()) + // Check if target is the lowest threat (last in sorted list) + ThreatReference const* lowestRef = nullptr; + for (ThreatReference const* ref : me->GetThreatMgr().GetSortedThreatList()) + lowestRef = ref; // Last iteration will have the lowest threat target + + if (lowestRef && lowestRef->GetVictim() != target) { - if (lastTarget != target) - { - return !target->HasAura(SPELL_GOUGE); - } + return !target->HasAura(SPELL_GOUGE); } } @@ -161,10 +161,9 @@ public: if (_thousandBladesTargets.empty()) { std::vector targetList; - ThreatContainer::StorageType const& threatlist = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatlist.begin(); itr != threatlist.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* target = (*itr)->getTarget()) + if (Unit* target = ref->GetVictim()) { if (target->IsAlive() && target->IsWithinDist2d(me, 100.f)) { diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp index 92d833181..f5a82207f 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp @@ -270,7 +270,7 @@ struct boss_ouro : public BossAI void EnterEvadeMode(EvadeReason /*why*/) override { - if (me->GetThreatMgr().GetThreatList().empty()) + if (me->GetThreatMgr().IsThreatListEmpty()) { DoCastSelf(SPELL_OURO_SUBMERGE_VISUAL); me->DespawnOrUnsummon(1s); diff --git a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_hadronox.cpp b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_hadronox.cpp index 0baaa5ca2..4d119ed5e 100644 --- a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_hadronox.cpp +++ b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_hadronox.cpp @@ -235,9 +235,10 @@ struct boss_hadronox : public BossAI bool IsInCombatWithPlayer() const { - return std::ranges::any_of(me->GetThreatMgr().GetThreatList(), [](auto const& ref) { - return ref->getTarget()->IsControlledByPlayer(); - }); + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim()->IsControlledByPlayer()) + return true; + return false; } void DamageTaken(Unit* who, uint32& damage, DamageEffectType /*damageType*/, SpellSchoolMask /*damageSchoolMask*/) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp index aea64c22c..5a53b1bdd 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp @@ -1080,7 +1080,7 @@ class spell_halion_twilight_phasing_aura : public AuraScript GetTarget()->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); GetTarget()->ToCreature()->SetReactState(REACT_DEFENSIVE); GetTarget()->GetMotionMaster()->Clear(); - GetTarget()->GetThreatMgr().clearReferences(); + GetTarget()->GetThreatMgr().ClearAllThreat(); GetTarget()->RemoveAllAttackers(); GetTarget()->AttackStop(); } @@ -1090,7 +1090,7 @@ class spell_halion_twilight_phasing_aura : public AuraScript GetTarget()->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); GetTarget()->ToCreature()->SetReactState(REACT_DEFENSIVE); GetTarget()->GetMotionMaster()->Clear(); - GetTarget()->GetThreatMgr().clearReferences(); + GetTarget()->GetThreatMgr().ClearAllThreat(); GetTarget()->RemoveAllAttackers(); GetTarget()->AttackStop(); } diff --git a/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_argent_challenge.cpp b/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_argent_challenge.cpp index f822281b9..8d1bc1864 100644 --- a/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_argent_challenge.cpp +++ b/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_argent_challenge.cpp @@ -138,7 +138,7 @@ public: me->SetFaction(FACTION_FRIENDLY); events.Reset(); Talk(SAY_EADRIC_DEFEATED); - me->GetThreatMgr().clearReferences(); + me->GetThreatMgr().ClearAllThreat(); me->SetRegeneratingHealth(false); _EnterEvadeMode(); me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); @@ -281,7 +281,7 @@ public: me->SetFaction(FACTION_FRIENDLY); events.Reset(); Talk(SAY_PALETRESS_DEFEATED); - me->GetThreatMgr().clearReferences(); + me->GetThreatMgr().ClearAllThreat(); me->SetRegeneratingHealth(false); _EnterEvadeMode(); me->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE); diff --git a/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_grand_champions.cpp b/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_grand_champions.cpp index db05e177a..374f1d2b5 100644 --- a/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_grand_champions.cpp +++ b/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_grand_champions.cpp @@ -143,7 +143,6 @@ public: void Reset() override { me->SetReactState(REACT_PASSIVE); - me->getHostileRefMgr().setOnlineOfflineState(false); } void OnCharmed(bool apply) override diff --git a/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheCrusader/boss_faction_champions.cpp b/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheCrusader/boss_faction_champions.cpp index d27d43b38..7a13b2d47 100644 --- a/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheCrusader/boss_faction_champions.cpp +++ b/src/server/scripts/Northrend/CrusadersColiseum/TrialOfTheCrusader/boss_faction_champions.cpp @@ -103,17 +103,14 @@ struct boss_faction_championsAI : public ScriptedAI void RecalculateThreat() { - ThreatContainer::StorageType const& tList = me->GetThreatMgr().GetThreatList(); - for( ThreatContainer::StorageType::const_iterator itr = tList.begin(); itr != tList.end(); ++itr ) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - Unit* pUnit = ObjectAccessor::GetUnit(*me, (*itr)->getUnitGuid()); + Unit* pUnit = ref->GetVictim(); if (pUnit && pUnit->IsPlayer() && me->GetThreatMgr().GetThreat(pUnit)) { float threatMod = GetThreatMod(me->GetDistance2d(pUnit), (float)pUnit->GetArmor(), pUnit->GetHealth(), pUnit->GetMaxHealth(), pUnit); - me->GetThreatMgr().ModifyThreatByPercent(pUnit, -100); - //me->getThreatMgr().DoAddThreat(pUnit, 10000000.0f * threatMod); - if (HostileReference* ref = me->GetThreatMgr().GetOnlineContainer().getReferenceByTarget(pUnit)) - ref->AddThreat(10000000.0f * threatMod); + me->GetThreatMgr().ResetThreat(pUnit); + me->GetThreatMgr().AddThreat(pUnit, 10000000.0f * threatMod, nullptr, true, true); } } } @@ -179,27 +176,23 @@ struct boss_faction_championsAI : public ScriptedAI uint32 EnemiesInRange(float distance) { - ThreatContainer::StorageType const& tList = me->GetThreatMgr().GetThreatList(); uint32 count = 0; - Unit* target; - for( ThreatContainer::StorageType::const_iterator iter = tList.begin(); iter != tList.end(); ++iter ) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - target = ObjectAccessor::GetUnit((*me), (*iter)->getUnitGuid()); - if (target && me->GetDistance2d(target) < distance ) - ++count; + if (Unit* target = ref->GetVictim()) + if (me->GetDistance2d(target) < distance) + ++count; } return count; } Unit* SelectEnemyCaster(bool casting, float range) { - ThreatContainer::StorageType const& tList = me->GetThreatMgr().GetThreatList(); - Unit* target; - for( ThreatContainer::StorageType::const_iterator iter = tList.begin(); iter != tList.end(); ++iter ) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - target = ObjectAccessor::GetUnit((*me), (*iter)->getUnitGuid()); - if (target && target->getPowerType() == POWER_MANA && (!casting || target->HasUnitState(UNIT_STATE_CASTING)) && me->GetExactDist(target) <= range ) - return target; + if (Unit* target = ref->GetVictim()) + if (target->getPowerType() == POWER_MANA && (!casting || target->HasUnitState(UNIT_STATE_CASTING)) && me->GetExactDist(target) <= range) + return target; } return nullptr; } diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_icecrown_gunship_battle.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_icecrown_gunship_battle.cpp index 0e9ff125d..fc562f39b 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_icecrown_gunship_battle.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_icecrown_gunship_battle.cpp @@ -980,7 +980,7 @@ public: for (Map::PlayerList::const_iterator itr = pl.begin(); itr != pl.end(); ++itr) if (Player* p = itr->GetSource()) if (!p->IsGameMaster()) - p->SetInCombatState(true); + p->SetInCombatWith(me); _events.ScheduleEvent(EVENT_KEEP_PLAYER_IN_COMBAT, 4s); } break; @@ -1319,7 +1319,7 @@ public: for (Map::PlayerList::const_iterator itr = pl.begin(); itr != pl.end(); ++itr) if (Player* p = itr->GetSource()) if (!p->IsGameMaster()) - p->SetInCombatState(true); + p->SetInCombatWith(me); _events.ScheduleEvent(EVENT_KEEP_PLAYER_IN_COMBAT, 4s); } break; diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_lady_deathwhisper.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_lady_deathwhisper.cpp index d83bd041d..a80ff3250 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_lady_deathwhisper.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_lady_deathwhisper.cpp @@ -301,13 +301,13 @@ public: if (events.GetPhaseMask() & PHASE_ONE_MASK && damage >= me->GetPower(POWER_MANA)) { // reset threat - ThreatContainer::StorageType const& threatlist = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatlist.begin(); itr != threatlist.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - Unit* unit = ObjectAccessor::GetUnit((*me), (*itr)->getUnitGuid()); - - if (unit && DoGetThreat(unit)) - DoModifyThreatByPercent(unit, -100); + if (Unit* unit = ref->GetVictim()) + { + if (DoGetThreat(unit)) + DoModifyThreatByPercent(unit, -100); + } } Talk(SAY_PHASE_2); diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_professor_putricide.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_professor_putricide.cpp index b22267b70..5c5354775 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_professor_putricide.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_professor_putricide.cpp @@ -209,7 +209,7 @@ public: bool operator()(Unit const* target) const { - if (!me || !target || !target->IsPlayer() || target == me->GetThreatMgr().GetCurrentVictim()) + if (!me || !target || !target->IsPlayer() || target == me->GetThreatMgr().GetLastVictim()) return false; if (me->IsWithinCombatRange(target, 7.0f)) diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp index f32da2d84..346997c48 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp @@ -498,7 +498,7 @@ public: bool Execute(uint64 /*time*/, uint32 /*diff*/) override { _owner->SetReactState(REACT_AGGRESSIVE); - if (!_owner->GetThreatMgr().isThreatListEmpty()) + if (!_owner->GetThreatMgr().IsThreatListEmpty()) if (Unit* target = _owner->SelectVictim()) _owner->AI()->AttackStart(target); if (!_owner->GetVictim()) @@ -1180,7 +1180,7 @@ public: { if (summon->GetEntry() == NPC_VILE_SPIRIT) { - summon->m_Events.KillAllEvents(true); + summon->m_Events.KillAllEvents(false); summon->m_Events.AddEventAtOffset(new VileSpiritActivateEvent(summon), 55s); summon->GetMotionMaster()->Clear(true); summon->StopMoving(); @@ -2273,7 +2273,7 @@ public: { me->SetControlled(false, UNIT_STATE_ROOT); - if (!me->GetThreatMgr().isThreatListEmpty()) + if (!me->GetThreatMgr().IsThreatListEmpty()) if (Unit* target = me->SelectVictim()) AttackStart(target); if (!me->GetVictim()) diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_valithria_dreamwalker.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_valithria_dreamwalker.cpp index afab4da61..d700cb08c 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_valithria_dreamwalker.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_valithria_dreamwalker.cpp @@ -593,12 +593,12 @@ public: { checkTimer = 3000; me->SetInCombatWithZone(); - ThreatContainer::StorageType const& threatList = me->GetThreatMgr().GetThreatList(); - if (!threatList.empty()) - for (ThreatContainer::StorageType::const_iterator itr = threatList.begin(); itr != threatList.end(); ++itr) - if (Unit* target = (*itr)->getTarget()) - if (target->IsAlive() && target->IsPlayer() && me->GetExactDist(target) < 200.0f && !target->IsImmunedToDamageOrSchool(SPELL_SCHOOL_MASK_ALL)) - return; + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) + { + if (Unit* target = ref->GetVictim()) + if (target->IsAlive() && target->IsPlayer() && me->GetExactDist(target) < 200.0f && !target->IsImmunedToDamageOrSchool(SPELL_SCHOOL_MASK_ALL)) + return; + } EnterEvadeMode(); } else diff --git a/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.cpp b/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.cpp index 1c1743841..94ca3e5c4 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.cpp @@ -1762,15 +1762,26 @@ public: Position myPos = me->GetPosition(); me->NearTeleportTo(c->GetPositionX(), c->GetPositionY(), c->GetPositionZ(), c->GetOrientation()); c->NearTeleportTo(myPos.GetPositionX(), myPos.GetPositionY(), myPos.GetPositionZ(), myPos.GetOrientation()); - const ThreatContainer::StorageType me_tl = me->GetThreatMgr().GetThreatList(); - const ThreatContainer::StorageType target_tl = c->GetThreatMgr().GetThreatList(); + + // Store threat values before reset + std::vector> myThreats; + std::vector> targetThreats; + + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) + if (Unit* victim = ref->GetVictim()) + myThreats.push_back({victim, ref->GetThreat()}); + + for (ThreatReference const* ref : c->GetThreatMgr().GetUnsortedThreatList()) + if (Unit* victim = ref->GetVictim()) + targetThreats.push_back({victim, ref->GetThreat()}); + DoResetThreatList(); - for (ThreatContainer::StorageType::const_iterator iter = target_tl.begin(); iter != target_tl.end(); ++iter) - me->GetThreatMgr().AddThreat((*iter)->getTarget(), (*iter)->GetThreat()); + for (auto const& pair : targetThreats) + me->GetThreatMgr().AddThreat(pair.first, pair.second); c->GetThreatMgr().ResetAllThreat(); - for (ThreatContainer::StorageType::const_iterator iter = me_tl.begin(); iter != me_tl.end(); ++iter) - c->GetThreatMgr().AddThreat((*iter)->getTarget(), (*iter)->GetThreat()); + for (auto const& pair : myThreats) + c->GetThreatMgr().AddThreat(pair.first, pair.second); } } diff --git a/src/server/scripts/Northrend/Naxxramas/boss_gothik.cpp b/src/server/scripts/Northrend/Naxxramas/boss_gothik.cpp index 7c26e95f4..6125ba262 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_gothik.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_gothik.cpp @@ -406,16 +406,19 @@ public: events.Repeat(15s); break; case EVENT_TELEPORT: + { me->AttackStop(); if (IN_LIVE_SIDE(me)) - { me->CastSpell(me, SPELL_TELEPORT_DEAD, false); - } else - { me->CastSpell(me, SPELL_TELEPORT_LIVE, false); - } - me->GetThreatMgr().resetAggro(NotOnSameSide(me)); + + // Clear threat from targets not on the same side as Gothik + NotOnSameSide notOnSameSide(me); + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) + if (notOnSameSide(ref->GetVictim())) + me->GetThreatMgr().ClearThreat(ref->GetVictim()); + if (Unit* pTarget = SelectTarget(SelectTargetMethod::MaxDistance, 0)) { me->GetThreatMgr().AddThreat(pTarget, 100.0f); @@ -423,6 +426,7 @@ public: } events.Repeat(20s); break; + } case EVENT_CHECK_HEALTH: if (me->HealthBelowPct(30)) { diff --git a/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp b/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp index 38fce6620..abb53e6e2 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp @@ -401,15 +401,17 @@ public: case EVENT_DETONATE_MANA: { std::vector unitList; - ThreatContainer::StorageType const& threatList = me->GetThreatMgr().GetThreatList(); - for (auto itr : threatList) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (itr->getTarget()->IsPlayer() - && itr->getTarget()->getPowerType() == POWER_MANA - && itr->getTarget()->GetPower(POWER_MANA)) - { - unitList.push_back(itr->getTarget()); - } + if (Unit* target = ref->GetVictim()) + { + if (target->IsPlayer() + && target->getPowerType() == POWER_MANA + && target->GetPower(POWER_MANA)) + { + unitList.push_back(target); + } + } } if (!unitList.empty()) { diff --git a/src/server/scripts/Northrend/Naxxramas/boss_patchwerk.cpp b/src/server/scripts/Northrend/Naxxramas/boss_patchwerk.cpp index 319f08966..25d46fcbd 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_patchwerk.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_patchwerk.cpp @@ -117,11 +117,11 @@ public: std::list meleeRangeTargets; Unit* finalTarget = nullptr; uint8 counter = 0; - auto i = me->GetThreatMgr().GetThreatList().begin(); - for (; i != me->GetThreatMgr().GetThreatList().end(); ++i, ++counter) + auto threatList = me->GetThreatMgr().GetSortedThreatList(); + for (auto i = threatList.begin(); i != threatList.end(); ++i, ++counter) { // Gather all units with melee range - Unit* target = (*i)->getTarget(); + Unit* target = (*i)->GetVictim(); if (me->IsWithinMeleeRange(target)) { meleeRangeTargets.push_back(target); diff --git a/src/server/scripts/Northrend/Naxxramas/boss_sapphiron.cpp b/src/server/scripts/Northrend/Naxxramas/boss_sapphiron.cpp index c7407f554..8f19a4239 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_sapphiron.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_sapphiron.cpp @@ -315,17 +315,17 @@ public: } std::vector targets; - auto i = me->GetThreatMgr().GetThreatList().begin(); - for (; i != me->GetThreatMgr().GetThreatList().end(); ++i) + auto i = me->GetThreatMgr().GetUnsortedThreatList().begin(); + for (; i != me->GetThreatMgr().GetUnsortedThreatList().end(); ++i) { - if ((*i)->getTarget()->IsPlayer()) + if ((*i)->GetVictim()->IsPlayer()) { bool inList = false; if (!blockList.empty()) { for (GuidList::const_iterator itr = blockList.begin(); itr != blockList.end(); ++itr) { - if ((*i)->getTarget()->GetGUID() == *itr) + if ((*i)->GetVictim()->GetGUID() == *itr) { inList = true; break; @@ -334,7 +334,7 @@ public: } if (!inList) { - targets.push_back((*i)->getTarget()); + targets.push_back((*i)->GetVictim()); } } } diff --git a/src/server/scripts/Northrend/Naxxramas/boss_thaddius.cpp b/src/server/scripts/Northrend/Naxxramas/boss_thaddius.cpp index 7fb48d6d1..d71e12468 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_thaddius.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_thaddius.cpp @@ -128,8 +128,8 @@ public: bool IsAnyPlayerInMeleeRange() const { - for (auto const& ref : me->GetThreatMgr().GetThreatList()) - if (Unit* target = ref->getTarget()) + for (auto const* ref : me->GetThreatMgr().GetUnsortedThreatList()) + if (Unit* target = ref->GetVictim()) if (target->IsPlayer() && me->IsWithinMeleeRange(target)) return true; return false; diff --git a/src/server/scripts/Northrend/zone_dragonblight.cpp b/src/server/scripts/Northrend/zone_dragonblight.cpp index 4b0238f3d..9e2781dba 100644 --- a/src/server/scripts/Northrend/zone_dragonblight.cpp +++ b/src/server/scripts/Northrend/zone_dragonblight.cpp @@ -1419,7 +1419,7 @@ public: { me->CastSpell(me, SPELL_SAC_GHOUL_EXPLODE, true); me->KillSelf(); - me->m_Events.KillAllEvents(true); + me->m_Events.KillAllEvents(false); Deactivate(); } } diff --git a/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_blackheart_the_inciter.cpp b/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_blackheart_the_inciter.cpp index 6634e15f0..155ad15fd 100644 --- a/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_blackheart_the_inciter.cpp +++ b/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_blackheart_the_inciter.cpp @@ -85,15 +85,16 @@ struct boss_blackheart_the_inciter : public BossAI DoCastAOE(SPELL_INCITE_CHAOS); DoCastSelf(SPELL_LAUGHTER, true); uint32 inciteTriggerID = NPC_INCITE_TRIGGER; - std::list t_list = me->GetThreatMgr().GetThreatList(); - for (std::list::const_iterator itr = t_list.begin(); itr != t_list.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - Unit* target = ObjectAccessor::GetUnit(*me, (*itr)->getUnitGuid()); - if (target && target->IsPlayer()) + if (Unit* target = ref->GetVictim()) { - if (Creature* inciteTrigger = me->SummonCreature(inciteTriggerID++, *target, TEMPSUMMON_TIMED_DESPAWN, 15 * IN_MILLISECONDS)) + if (target->IsPlayer()) { - inciteTrigger->CastSpell(target, SPELL_INCITE_CHAOS_B, true); + if (Creature* inciteTrigger = me->SummonCreature(inciteTriggerID++, *target, TEMPSUMMON_TIMED_DESPAWN, 15 * IN_MILLISECONDS)) + { + inciteTrigger->CastSpell(target, SPELL_INCITE_CHAOS_B, true); + } } } } diff --git a/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_murmur.cpp b/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_murmur.cpp index 60feacbce..a9c1471e9 100644 --- a/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_murmur.cpp +++ b/src/server/scripts/Outland/Auchindoun/ShadowLabyrinth/boss_murmur.cpp @@ -87,7 +87,7 @@ struct boss_murmur : public BossAI void EnterEvadeMode(EvadeReason why) override { - if (me->GetThreatMgr().GetThreatList().empty()) + if (me->GetThreatMgr().IsThreatListEmpty()) { BossAI::EnterEvadeMode(why); } diff --git a/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp b/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp index 4fe7fc412..c41e1ecc2 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp @@ -1104,7 +1104,7 @@ struct npc_maiev_illidan : public ScriptedAI void Reset() override { scheduler.CancelAll(); - me->m_Events.KillAllEvents(true); + me->m_Events.KillAllEvents(false); } void DoAction(int32 param) override diff --git a/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp b/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp index c1d916de0..5a5796538 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_supremus.cpp @@ -168,13 +168,14 @@ struct boss_supremus : public BossAI Unit* FindHatefulStrikeTarget() { Unit* target = nullptr; - ThreatContainer::StorageType const& threatlist = me->GetThreatMgr().GetThreatList(); - for (ThreatContainer::StorageType::const_iterator i = threatlist.begin(); i != threatlist.end(); ++i) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - Unit* unit = ObjectAccessor::GetUnit(*me, (*i)->getUnitGuid()); - if (unit && me->IsWithinMeleeRange(unit)) - if (!target || unit->GetHealth() > target->GetHealth()) - target = unit; + if (Unit* unit = ref->GetVictim()) + { + if (me->IsWithinMeleeRange(unit)) + if (!target || unit->GetHealth() > target->GetHealth()) + target = unit; + } } return target; diff --git a/src/server/scripts/Outland/BlackTemple/instance_black_temple.cpp b/src/server/scripts/Outland/BlackTemple/instance_black_temple.cpp index 5d971da18..188e1386a 100644 --- a/src/server/scripts/Outland/BlackTemple/instance_black_temple.cpp +++ b/src/server/scripts/Outland/BlackTemple/instance_black_temple.cpp @@ -215,8 +215,9 @@ class spell_black_template_harpooners_mark_aura : public AuraScript GetUnitOwner()->GetCreaturesWithEntryInRange(creatureList, 80.0f, NPC_DRAGON_TURTLE); for (std::list::const_iterator itr = creatureList.begin(); itr != creatureList.end(); ++itr) { - (*itr)->TauntApply(GetUnitOwner()); (*itr)->AddThreat(GetUnitOwner(), 10000000.0f); + if ((*itr)->AI()) + (*itr)->AI()->AttackStart(GetUnitOwner()); _turtleSet.insert((*itr)->GetGUID()); } } @@ -226,7 +227,6 @@ class spell_black_template_harpooners_mark_aura : public AuraScript for (ObjectGuid const& guid : _turtleSet) if (Creature* turtle = ObjectAccessor::GetCreature(*GetUnitOwner(), guid)) { - turtle->TauntFadeOut(GetUnitOwner()); turtle->AddThreat(GetUnitOwner(), -10000000.0f); } } diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lurker_below.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lurker_below.cpp index 510fdf745..5f3ea7573 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lurker_below.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lurker_below.cpp @@ -212,10 +212,9 @@ struct boss_the_lurker_below : public BossAI } else { - ThreatContainer::StorageType const& t_list = me->GetThreatMgr().GetThreatList(); - for (ThreatReference const* ref : t_list) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if (Unit* threatTarget = ObjectAccessor::GetUnit(*me, ref->getUnitGuid())) + if (Unit* threatTarget = ref->GetVictim()) { if (me->IsWithinMeleeRange(threatTarget)) { diff --git a/src/server/scripts/Outland/TempestKeep/Eye/boss_alar.cpp b/src/server/scripts/Outland/TempestKeep/Eye/boss_alar.cpp index ae178c822..910d8b8cc 100644 --- a/src/server/scripts/Outland/TempestKeep/Eye/boss_alar.cpp +++ b/src/server/scripts/Outland/TempestKeep/Eye/boss_alar.cpp @@ -172,7 +172,7 @@ struct boss_alar : public BossAI { if (why == EVADE_REASON_BOUNDARY) BossAI::EnterEvadeMode(why); - else if (me->GetThreatMgr().GetThreatList().empty()) + else if (me->GetThreatMgr().IsThreatListEmpty()) BossAI::EnterEvadeMode(why); } diff --git a/src/server/scripts/Outland/TempestKeep/Eye/boss_kaelthas.cpp b/src/server/scripts/Outland/TempestKeep/Eye/boss_kaelthas.cpp index c4c6c84e3..31390d5a3 100644 --- a/src/server/scripts/Outland/TempestKeep/Eye/boss_kaelthas.cpp +++ b/src/server/scripts/Outland/TempestKeep/Eye/boss_kaelthas.cpp @@ -1083,13 +1083,14 @@ class spell_kaelthas_nether_beam : public SpellScript { PreventHitEffect(effIndex); - ThreatContainer::StorageType const& ThreatList = GetCaster()-> GetThreatMgr().GetThreatList(); std::list targetList; - for (ThreatContainer::StorageType::const_iterator itr = ThreatList.begin(); itr != ThreatList.end(); ++itr) + for (ThreatReference const* ref : GetCaster()->GetThreatMgr().GetUnsortedThreatList()) { - Unit* target = ObjectAccessor::GetUnit(*GetCaster(), (*itr)->getUnitGuid()); - if (target && target->IsPlayer()) - targetList.push_back(target); + if (Unit* target = ref->GetVictim()) + { + if (target->IsPlayer()) + targetList.push_back(target); + } } Acore::Containers::RandomResize(targetList, 5); diff --git a/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp b/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp index ffa90492a..ad0f03273 100644 --- a/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp +++ b/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp @@ -59,8 +59,9 @@ class spell_botanica_call_of_the_falcon_aura : public AuraScript GetUnitOwner()->GetCreaturesWithEntryInRange(creatureList, 80.0f, NPC_BLOODFALCON); for (std::list::const_iterator itr = creatureList.begin(); itr != creatureList.end(); ++itr) { - (*itr)->TauntApply(GetUnitOwner()); (*itr)->AddThreat(GetUnitOwner(), 10000000.0f); + if ((*itr)->AI()) + (*itr)->AI()->AttackStart(GetUnitOwner()); _falconSet.insert((*itr)->GetGUID()); } } @@ -70,7 +71,6 @@ class spell_botanica_call_of_the_falcon_aura : public AuraScript for (ObjectGuid const& guid : _falconSet) if (Creature* falcon = ObjectAccessor::GetCreature(*GetUnitOwner(), guid)) { - falcon->TauntFadeOut(GetUnitOwner()); falcon->AddThreat(GetUnitOwner(), -10000000.0f); } } diff --git a/src/server/scripts/Outland/zone_blades_edge_mountains.cpp b/src/server/scripts/Outland/zone_blades_edge_mountains.cpp index 18dd9891f..514da0afb 100644 --- a/src/server/scripts/Outland/zone_blades_edge_mountains.cpp +++ b/src/server/scripts/Outland/zone_blades_edge_mountains.cpp @@ -195,7 +195,6 @@ public: if (Creature* Target = GetClosestCreatureWithEntry(me, NPC_DEATHS_DOOR_FEL_CANNON_TARGET_BUNNY, 200.0f)) { me->SetFacingToObject(Target); - me->TauntFadeOut(Target); me->CombatStop(); // force } diff --git a/src/server/scripts/Pet/pet_hunter.cpp b/src/server/scripts/Pet/pet_hunter.cpp index 377144030..239ae9660 100644 --- a/src/server/scripts/Pet/pet_hunter.cpp +++ b/src/server/scripts/Pet/pet_hunter.cpp @@ -184,7 +184,7 @@ class spell_pet_guard_dog : public AuraScript return; float addThreat = CalculatePct(static_cast(procSpellInfo->Effects[EFFECT_0].CalcValue(caster)), aurEff->GetAmount()); - target->GetThreatMgr().AddThreat(caster, addThreat, SPELL_SCHOOL_MASK_NORMAL, GetSpellInfo()); + target->GetThreatMgr().AddThreat(caster, addThreat, GetSpellInfo()); } void Register() override diff --git a/src/server/scripts/Pet/pet_mage.cpp b/src/server/scripts/Pet/pet_mage.cpp index ce9be114f..6ccf7673f 100644 --- a/src/server/scripts/Pet/pet_mage.cpp +++ b/src/server/scripts/Pet/pet_mage.cpp @@ -106,12 +106,10 @@ struct npc_pet_mage_mirror_image : CasterAI // Xinef: Inherit Master's Threat List (not yet implemented) //owner->CastSpell((Unit*)nullptr, SPELL_MAGE_MASTERS_THREAT_LIST, true); - HostileReference* ref = owner->getHostileRefMgr().getFirst(); - while (ref) + for (auto const& pair : owner->GetThreatMgr().GetThreatenedByMeList()) { - if (Unit* unit = ref->GetSource()->GetOwner()) - unit->AddThreat(me, ref->GetThreat() - ref->getTempThreatModifier()); - ref = ref->next(); + if (Unit* unit = pair.second->GetOwner()) + unit->GetThreatMgr().AddThreat(me, pair.second->GetThreat()); } _ebonGargoyleGUID.Clear(); diff --git a/src/server/scripts/Spells/spell_dk.cpp b/src/server/scripts/Spells/spell_dk.cpp index 6222592b8..ae2d2c517 100644 --- a/src/server/scripts/Spells/spell_dk.cpp +++ b/src/server/scripts/Spells/spell_dk.cpp @@ -673,6 +673,20 @@ class spell_dk_dancing_rune_weapon : public AuraScript { PrepareAuraScript(spell_dk_dancing_rune_weapon); + void HandleApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) + { + Unit* caster = GetCaster(); + if (!caster) + return; + + // Redirect 100% of the DRW's threat to the DK player + uint32 npcEntry = GetSpellInfo()->Effects[EFFECT_0].MiscValue; + std::list runeWeapons; + caster->GetAllMinionsByEntry(runeWeapons, npcEntry); + for (Creature* temp : runeWeapons) + temp->GetThreatMgr().RegisterRedirectThreat(GetId(), caster->GetGUID(), 100); + } + bool CheckProc(ProcEventInfo& eventInfo) { if (!eventInfo.GetActor() || !eventInfo.GetActionTarget() || !eventInfo.GetActionTarget()->IsAlive() || !eventInfo.GetActor()->IsPlayer()) @@ -749,6 +763,7 @@ class spell_dk_dancing_rune_weapon : public AuraScript void Register() override { + AfterEffectApply += AuraEffectApplyFn(spell_dk_dancing_rune_weapon::HandleApply, EFFECT_2, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); DoCheckProc += AuraCheckProcFn(spell_dk_dancing_rune_weapon::CheckProc); OnEffectProc += AuraEffectProcFn(spell_dk_dancing_rune_weapon::HandleProc, EFFECT_1, SPELL_AURA_DUMMY); } diff --git a/src/server/scripts/Spells/spell_hunter.cpp b/src/server/scripts/Spells/spell_hunter.cpp index 4e6e5d9be..ed9556de2 100644 --- a/src/server/scripts/Spells/spell_hunter.cpp +++ b/src/server/scripts/Spells/spell_hunter.cpp @@ -52,6 +52,7 @@ enum HunterSpells SPELL_HUNTER_IMPROVED_MEND_PET = 24406, SPELL_HUNTER_INVIGORATION_TRIGGERED = 53398, SPELL_HUNTER_MASTERS_CALL_TRIGGERED = 62305, + SPELL_HUNTER_MISDIRECTION = 34477, SPELL_HUNTER_MISDIRECTION_PROC = 35079, SPELL_HUNTER_PET_LAST_STAND_TRIGGERED = 53479, SPELL_HUNTER_PET_HEART_OF_THE_PHOENIX = 55709, @@ -893,7 +894,7 @@ class spell_hun_misdirection : public AuraScript void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) { if (GetTargetApplication()->GetRemoveMode() != AURA_REMOVE_BY_DEFAULT || !GetTarget()->HasAura(SPELL_HUNTER_MISDIRECTION_PROC)) - GetTarget()->ResetRedirectThreat(); + GetTarget()->GetThreatMgr().UnregisterRedirectThreat(SPELL_HUNTER_MISDIRECTION); } bool CheckProc(ProcEventInfo& eventInfo) @@ -902,7 +903,7 @@ class spell_hun_misdirection : public AuraScript if ((eventInfo.GetProcSpell() && (eventInfo.GetProcSpell()->GetSpellInfo()->SpellFamilyFlags[0] & 0x800000)) || (eventInfo.GetHealInfo() && (eventInfo.GetHealInfo()->GetSpellInfo()->SpellFamilyFlags[0] & 0x800000))) return false; - return GetTarget()->GetRedirectThreatTarget(); + return GetTarget()->GetThreatMgr().HasRedirects(); } void HandleProc(AuraEffect const* aurEff, ProcEventInfo& /*eventInfo*/) @@ -926,7 +927,7 @@ class spell_hun_misdirection_proc : public AuraScript void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) { - GetTarget()->ResetRedirectThreat(); + GetTarget()->GetThreatMgr().UnregisterRedirectThreat(SPELL_HUNTER_MISDIRECTION); } void Register() override diff --git a/src/server/scripts/Spells/spell_priest.cpp b/src/server/scripts/Spells/spell_priest.cpp index 223a5d52a..bcd9673e4 100644 --- a/src/server/scripts/Spells/spell_priest.cpp +++ b/src/server/scripts/Spells/spell_priest.cpp @@ -915,9 +915,8 @@ class spell_pri_mind_control : public AuraScript { if (Unit* target = GetTarget()) { - uint32 duration = static_cast(GetDuration()); - caster->SetInCombatWith(target, duration); - target->SetInCombatWith(caster, duration); + caster->SetInCombatWith(target); + target->SetInCombatWith(caster); } } } diff --git a/src/server/scripts/Spells/spell_rogue.cpp b/src/server/scripts/Spells/spell_rogue.cpp index 8a0cc51cd..5d53b04d0 100644 --- a/src/server/scripts/Spells/spell_rogue.cpp +++ b/src/server/scripts/Spells/spell_rogue.cpp @@ -42,6 +42,7 @@ enum RogueSpells SPELL_ROGUE_KILLING_SPREE_DMG_BUFF = 61851, SPELL_ROGUE_PREY_ON_THE_WEAK = 58670, SPELL_ROGUE_SHIV_TRIGGERED = 5940, + SPELL_ROGUE_TRICKS_OF_THE_TRADE = 57934, SPELL_ROGUE_TRICKS_OF_THE_TRADE_DMG_BOOST = 57933, SPELL_ROGUE_TRICKS_OF_THE_TRADE_PROC = 59628, // Proc system spells @@ -616,53 +617,68 @@ class spell_rog_shiv : public SpellScript } }; -// 57934 - Tricks of the Trade -class spell_rog_tricks_of_the_trade : public AuraScript +// 57934 - Tricks of the Trade (AuraScript) +class spell_rog_tricks_of_the_trade_aura : public AuraScript { - PrepareAuraScript(spell_rog_tricks_of_the_trade); + PrepareAuraScript(spell_rog_tricks_of_the_trade_aura); bool Validate(SpellInfo const* /*spellInfo*/) override { return ValidateSpellInfo({ SPELL_ROGUE_TRICKS_OF_THE_TRADE_DMG_BOOST, SPELL_ROGUE_TRICKS_OF_THE_TRADE_PROC }); } - bool Load() override - { - _redirectTarget = nullptr; - return true; - } - void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) { - if (GetTargetApplication()->GetRemoveMode() != AURA_REMOVE_BY_DEFAULT) - GetTarget()->ResetRedirectThreat(); + if (GetTargetApplication()->GetRemoveMode() != AURA_REMOVE_BY_DEFAULT || !GetTarget()->HasAura(SPELL_ROGUE_TRICKS_OF_THE_TRADE_PROC)) + GetTarget()->GetThreatMgr().UnregisterRedirectThreat(SPELL_ROGUE_TRICKS_OF_THE_TRADE); } - bool CheckProc(ProcEventInfo& /*eventInfo*/) - { - _redirectTarget = GetTarget()->GetRedirectThreatTarget(); - return _redirectTarget; - } - - void HandleProc(AuraEffect const* /*aurEff*/, ProcEventInfo& /*eventInfo*/) + void HandleProc(AuraEffect const* aurEff, ProcEventInfo& /*eventInfo*/) { PreventDefaultAction(); - Unit* target = GetTarget(); - target->CastSpell(_redirectTarget, SPELL_ROGUE_TRICKS_OF_THE_TRADE_DMG_BOOST, true); - target->CastSpell(target, SPELL_ROGUE_TRICKS_OF_THE_TRADE_PROC, true); - Remove(AURA_REMOVE_BY_DEFAULT); // maybe handle by proc charges + Unit* rogue = GetTarget(); + Unit* target = ObjectAccessor::GetUnit(*rogue, _redirectTarget); + if (target) + { + rogue->CastSpell(target, SPELL_ROGUE_TRICKS_OF_THE_TRADE_DMG_BOOST, true, nullptr, aurEff); + rogue->CastSpell(rogue, SPELL_ROGUE_TRICKS_OF_THE_TRADE_PROC, true, nullptr, aurEff); + } + Remove(AURA_REMOVE_BY_DEFAULT); } void Register() override { - AfterEffectRemove += AuraEffectRemoveFn(spell_rog_tricks_of_the_trade::OnRemove, EFFECT_1, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); - DoCheckProc += AuraCheckProcFn(spell_rog_tricks_of_the_trade::CheckProc); - OnEffectProc += AuraEffectProcFn(spell_rog_tricks_of_the_trade::HandleProc, EFFECT_1, SPELL_AURA_DUMMY); + AfterEffectRemove += AuraEffectRemoveFn(spell_rog_tricks_of_the_trade_aura::OnRemove, EFFECT_1, SPELL_AURA_DUMMY, AURA_EFFECT_HANDLE_REAL); + OnEffectProc += AuraEffectProcFn(spell_rog_tricks_of_the_trade_aura::HandleProc, EFFECT_1, SPELL_AURA_DUMMY); } -private: - Unit* _redirectTarget; + ObjectGuid _redirectTarget; +public: + void SetRedirectTarget(ObjectGuid const& guid) { _redirectTarget = guid; } +}; + +// 57934 - Tricks of the Trade (SpellScript) +class spell_rog_tricks_of_the_trade : public SpellScript +{ + PrepareSpellScript(spell_rog_tricks_of_the_trade); + + void DoAfterHit() + { + if (Aura* aura = GetHitAura()) + if (auto* script = aura->GetScript("spell_rog_tricks_of_the_trade")) + { + if (Unit* explTarget = GetExplTargetUnit()) + script->SetRedirectTarget(explTarget->GetGUID()); + else + script->SetRedirectTarget(ObjectGuid::Empty); + } + } + + void Register() override + { + AfterHit += SpellHitFn(spell_rog_tricks_of_the_trade::DoAfterHit); + } }; // 59628 - Tricks of the Trade (Proc) @@ -672,7 +688,7 @@ class spell_rog_tricks_of_the_trade_proc : public AuraScript void HandleRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) { - GetTarget()->ResetRedirectThreat(); + GetTarget()->GetThreatMgr().UnregisterRedirectThreat(SPELL_ROGUE_TRICKS_OF_THE_TRADE); } void Register() override @@ -1162,7 +1178,7 @@ void AddSC_rogue_spell_scripts() RegisterSpellScript(spell_rog_prey_on_the_weak); RegisterSpellScript(spell_rog_rupture); RegisterSpellScript(spell_rog_shiv); - RegisterSpellScript(spell_rog_tricks_of_the_trade); + RegisterSpellAndAuraScriptPair(spell_rog_tricks_of_the_trade, spell_rog_tricks_of_the_trade_aura); RegisterSpellScript(spell_rog_tricks_of_the_trade_proc); RegisterSpellScript(spell_rog_pickpocket); RegisterSpellScript(spell_rog_vanish_purge); diff --git a/src/server/scripts/Spells/spell_warrior.cpp b/src/server/scripts/Spells/spell_warrior.cpp index e056632b2..b6a96008f 100644 --- a/src/server/scripts/Spells/spell_warrior.cpp +++ b/src/server/scripts/Spells/spell_warrior.cpp @@ -748,7 +748,7 @@ class spell_warr_vigilance : public AuraScript target->RemoveAurasDueToSpell(SPELL_GEN_DAMAGE_REDUCTION_AURA); } - target->ResetRedirectThreat(); + target->GetThreatMgr().UnregisterRedirectThreat(SPELL_WARRIOR_VIGILANCE_REDIRECT_THREAT, GetCasterGUID()); } bool CheckProc(ProcEventInfo& /*eventInfo*/) diff --git a/src/server/scripts/World/boss_emerald_dragons.cpp b/src/server/scripts/World/boss_emerald_dragons.cpp index 002b56988..a6137b474 100644 --- a/src/server/scripts/World/boss_emerald_dragons.cpp +++ b/src/server/scripts/World/boss_emerald_dragons.cpp @@ -320,13 +320,13 @@ public: { Talk(SAY_YSONDRE_SUMMON_DRUIDS); - auto const& attackers = me->GetThreatMgr().GetThreatList(); uint8 attackersCount = 0; - for (const auto attacker : attackers) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if ((*attacker)->ToPlayer() && (*attacker)->IsAlive()) - ++attackersCount; + if (Unit* victim = ref->GetVictim()) + if (victim->ToPlayer() && victim->IsAlive()) + ++attackersCount; } uint8 amount = attackersCount < 30 ? attackersCount * 0.5f : 15; diff --git a/src/server/scripts/World/npc_stave_of_ancients.cpp b/src/server/scripts/World/npc_stave_of_ancients.cpp index fbe9a9c36..af9fbc41a 100644 --- a/src/server/scripts/World/npc_stave_of_ancients.cpp +++ b/src/server/scripts/World/npc_stave_of_ancients.cpp @@ -53,11 +53,14 @@ void NPCStaveQuestAI::StorePlayerGUID() return; } - for (ThreatContainer::StorageType::const_iterator itr = threatList.begin(); itr != threatList.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - if ((*itr)->getTarget()->IsPlayer()) + if (Unit* target = ref->GetVictim()) { - playerGUID = (*itr)->getUnitGuid(); + if (target->IsPlayer()) + { + playerGUID = target->GetGUID(); + } } } } @@ -107,18 +110,16 @@ bool NPCStaveQuestAI::UnitIsUnfair(Unit* unit) bool NPCStaveQuestAI::IsFairFight() { - for (ThreatContainer::StorageType::const_iterator itr = threatList.begin(); itr != threatList.end(); ++itr) + for (ThreatReference const* ref : me->GetThreatMgr().GetUnsortedThreatList()) { - Unit* unit = ObjectAccessor::GetUnit(*me, (*itr)->getUnitGuid()); - - if (!(*itr)->GetThreat()) + if (!ref->GetThreat()) { // if target threat is 0 its fair, this prevents despawn in the case when // there is a bystander since UpdateVictim adds nearby enemies to the threatlist continue; } - if (UnitIsUnfair(unit)) + if (UnitIsUnfair(ref->GetVictim())) { return false; } @@ -129,7 +130,7 @@ bool NPCStaveQuestAI::IsFairFight() bool NPCStaveQuestAI::ValidThreatlist() { - if (threatList.size() == 1) + if (me->GetThreatMgr().GetThreatListSize() == 1) { return true; } @@ -232,7 +233,7 @@ void NPCStaveQuestAI::ResetState(uint32 aura = 0) if (InNormalForm()) { - me->m_Events.KillAllEvents(true); + me->m_Events.KillAllEvents(false); me->SetNpcFlag(UNIT_NPC_FLAG_GOSSIP); } diff --git a/src/server/scripts/World/npc_stave_of_ancients.h b/src/server/scripts/World/npc_stave_of_ancients.h index f21ac3751..cb4ab6e2d 100644 --- a/src/server/scripts/World/npc_stave_of_ancients.h +++ b/src/server/scripts/World/npc_stave_of_ancients.h @@ -137,7 +137,6 @@ struct NPCStaveQuestAI : public ScriptedAI ObjectGuid gossipPlayerGUID; ObjectGuid playerGUID; bool encounterStarted; - ThreatContainer::StorageType const& threatList = me->GetThreatMgr().GetThreatList(); std::map entryKeys = { { ARTORIUS_NORMAL_ENTRY, 1 }, diff --git a/src/server/scripts/World/npcs_special.cpp b/src/server/scripts/World/npcs_special.cpp index 59405ce01..ba6deb25d 100644 --- a/src/server/scripts/World/npcs_special.cpp +++ b/src/server/scripts/World/npcs_special.cpp @@ -312,123 +312,85 @@ public: } }; -enum eTrainingDummy +struct npc_training_dummy : NullCreatureAI { - SPELL_STUN_PERMANENT = 61204 -}; + npc_training_dummy(Creature* creature) : NullCreatureAI(creature) { } -class npc_training_dummy : public CreatureScript -{ -public: - npc_training_dummy() : CreatureScript("npc_training_dummy") { } - - struct npc_training_dummyAI : ScriptedAI + void JustEnteredCombat(Unit* who) override { - npc_training_dummyAI(Creature* creature) : ScriptedAI(creature) + _combatTimer[who->GetGUID()] = 5s; + } + + void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType damageType, SpellSchoolMask) override + { + damage = 0; + + if (!attacker || damageType == DOT) + return; + + _combatTimer[attacker->GetGUID()] = 5s; + } + + void UpdateAI(uint32 diff) override + { + for (auto itr = _combatTimer.begin(); itr != _combatTimer.end();) { - me->SetCombatMovement(false); - me->ApplySpellImmune(0, IMMUNITY_EFFECT, SPELL_EFFECT_KNOCK_BACK, true); //imune to knock aways like blast wave - } - - uint32 resetTimer; - - void Reset() override - { - me->CastSpell(me, SPELL_STUN_PERMANENT, true); - resetTimer = 5000; - } - - void EnterEvadeMode(EvadeReason why) override - { - if (!_EnterEvadeMode(why)) - return; - - Reset(); - } - - void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override - { - resetTimer = 5000; - damage = 0; - } - - void UpdateAI(uint32 diff) override - { - if (!UpdateVictim()) - return; - - if (resetTimer <= diff) + itr->second -= Milliseconds(diff); + if (itr->second <= 0s) { - EnterEvadeMode(EVADE_REASON_NO_HOSTILES); - resetTimer = 5000; + // The attacker has not dealt any damage to the dummy for over 5 seconds. End combat. + auto const& pveRefs = me->GetCombatManager().GetPvECombatRefs(); + auto it = pveRefs.find(itr->first); + if (it != pveRefs.end()) + it->second->EndCombat(); + + itr = _combatTimer.erase(itr); } else - resetTimer -= diff; + ++itr; } - - void MoveInLineOfSight(Unit* /*who*/) override { } - }; - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_training_dummyAI(creature); } + +private: + std::unordered_map _combatTimer; }; -class npc_target_dummy : public CreatureScript +struct npc_target_dummy : NullCreatureAI { -public: - npc_target_dummy() : CreatureScript("npc_target_dummy") { } - - struct npc_target_dummyAI : ScriptedAI + npc_target_dummy(Creature* creature) : NullCreatureAI(creature) { - npc_target_dummyAI(Creature* creature) : ScriptedAI(creature) - { - me->SetCombatMovement(false); - deathTimer = 15000; - me->ApplySpellImmune(0, IMMUNITY_EFFECT, SPELL_EFFECT_KNOCK_BACK, true); //imune to knock aways like blast wave - } + _deathTimer = 15s; + } - uint32 deathTimer; + void Reset() override + { + me->SetControlled(true, UNIT_STATE_STUNNED); + me->SetLootRecipient(me->GetOwner()); + me->SelectLevel(); + } - void Reset() override + void DamageTaken(Unit*, uint32& damage, DamageEffectType, SpellSchoolMask) override + { + damage = 0; + } + + void UpdateAI(uint32 diff) override + { + if (!me->HasUnitState(UNIT_STATE_STUNNED)) + me->SetControlled(true, UNIT_STATE_STUNNED); + + _deathTimer -= Milliseconds(diff); + if (_deathTimer <= 0s) { - me->SetControlled(true, UNIT_STATE_STUNNED); //disable rotate me->SetLootRecipient(me->GetOwner()); - me->SelectLevel(); + me->LowerPlayerDamageReq(me->GetMaxHealth()); + me->KillSelf(); + _deathTimer = 600s; } - - void EnterEvadeMode(EvadeReason why) override - { - if (!_EnterEvadeMode(why)) - return; - - Reset(); - } - - void UpdateAI(uint32 diff) override - { - if (!me->HasUnitState(UNIT_STATE_STUNNED)) - me->SetControlled(true, UNIT_STATE_STUNNED);//disable rotate - - if (deathTimer <= diff) - { - me->SetLootRecipient(me->GetOwner()); - me->LowerPlayerDamageReq(me->GetMaxHealth()); - me->KillSelf(); - deathTimer = 600000; - } - else - deathTimer -= diff; - } - - void MoveInLineOfSight(Unit* /*who*/) override { } - }; - - CreatureAI* GetAI(Creature* creature) const override - { - return new npc_target_dummyAI(creature); } + +private: + Milliseconds _deathTimer; }; /*######## @@ -2738,8 +2700,8 @@ void AddSC_npcs_special() { new npc_elder_clearwater(); new npc_riggle_bassbait(); - new npc_target_dummy(); - new npc_training_dummy(); + RegisterCreatureAI(npc_target_dummy); + RegisterCreatureAI(npc_training_dummy); new npc_venomhide_hatchling(); new npc_air_force_bots(); new npc_chicken_cluck(); diff --git a/src/test/mocks/TestCreature.cpp b/src/test/mocks/TestCreature.cpp new file mode 100644 index 000000000..cbe96b32c --- /dev/null +++ b/src/test/mocks/TestCreature.cpp @@ -0,0 +1,141 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "TestCreature.h" +#include "ThreatManager.h" +#include "CombatManager.h" + +// Heap-allocated to avoid static init calling CreatureMovementData() +// which requires sWorld to be set up (calls getIntConfig). +CreatureTemplate* TestCreature::_fakeCreatureTemplate = nullptr; +bool TestCreature::_fakeTemplateInitialized = false; + +TestCreature::TestCreature() : Creature() +{ +} + +TestCreature::~TestCreature() +{ + CleanupCombatState(); +} + +void TestCreature::CleanupCombatState() +{ + m_threatManager.ClearAllThreat(); + m_combatManager.EndAllCombat(); + // Must remove from world before destruction or ~Object will ABORT + SetInWorld(false); +} + +void TestCreature::ForceInitValues(ObjectGuid::LowType guidLow, uint32 entry) +{ + Object::_Create(guidLow, entry, HighGuid::Unit); + + m_objectType |= TYPEMASK_UNIT; + m_objectTypeId = TYPEID_UNIT; + + m_originalEntry = entry; + + // Initialize the fake creature template once with safe defaults. + // Heap-allocated because CreatureMovementData() constructor needs sWorld. + if (!_fakeTemplateInitialized) + { + _fakeCreatureTemplate = new CreatureTemplate(); + _fakeCreatureTemplate->Entry = 0; + _fakeCreatureTemplate->faction = 14; // hostile monster default + _fakeCreatureTemplate->unit_class = 1; // CLASS_WARRIOR + _fakeCreatureTemplate->speed_walk = 1.0f; + _fakeCreatureTemplate->speed_run = 1.14286f; + _fakeCreatureTemplate->speed_swim = 1.0f; + _fakeCreatureTemplate->speed_flight = 1.0f; + _fakeCreatureTemplate->scale = 1.0f; + _fakeCreatureTemplate->DamageModifier = 1.0f; + _fakeCreatureTemplate->BaseAttackTime = 2000; + _fakeCreatureTemplate->RangeAttackTime = 2000; + _fakeCreatureTemplate->BaseVariance = 1.0f; + _fakeCreatureTemplate->RangeVariance = 1.0f; + _fakeCreatureTemplate->ModHealth = 1.0f; + _fakeCreatureTemplate->ModMana = 1.0f; + _fakeCreatureTemplate->ModArmor = 1.0f; + _fakeCreatureTemplate->ModExperience = 1.0f; + _fakeCreatureTemplate->HoverHeight = 1.0f; + _fakeCreatureTemplate->detection_range = 20.0f; + _fakeCreatureTemplate->flags_extra = 0; + _fakeCreatureTemplate->unit_flags = 0; + _fakeCreatureTemplate->unit_flags2 = 0; + _fakeCreatureTemplate->dynamicflags = 0; + _fakeCreatureTemplate->type = 0; + _fakeCreatureTemplate->type_flags = 0; + // Movement is default-constructed by new CreatureTemplate(): + // Ground=Run, Swim=true -> CanWalk() and CanSwim() return true + _fakeTemplateInitialized = true; + } + + m_creatureInfo = _fakeCreatureTemplate; +} + +void TestCreature::SetTestMap(Map* map) +{ + _testMap = map; + // Also set the base class map pointer so GetMap() works + // through the Unit* base pointer (polymorphic calls) + WorldObject::SetMap(map); +} + +void TestCreature::SetAlive(bool alive) +{ + m_deathState = alive ? DeathState::Alive : DeathState::Dead; +} + +void TestCreature::SetInWorld(bool inWorld) +{ + if (inWorld && !Object::IsInWorld()) + Object::AddToWorld(); + else if (!inWorld && Object::IsInWorld()) + Object::RemoveFromWorld(); +} + +void TestCreature::SetPhase(uint32 phase) +{ + SetPhaseMask(phase, false); +} + +void TestCreature::SetFaction(uint32 faction) +{ + // Set faction directly, bypassing Unit::SetFaction which calls + // UpdateMoveInLineOfSightState() -> sObjectMgr->GetCreatureTemplate() + SetUInt32Value(UNIT_FIELD_FACTIONTEMPLATE, faction); +} + +void TestCreature::SetupForCombatTest(Map* map, ObjectGuid::LowType guidLow, uint32 entry) +{ + ForceInitValues(guidLow, entry); + // SetTestMap calls WorldObject::SetMap which asserts !IsInWorld(), + // so we must set map BEFORE SetInWorld + SetTestMap(map); + SetInWorld(true); + SetAlive(true); + SetPhase(1); + SetHostileFaction(); + SetIsCombatDisallowed(false); + ClearUnitState(UNIT_STATE_EVADE); + ClearUnitState(UNIT_STATE_IN_FLIGHT); + InitializeThreatManager(); +} + +void TestCreature::InitializeThreatManager() +{ + m_threatManager.Initialize(); +} diff --git a/src/test/mocks/TestCreature.h b/src/test/mocks/TestCreature.h new file mode 100644 index 000000000..0dfe09f66 --- /dev/null +++ b/src/test/mocks/TestCreature.h @@ -0,0 +1,87 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TEST_CREATURE_H +#define TEST_CREATURE_H + +#include "Creature.h" +#include "CreatureData.h" +#include "ObjectGuid.h" + +class TestMap; + +/** + * TestCreature - A test harness for Creature that bypasses database dependencies. + * + * Usage: + * TestCreature* creature = new TestCreature(); + * creature->ForceInitValues(1, 12345); // guidLow, entry + * creature->SetTestMap(testMap); + * creature->SetAlive(true); + * creature->SetupForCombatTest(); // Sets up all necessary state for combat/threat tests + */ +class TestCreature : public Creature +{ +public: + TestCreature(); + ~TestCreature() override; + + // Override methods that require database/world access + void UpdateObjectVisibility(bool /*forced*/ = true, bool /*fromUpdate*/ = false) override { } + void AddToWorld() override { } + void RemoveFromWorld() override { } + + // Force initialization without database + void ForceInitValues(ObjectGuid::LowType guidLow, uint32 entry); + + // Test control methods + void SetTestMap(Map* map); + + // Set alive state (affects m_deathState) + void SetAlive(bool alive); + + // Set in-world state (affects m_inWorld) + void SetInWorld(bool inWorld); + + // Set phase mask for phase checks + void SetPhase(uint32 phase); + + // Set faction for friendliness checks + // Use hostile factions (14 = hostile monster) for combat tests + void SetHostileFaction() { SetFaction(14); } + void SetFriendlyFaction() { SetFaction(35); } + void SetFaction(uint32 faction); + + // Complete setup for combat/threat testing + // Sets creature to be alive, in-world, hostile, and initializes managers + void SetupForCombatTest(Map* map, ObjectGuid::LowType guidLow, uint32 entry); + + // Initialize ThreatManager for testing + void InitializeThreatManager(); + + // Access managers directly for testing + ThreatManager& TestGetThreatMgr() { return m_threatManager; } + CombatManager& TestGetCombatMgr() { return m_combatManager; } + + // Clear all combat state for cleanup + void CleanupCombatState(); + +private: + Map* _testMap = nullptr; + static CreatureTemplate* _fakeCreatureTemplate; + static bool _fakeTemplateInitialized; +}; + +#endif // TEST_CREATURE_H diff --git a/src/test/mocks/TestMap.cpp b/src/test/mocks/TestMap.cpp new file mode 100644 index 000000000..012f6eeee --- /dev/null +++ b/src/test/mocks/TestMap.cpp @@ -0,0 +1,62 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "TestMap.h" +#include "DBCStores.h" +#include "ScriptMgr.h" +#include "ScriptDefines/AllMapScript.h" +#include "ScriptDefines/GlobalScript.h" +#include "ScriptDefines/MiscScript.h" +#include "ScriptDefines/UnitScript.h" +#include "ScriptDefines/WorldObjectScript.h" + +TestMap::TestMap() + : Map(0, 0, REGULAR_DIFFICULTY, nullptr) +{ + _fakeMapEntry = {}; + _fakeMapEntry.map_type = MAP_COMMON; + _fakeMapEntry.MapID = 0; + const_cast(i_mapEntry) = &_fakeMapEntry; +} + +TestMap::~TestMap() +{ +} + +/*static*/ void TestMap::EnsureDBC() +{ + static bool initialized = false; + if (initialized) + return; + initialized = true; + + // Insert a fake MapEntry so Map constructor doesn't crash + if (!sMapStore.LookupEntry(0)) + { + auto* entry = new MapEntry{}; + entry->MapID = 0; + entry->map_type = MAP_COMMON; + entry->entrance_map = -1; + sMapStore.SetEntry(0, entry); + } + + // Initialize all script registries so CALL_ENABLED_HOOKS doesn't + // crash on uninitialized vectors during Object/Unit/Map operations + ScriptRegistry::InitEnabledHooksIfNeeded(ALLMAPHOOK_END); + ScriptRegistry::InitEnabledHooksIfNeeded(GLOBALHOOK_END); + ScriptRegistry::InitEnabledHooksIfNeeded(MISCHOOK_END); + ScriptRegistry::InitEnabledHooksIfNeeded(UNITHOOK_END); + ScriptRegistry::InitEnabledHooksIfNeeded(WORLDOBJECTHOOK_END); +} diff --git a/src/test/mocks/TestMap.h b/src/test/mocks/TestMap.h new file mode 100644 index 000000000..07bfa4638 --- /dev/null +++ b/src/test/mocks/TestMap.h @@ -0,0 +1,51 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#ifndef TEST_MAP_H +#define TEST_MAP_H + +#include "Map.h" +#include "DBCStructure.h" + +/** + * TestMap - A minimal test harness for Map. + * + * Since Map has a complex constructor requiring MapEntry from DBC, + * this creates a fake MapEntry to control IsRaid()/IsDungeon() behavior. + * + * Usage: + * TestMap* map = new TestMap(); + * map->SetIsRaid(true); // Make this a raid map + */ +class TestMap : public Map +{ +public: + TestMap(); + ~TestMap() override; + + // Must be called before constructing TestMap to insert fake DBC entry + // and initialize script registries + static void EnsureDBC(); + + // Control map type for testing + void SetMapType(uint32 type) { _fakeMapEntry.map_type = type; } + void SetIsRaid(bool val) { _fakeMapEntry.map_type = val ? MAP_RAID : MAP_COMMON; } + void SetIsDungeon(bool val) { _fakeMapEntry.map_type = val ? MAP_INSTANCE : MAP_COMMON; } + +private: + MapEntry _fakeMapEntry; +}; + +#endif // TEST_MAP_H diff --git a/src/test/server/game/Combat/CombatManagerTest.cpp b/src/test/server/game/Combat/CombatManagerTest.cpp new file mode 100644 index 000000000..537d68665 --- /dev/null +++ b/src/test/server/game/Combat/CombatManagerTest.cpp @@ -0,0 +1,1320 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "CombatManager.h" +#include "ThreatManager.h" +#include "DBCStores.h" +#include "TestCreature.h" +#include "TestMap.h" +#include "WorldMock.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace testing; + +namespace +{ + +// ============================================================================ +// Integration tests for CombatManager and evade timer system. +// Uses TestCreature/TestMap with hostile DBC faction entries. +// ============================================================================ +class CombatManagerIntegrationTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _previousWorld = std::move(sWorld); + _worldMock = new NiceMock(); + + ON_CALL(*_worldMock, getIntConfig(_)).WillByDefault(Return(0)); + ON_CALL(*_worldMock, getFloatConfig(_)).WillByDefault(Return(1.0f)); + ON_CALL(*_worldMock, getBoolConfig(_)).WillByDefault(Return(false)); + + sWorld.reset(_worldMock); + + // Insert two mutually hostile DBC faction entries + auto* factionA = new FactionTemplateEntry{}; + factionA->ID = 90001; + factionA->faction = 90001; + factionA->factionFlags = 0; + factionA->ourMask = 1; + factionA->friendlyMask = 0; + factionA->hostileMask = 2; + for (auto& e : factionA->enemyFaction) e = 0; + for (auto& f : factionA->friendFaction) f = 0; + sFactionTemplateStore.SetEntry(90001, factionA); + + auto* factionB = new FactionTemplateEntry{}; + factionB->ID = 90002; + factionB->faction = 90002; + factionB->factionFlags = 0; + factionB->ourMask = 2; + factionB->friendlyMask = 0; + factionB->hostileMask = 1; + for (auto& e : factionB->enemyFaction) e = 0; + for (auto& f : factionB->friendFaction) f = 0; + sFactionTemplateStore.SetEntry(90002, factionB); + + TestMap::EnsureDBC(); + _map = new TestMap(); + + _creatureA = new TestCreature(); + _creatureA->SetupForCombatTest(_map, 1, 12345); + _creatureA->SetFaction(90001); + + _creatureB = new TestCreature(); + _creatureB->SetupForCombatTest(_map, 2, 12346); + _creatureB->SetFaction(90002); + } + + void TearDown() override + { + _creatureA->CleanupCombatState(); + _creatureB->CleanupCombatState(); + delete _creatureA; + delete _creatureB; + delete _map; + sWorld = std::move(_previousWorld); + } + + std::unique_ptr _previousWorld; + NiceMock* _worldMock = nullptr; + TestMap* _map = nullptr; + TestCreature* _creatureA = nullptr; + TestCreature* _creatureB = nullptr; +}; + +// ============================================================================ +// SetInCombatWith Tests +// ============================================================================ + +// cppcheck-suppress syntaxError +TEST_F(CombatManagerIntegrationTest, SetInCombatWith_CreatesReference) +{ + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + + bool result = _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + EXPECT_TRUE(result); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + // Combat is bidirectional + EXPECT_TRUE(_creatureB->TestGetCombatMgr().HasCombat()); + EXPECT_TRUE(_creatureB->TestGetCombatMgr().IsInCombatWith(_creatureA)); +} + +TEST_F(CombatManagerIntegrationTest, SetInCombatWith_SameUnit_Fails) +{ + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureA)); +} + +TEST_F(CombatManagerIntegrationTest, SetInCombatWith_DeadUnit_Fails) +{ + _creatureB->SetAlive(false); + + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); +} + +TEST_F(CombatManagerIntegrationTest, SetInCombatWith_DifferentPhase_Fails) +{ + _creatureB->SetPhase(2); + + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); +} + +// ============================================================================ +// EndAllPvECombat Tests +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EndAllPvECombat_ClearsAll) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->TestGetCombatMgr().EndAllPvECombat(); + + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); +} + +TEST_F(CombatManagerIntegrationTest, EndAllPvECombat_AlsoClearsThreat) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + + _creatureA->TestGetCombatMgr().EndAllPvECombat(); + + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); +} + +// ============================================================================ +// HasCombat Tests +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, HasCombat_ReflectsState) +{ + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->TestGetCombatMgr().EndAllPvECombat(); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); +} + +// ============================================================================ +// Evade Timer Tests +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_StartAndExpiry) +{ + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInEvadeMode()); + + _creatureA->TestGetCombatMgr().StartEvadeTimer(); + + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeState_Transitions) +{ + EXPECT_EQ(_creatureA->TestGetCombatMgr().GetEvadeState(), EVADE_STATE_NONE); + + _creatureA->TestGetCombatMgr().SetEvadeState(EVADE_STATE_HOME); + EXPECT_EQ(_creatureA->TestGetCombatMgr().GetEvadeState(), EVADE_STATE_HOME); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsEvadingHome()); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInEvadeMode()); + + _creatureA->TestGetCombatMgr().StopEvade(); + EXPECT_EQ(_creatureA->TestGetCombatMgr().GetEvadeState(), EVADE_STATE_NONE); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInEvadeMode()); +} + +// ============================================================================ +// ContinueEvadeRegen Tests +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, ContinueEvadeRegen_KeepsRegenActive) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // StartEvadeTimer begins at 10s — NOT in regen zone yet + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_FALSE(cm.IsEvadeRegen()); + + // ContinueEvadeRegen sets timer to regen threshold + cm.ContinueEvadeRegen(); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_TRUE(cm.IsEvadeRegen()); + + // After partial countdown, still in regen zone + cm.Update(3000); + EXPECT_TRUE(cm.IsEvadeRegen()); +} + +TEST_F(CombatManagerIntegrationTest, ContinueEvadeRegen_StopEvadeClearsIt) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.ContinueEvadeRegen(); + EXPECT_TRUE(cm.IsEvadeRegen()); + + cm.StopEvade(); + EXPECT_FALSE(cm.IsEvadeRegen()); + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +// ============================================================================ +// Constants Tests (compacted) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EvadeConstants) +{ + constexpr uint32 duration = CombatManager::EVADE_TIMER_DURATION; + constexpr uint32 delay = CombatManager::EVADE_REGEN_DELAY; + constexpr uint32 pvpTimeout = PvPCombatReference::PVP_COMBAT_TIMEOUT; + + EXPECT_EQ(duration, 10000u); + EXPECT_EQ(delay, 5000u); + EXPECT_EQ(pvpTimeout, 5000u); + EXPECT_EQ(delay * 2, duration); + + EXPECT_EQ(static_cast(EVADE_STATE_NONE), 0); + EXPECT_EQ(static_cast(EVADE_STATE_COMBAT), 1); + EXPECT_EQ(static_cast(EVADE_STATE_HOME), 2); +} + +// ============================================================================ +// Individual CombatReference::EndCombat Tests (Issue #4 regression) +// Used by SetImmuneToPC/NPC to end specific combat references +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EndCombat_SingleReference_KeepsOthers) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // A in combat with both B and C + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + _creatureA->TestGetCombatMgr().SetInCombatWith(creatureC); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + + // End only the reference with B + auto const& refs = _creatureA->TestGetCombatMgr().GetPvECombatRefs(); + CombatReference* refToEnd = nullptr; + for (auto const& pair : refs) + if (pair.second->GetOther(_creatureA) == _creatureB) + refToEnd = pair.second; + + ASSERT_NE(refToEnd, nullptr); + refToEnd->EndCombat(); + + // B reference gone, C reference remains + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(CombatManagerIntegrationTest, EndCombat_AlsoClearsThreatForThatRef) +{ + // Add threat (which implies combat) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // End the combat reference directly + auto const& refs = _creatureA->TestGetCombatMgr().GetPvECombatRefs(); + ASSERT_FALSE(refs.empty()); + auto it = refs.begin(); + it->second->EndCombat(); + + // Both combat and threat should be cleared + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); +} + +TEST_F(CombatManagerIntegrationTest, EndCombat_BidirectionalCleanup) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + // End from A's side + auto const& refs = _creatureA->TestGetCombatMgr().GetPvECombatRefs(); + ASSERT_FALSE(refs.empty()); + refs.begin()->second->EndCombat(); + + // Both sides should be cleaned up + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + EXPECT_FALSE(_creatureB->TestGetCombatMgr().HasCombat()); +} + +// ============================================================================ +// SetImmuneToNPC Tests (selective EndCombat by unit flag) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SetImmuneToNPC_EndsCombatWithNPCs) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // A in combat with both B and C (all NPCs, none have UNIT_FLAG_PLAYER_CONTROLLED) + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + _creatureA->TestGetCombatMgr().SetInCombatWith(creatureC); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + // SetImmuneToNPC should end combat with non-player-controlled units + _creatureA->SetImmuneToNPC(true, false); + + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->SetImmuneToNPC(false, false); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(CombatManagerIntegrationTest, SetImmuneToNPC_KeepCombat_PreservesRefs) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + // keepCombat=true should not end any combat + _creatureA->SetImmuneToNPC(true, true); + + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + _creatureA->SetImmuneToNPC(false, false); +} + +// ============================================================================ +// StopAttackFaction Tests (EndCombat instead of ClearThreat) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, StopAttackFaction_EndsCombatForFaction) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); // same faction as A + + // A in combat with B (faction 90002), threat too + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + + // Also have C fight B + creatureC->TestGetThreatMgr().AddThreat(_creatureB, 200.0f); + EXPECT_TRUE(creatureC->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // A stops attacking faction 90002 (B's faction) + _creatureA->StopAttackFaction(90002); + + // A should no longer be in combat with B, and threat should be cleared + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + + // C's combat with B should be unaffected + EXPECT_TRUE(creatureC->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// ClearInCombat Tests (deferred flag removal) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, ClearInCombat_ClearsAllState) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->ClearInCombat(); + + // All combat and threat should be cleared + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + EXPECT_FALSE(_creatureA->IsInCombat()); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); +} + +// ============================================================================ +// Evade Timer Flow Tests +// Tests the complete evade timer lifecycle: +// StartEvadeTimer -> countdown -> expiry -> EvadeTimerExpired callback +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_PartialCountdown_StillActive) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Advance 3 seconds — timer started at 10s, should still be active + cm.Update(3000); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Advance another 3 seconds — 6s total, still active + cm.Update(3000); + EXPECT_TRUE(cm.IsInEvadeMode()); + + cm.StopEvade(); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_FullCountdown_Expires) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Advance the full 10 seconds — timer should expire + // Note: EvadeTimerExpired would be called but no AI is set in tests, + // so it's a no-op. We verify the timer state itself. + cm.Update(CombatManager::EVADE_TIMER_DURATION); + + // Timer has expired and no evade state was set by AI callback + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_Overshoot_StillExpires) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + + // Advance more than the timer duration + cm.Update(CombatManager::EVADE_TIMER_DURATION + 5000); + + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_IncrementalCountdown_Expires) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + + // Advance in 1-second increments + for (uint32 i = 0; i < 9; ++i) + { + cm.Update(1000); + EXPECT_TRUE(cm.IsInEvadeMode()) << "Timer should be active at " << (i + 1) << "s"; + } + + // 10th second — should expire + cm.Update(1000); + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_StopEvade_CancelsBeforeExpiry) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + cm.Update(5000); // 5 seconds in + EXPECT_TRUE(cm.IsInEvadeMode()); + + cm.StopEvade(); + EXPECT_FALSE(cm.IsInEvadeMode()); + + // Further updates should not re-trigger anything + cm.Update(10000); + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_RestartAfterStop_WorksAgain) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + cm.Update(5000); + cm.StopEvade(); + EXPECT_FALSE(cm.IsInEvadeMode()); + + // Restart the timer + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + + cm.Update(5000); + EXPECT_TRUE(cm.IsInEvadeMode()); // Still active — 5s into new 10s timer + + cm.Update(5000); + EXPECT_FALSE(cm.IsInEvadeMode()); // Expired +} + +// ============================================================================ +// Evade Timer + Regen Phase Interaction +// Tests the IsEvadeRegen state machine. +// Timer > EVADE_REGEN_DELAY: not in regen (just counting down) +// Timer <= EVADE_REGEN_DELAY: in regen (creature should regenerate) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_InitialStart_NotInRegen) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // StartEvadeTimer sets timer to 10s — above the 5s regen threshold + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_FALSE(cm.IsEvadeRegen()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_CountdownToRegenPhase) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + + // Advance 5 seconds — timer is now at exactly EVADE_REGEN_DELAY (5000) + cm.Update(CombatManager::EVADE_TIMER_DURATION - CombatManager::EVADE_REGEN_DELAY); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_TRUE(cm.IsEvadeRegen()); // timer <= EVADE_REGEN_DELAY + + // Advance 1 more second — timer at 4000, still in regen + cm.Update(1000); + EXPECT_TRUE(cm.IsEvadeRegen()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_ContinueEvadeRegen_ResetsToRegenDelay) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // Start from a non-regen state + cm.StartEvadeTimer(); + EXPECT_FALSE(cm.IsEvadeRegen()); + + // ContinueEvadeRegen jumps the timer directly to the regen delay value + cm.ContinueEvadeRegen(); + EXPECT_TRUE(cm.IsEvadeRegen()); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Advance 4 seconds — timer at 1000, still regen + cm.Update(4000); + EXPECT_TRUE(cm.IsEvadeRegen()); + + // Advance final 1 second — timer expires + cm.Update(1000); + EXPECT_FALSE(cm.IsInEvadeMode()); + EXPECT_FALSE(cm.IsEvadeRegen()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_ContinueEvadeRegen_RepeatKeepsRegen) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // In raids, ContinueEvadeRegen is called repeatedly to keep regen going + cm.ContinueEvadeRegen(); + EXPECT_TRUE(cm.IsEvadeRegen()); + + // Advance 3 seconds + cm.Update(3000); + EXPECT_TRUE(cm.IsEvadeRegen()); + + // Call ContinueEvadeRegen again (simulates raid behavior — reset timer) + cm.ContinueEvadeRegen(); + EXPECT_TRUE(cm.IsEvadeRegen()); + + // Advance another 3 seconds (timer reset to 5s, so 3s in, still active) + cm.Update(3000); + EXPECT_TRUE(cm.IsEvadeRegen()); +} + +// ============================================================================ +// Evade State + Timer Interaction Tests +// Tests how SetEvadeState interacts with the evade timer. +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EvadeState_SetToNone_ClearsTimer) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // SetEvadeState(NONE) has an early return if _evadeState is already NONE. + // Set to HOME first so that the NONE transition actually executes. + cm.SetEvadeState(EVADE_STATE_HOME); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Now setting to NONE should clear the timer + cm.SetEvadeState(EVADE_STATE_NONE); + EXPECT_FALSE(cm.IsInEvadeMode()); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); +} + +TEST_F(CombatManagerIntegrationTest, EvadeState_HomeState_IsInEvadeMode) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.SetEvadeState(EVADE_STATE_HOME); + + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_TRUE(cm.IsEvadingHome()); + EXPECT_TRUE(cm.IsEvadeRegen()); // _evadeState != NONE triggers regen +} + +TEST_F(CombatManagerIntegrationTest, EvadeState_CombatState_IsInEvadeMode) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + cm.SetEvadeState(EVADE_STATE_COMBAT); + + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_FALSE(cm.IsEvadingHome()); + EXPECT_TRUE(cm.IsEvadeRegen()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeState_FullLifecycle) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // Phase 1: Target unreachable → start evade timer + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); + EXPECT_FALSE(cm.IsInEvadeMode()); + + cm.StartEvadeTimer(); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_FALSE(cm.IsEvadeRegen()); // Timer at 10s, above regen threshold + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); // State not changed yet + + // Phase 2: Timer counts down past regen threshold + cm.Update(6000); // 4s left + EXPECT_TRUE(cm.IsEvadeRegen()); + + // Phase 3: Timer expires + cm.Update(4000); + EXPECT_FALSE(cm.IsInEvadeMode()); // No AI to set state + + // Phase 4: Simulate what AI would do — set state to HOME + cm.SetEvadeState(EVADE_STATE_HOME); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_TRUE(cm.IsEvadingHome()); + + // Phase 5: Creature arrives home — state cleared + cm.SetEvadeState(EVADE_STATE_NONE); + EXPECT_FALSE(cm.IsInEvadeMode()); + EXPECT_FALSE(cm.IsEvadingHome()); + EXPECT_FALSE(cm.IsEvadeRegen()); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_StopEvade_WithActiveState_ClearsState) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // Set evade state (no timer involved) + cm.SetEvadeState(EVADE_STATE_HOME); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // StopEvade should clear the state + cm.StopEvade(); + EXPECT_FALSE(cm.IsInEvadeMode()); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); +} + +TEST_F(CombatManagerIntegrationTest, EvadeTimer_StopEvade_TimerAndState_ClearsBoth) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // Both timer and state active + cm.StartEvadeTimer(); + cm.SetEvadeState(EVADE_STATE_COMBAT); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // StopEvade clears both timer and state in a single call + cm.StopEvade(); + EXPECT_FALSE(cm.IsInEvadeMode()); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); +} + +// ============================================================================ +// SetCannotReachTarget Integration Tests +// Verifies Creature::SetCannotReachTarget starts/stops evade timer. +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SetCannotReachTarget_StartsEvadeTimer) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + EXPECT_FALSE(cm.IsInEvadeMode()); + + // Setting a target as unreachable starts the evade timer + _creatureA->SetCannotReachTarget(_creatureB->GetGUID()); + EXPECT_TRUE(cm.IsInEvadeMode()); + EXPECT_TRUE(_creatureA->CanNotReachTarget()); +} + +TEST_F(CombatManagerIntegrationTest, SetCannotReachTarget_Clear_StopsEvade) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + _creatureA->SetCannotReachTarget(_creatureB->GetGUID()); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Clear unreachable target — should stop evade + _creatureA->SetCannotReachTarget(); + EXPECT_FALSE(cm.IsInEvadeMode()); + EXPECT_FALSE(_creatureA->CanNotReachTarget()); +} + +TEST_F(CombatManagerIntegrationTest, SetCannotReachTarget_SameTarget_NoDuplicate) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + _creatureA->SetCannotReachTarget(_creatureB->GetGUID()); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Count down 5 seconds + cm.Update(5000); + + // Setting the same target again should be a no-op (early return) + _creatureA->SetCannotReachTarget(_creatureB->GetGUID()); + + // Timer should NOT have been restarted — still mid-countdown + // Advance 5 more seconds, timer should expire (was at 5s remaining) + cm.Update(5000); + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, IsNotReachableAndNeedRegen_Integration) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + EXPECT_FALSE(_creatureA->IsNotReachableAndNeedRegen()); + + // Set unreachable — starts timer at 10s, not in regen yet + _creatureA->SetCannotReachTarget(_creatureB->GetGUID()); + EXPECT_FALSE(_creatureA->IsNotReachableAndNeedRegen()); + + // Advance past the regen threshold + cm.Update(6000); // 4s left, within regen range + EXPECT_TRUE(_creatureA->IsNotReachableAndNeedRegen()); + + // Clear unreachable — regen stops + _creatureA->SetCannotReachTarget(); + EXPECT_FALSE(_creatureA->IsNotReachableAndNeedRegen()); +} + +// ============================================================================ +// CombatReference Suppression Tests +// Verifies that suppressed combat refs don't generate combat state. +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SuppressedRef_DoesNotGenerateCombat) +{ + // Create combat with second unit suppressed + bool result = _creatureA->TestGetCombatMgr().SetInCombatWith( + _creatureB, /* addSecondUnitSuppressed */ true); + EXPECT_TRUE(result); + + // A should be in combat (not suppressed) + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); + // B's side is suppressed — check HasPvECombat (which skips suppressed refs) + EXPECT_FALSE(_creatureB->TestGetCombatMgr().HasPvECombat()); + + // But the reference still exists on both sides + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_TRUE(_creatureB->TestGetCombatMgr().IsInCombatWith(_creatureA)); +} + +// ============================================================================ +// GAP COVERAGE: GetAnyTarget (CombatManager) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, GetAnyTarget_NoCombat_ReturnsNull) +{ + EXPECT_EQ(_creatureA->TestGetCombatMgr().GetAnyTarget(), nullptr); +} + +TEST_F(CombatManagerIntegrationTest, GetAnyTarget_WithCombat_ReturnsTarget) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + Unit* target = _creatureA->TestGetCombatMgr().GetAnyTarget(); + EXPECT_EQ(target, _creatureB); +} + +TEST_F(CombatManagerIntegrationTest, GetAnyTarget_SuppressedOnly_ReturnsNull) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB, true); + // A's ref is not suppressed, but B's is + EXPECT_NE(_creatureA->TestGetCombatMgr().GetAnyTarget(), nullptr); + // B's only ref is suppressed, so GetAnyTarget skips it + EXPECT_EQ(_creatureB->TestGetCombatMgr().GetAnyTarget(), nullptr); +} + +// ============================================================================ +// GAP COVERAGE: HasPvECombatWithPlayers (creatures only → always false) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, HasPvECombatWithPlayers_CreaturesOnly_ReturnsFalse) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + // All our units are creatures, not players + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasPvECombatWithPlayers()); +} + +// ============================================================================ +// GAP COVERAGE: EndAllCombat +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EndAllCombat_ClearsEverything) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetCombatMgr().SetInCombatWith(creatureC); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->TestGetCombatMgr().EndAllCombat(); + + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// GAP COVERAGE: EndAllPvPCombat (no PvP refs with creatures, verify no crash) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EndAllPvPCombat_NoRefs_DoesNotCrash) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + // PvE combat exists but no PvP + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasPvPCombat()); + + // Should not crash, should not affect PvE + _creatureA->TestGetCombatMgr().EndAllPvPCombat(); + + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); +} + +// ============================================================================ +// GAP COVERAGE: InheritCombatStatesFrom +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, InheritCombatStatesFrom_InheritsPvERefs) +{ + // C must be faction 90001 (hostile to B's 90002) so it can enter combat with B + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); + + // A is in combat with B + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(creatureC->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // C inherits A's combat states — should now be in combat with B + creatureC->TestGetCombatMgr().InheritCombatStatesFrom(_creatureA); + + EXPECT_TRUE(creatureC->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(CombatManagerIntegrationTest, InheritCombatStatesFrom_SkipsDuplicate) +{ + // C must be faction 90001 (hostile to B's 90002) so it can enter combat with B + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); + + // Both A and C already in combat with B + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + creatureC->TestGetCombatMgr().SetInCombatWith(_creatureB); + + // Should be a no-op (already in combat with B) + creatureC->TestGetCombatMgr().InheritCombatStatesFrom(_creatureA); + + EXPECT_TRUE(creatureC->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(CombatManagerIntegrationTest, InheritCombatStatesFrom_SkipsImmuneTargets) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + // Make C immune to NPCs — should skip inheriting combat with B + creatureC->SetImmuneToNPC(true, false); + creatureC->TestGetCombatMgr().InheritCombatStatesFrom(_creatureA); + + EXPECT_FALSE(creatureC->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + creatureC->SetImmuneToNPC(false, false); + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// GAP COVERAGE: EndCombatBeyondRange +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, EndCombatBeyondRange_InRange_KeepsCombat) +{ + // Both creatures at origin (0,0,0) — distance is 0 + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // Range of 50 — creatures are well within range + _creatureA->TestGetCombatMgr().EndCombatBeyondRange(50.0f); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); +} + +TEST_F(CombatManagerIntegrationTest, EndCombatBeyondRange_OutOfRange_EndsCombat) +{ + // Place B far away + _creatureB->Relocate(200.0f, 200.0f, 0.0f); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // Range of 50 — B is ~283 units away + _creatureA->TestGetCombatMgr().EndCombatBeyondRange(50.0f); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); +} + +TEST_F(CombatManagerIntegrationTest, EndCombatBeyondRange_MixedDistances_EndsOnlyFar) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B at origin (close), C far away + creatureC->Relocate(200.0f, 200.0f, 0.0f); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + _creatureA->TestGetCombatMgr().SetInCombatWith(creatureC); + + _creatureA->TestGetCombatMgr().EndCombatBeyondRange(50.0f); + + // B should remain, C should be gone + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// GAP COVERAGE: RevalidateCombat +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, RevalidateCombat_ValidRefs_KeepsAll) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + _creatureA->TestGetCombatMgr().RevalidateCombat(); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); +} + +TEST_F(CombatManagerIntegrationTest, RevalidateCombat_InvalidRef_RemovesIt) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // Make B dead — CanBeginCombat will now return false + _creatureB->SetAlive(false); + + _creatureA->TestGetCombatMgr().RevalidateCombat(); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + _creatureB->SetAlive(true); +} + +TEST_F(CombatManagerIntegrationTest, RevalidateCombat_DifferentPhase_RemovesIt) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + _creatureB->SetPhase(2); + + _creatureA->TestGetCombatMgr().RevalidateCombat(); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); +} + +// ============================================================================ +// GAP COVERAGE: CombatReference::Refresh (Un-suppress) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, CombatRefresh_UnsuppressesRef) +{ + // Create combat with B suppressed + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB, true); + EXPECT_FALSE(_creatureB->TestGetCombatMgr().HasPvECombat()); // B is suppressed + + // SetInCombatWith again should refresh the existing ref (un-suppress) + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureB->TestGetCombatMgr().HasPvECombat()); // B un-suppressed +} + +// ============================================================================ +// GAP COVERAGE: CombatReference::SuppressFor +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SuppressFor_SuppressesOneSide) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); + EXPECT_TRUE(_creatureB->TestGetCombatMgr().HasPvECombat()); + + // Suppress B's side + auto const& refs = _creatureA->TestGetCombatMgr().GetPvECombatRefs(); + ASSERT_FALSE(refs.empty()); + refs.begin()->second->SuppressFor(_creatureB); + + // A still in combat, B no longer + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); + EXPECT_FALSE(_creatureB->TestGetCombatMgr().HasPvECombat()); + + // But reference still exists on both sides + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_TRUE(_creatureB->TestGetCombatMgr().IsInCombatWith(_creatureA)); +} + +// ============================================================================ +// GAP COVERAGE: CanBeginCombat Edge Cases +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, CanBeginCombat_UnitStateEvade_Fails) +{ + _creatureB->AddUnitState(UNIT_STATE_EVADE); + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); + _creatureB->ClearUnitState(UNIT_STATE_EVADE); +} + +TEST_F(CombatManagerIntegrationTest, CanBeginCombat_UnitStateInFlight_Fails) +{ + _creatureB->AddUnitState(UNIT_STATE_IN_FLIGHT); + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); + _creatureB->ClearUnitState(UNIT_STATE_IN_FLIGHT); +} + +TEST_F(CombatManagerIntegrationTest, CanBeginCombat_CombatDisallowed_Fails) +{ + _creatureB->SetIsCombatDisallowed(true); + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); + _creatureB->SetIsCombatDisallowed(false); +} + +TEST_F(CombatManagerIntegrationTest, CanBeginCombat_FriendlyFactions_Fails) +{ + // Set both to same faction — they should be friendly + _creatureB->SetFaction(90001); // same as A + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); +} + +TEST_F(CombatManagerIntegrationTest, CanBeginCombat_ValidUnits_Succeeds) +{ + EXPECT_TRUE(CombatManager::CanBeginCombat(_creatureA, _creatureB)); +} + +// ============================================================================ +// GAP COVERAGE: CombatManager::IsInCombatWith (ObjectGuid variant) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, IsInCombatWith_ObjectGuid_Variant) +{ + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB->GetGUID())); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB->GetGUID())); +} + +// ============================================================================ +// GAP COVERAGE: CombatManager::Update with evade timer and no PvP refs +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, Update_NoPvPRefs_DoesNotCrash) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + // Update should handle PvP iteration gracefully when no PvP refs exist + _creatureA->TestGetCombatMgr().Update(1000); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); +} + +TEST_F(CombatManagerIntegrationTest, Update_EvadeTimerAndCombat_BothWork) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + _creatureA->TestGetCombatMgr().StartEvadeTimer(); + + // Both combat and evade timer are active + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInEvadeMode()); + + // Update ticks both + _creatureA->TestGetCombatMgr().Update(5000); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); // combat unchanged + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInEvadeMode()); // timer still active +} + +// ============================================================================ +// GAP COVERAGE: CombatReference::GetOther +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, CombatReference_GetOther_ReturnsPeer) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + auto const& aRefs = _creatureA->TestGetCombatMgr().GetPvECombatRefs(); + ASSERT_FALSE(aRefs.empty()); + + CombatReference* ref = aRefs.begin()->second; + EXPECT_EQ(ref->GetOther(_creatureA), _creatureB); + EXPECT_EQ(ref->GetOther(_creatureB), _creatureA); +} + +// ============================================================================ +// GAP COVERAGE: HasPvPCombat with no PvP refs +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, HasPvPCombat_NoPvPRefs_ReturnsFalse) +{ + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasPvPCombat()); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + // Creatures create PvE refs, not PvP + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasPvPCombat()); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); +} + +// ============================================================================ +// GAP COVERAGE: SuppressPvPCombat (no PvP refs, verify no crash) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SuppressPvPCombat_NoPvPRefs_DoesNotCrash) +{ + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + + // Should not crash with no PvP refs + _creatureA->TestGetCombatMgr().SuppressPvPCombat(); + + // PvE combat should be unaffected + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasPvECombat()); +} + +// ============================================================================ +// GAP COVERAGE: SetInCombatWith existing combat (refresh path) +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SetInCombatWith_ExistingPvE_RefreshesRef) +{ + // Create initial combat with B suppressed + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB, true); + EXPECT_FALSE(_creatureB->TestGetCombatMgr().HasPvECombat()); // suppressed + + // SetInCombatWith again (existing ref path) — should un-suppress via Refresh + bool result = _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(result); + EXPECT_TRUE(_creatureB->TestGetCombatMgr().HasPvECombat()); // refreshed +} + +// ============================================================================ +// GAP COVERAGE: Multiple combat references lifecycle +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, MultipleRefs_IndependentLifecycle) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + _creatureA->TestGetCombatMgr().SetInCombatWith(creatureC); + _creatureA->TestGetCombatMgr().SetInCombatWith(creatureD); + + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureD)); + + // End combat with C only + auto const& refs = _creatureA->TestGetCombatMgr().GetPvECombatRefs(); + for (auto const& pair : refs) + { + if (pair.second->GetOther(_creatureA) == creatureC) + { + pair.second->EndCombat(); + break; + } + } + + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureD)); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +// ============================================================================ +// GAP COVERAGE: CanBeginCombat — different maps +// Units on different maps cannot enter combat. +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, CanBeginCombat_DifferentMaps_Fails) +{ + TestMap* map2 = new TestMap(); + + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(map2, 3, 12347); + creatureC->SetFaction(90002); + + // A is on _map, C is on map2 — different maps + EXPECT_NE(_creatureA->GetMap(), creatureC->GetMap()); + EXPECT_FALSE(CombatManager::CanBeginCombat(_creatureA, creatureC)); + + creatureC->CleanupCombatState(); + delete creatureC; + delete map2; +} + +// ============================================================================ +// GAP COVERAGE: SetEvadeState — same-state early return +// Setting the same evade state twice should be a no-op. +// ============================================================================ + +TEST_F(CombatManagerIntegrationTest, SetEvadeState_SameState_NoOp) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // Set to HOME + cm.SetEvadeState(EVADE_STATE_HOME); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_HOME); + EXPECT_TRUE(cm.IsEvadingHome()); + + // Set to HOME again — early return, no crash, state unchanged + cm.SetEvadeState(EVADE_STATE_HOME); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_HOME); + EXPECT_TRUE(cm.IsEvadingHome()); + + // Same for NONE (after transitioning away first) + cm.SetEvadeState(EVADE_STATE_NONE); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); + + // NONE → NONE early return + cm.SetEvadeState(EVADE_STATE_NONE); + EXPECT_EQ(cm.GetEvadeState(), EVADE_STATE_NONE); + EXPECT_FALSE(cm.IsInEvadeMode()); +} + +TEST_F(CombatManagerIntegrationTest, SetEvadeState_SameState_DoesNotClearTimer) +{ + auto& cm = _creatureA->TestGetCombatMgr(); + + // Start evade timer (sets timer to 10s) and set evade state to COMBAT + cm.StartEvadeTimer(); + cm.SetEvadeState(EVADE_STATE_COMBAT); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Set to COMBAT again — should be no-op, timer should NOT be affected + cm.SetEvadeState(EVADE_STATE_COMBAT); + EXPECT_TRUE(cm.IsInEvadeMode()); + + // Timer should still be counting down normally + cm.Update(5000); + EXPECT_TRUE(cm.IsInEvadeMode()); + + cm.StopEvade(); +} + +} // namespace diff --git a/src/test/server/game/Combat/ThreatManagerTest.cpp b/src/test/server/game/Combat/ThreatManagerTest.cpp new file mode 100644 index 000000000..69a708340 --- /dev/null +++ b/src/test/server/game/Combat/ThreatManagerTest.cpp @@ -0,0 +1,2314 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * 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 2 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 . + */ + +#include "ThreatManager.h" +#include "CombatManager.h" +#include "DBCStores.h" +#include "TestCreature.h" +#include "TestMap.h" +#include "WorldMock.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace testing; + +namespace +{ + +// ============================================================================ +// Integration tests for the heap-based ThreatManager system. +// Uses TestCreature/TestMap with hostile DBC faction entries. +// ============================================================================ +class ThreatManagerIntegrationTest : public ::testing::Test +{ +protected: + void SetUp() override + { + _previousWorld = std::move(sWorld); + _worldMock = new NiceMock(); + + ON_CALL(*_worldMock, getIntConfig(_)).WillByDefault(Return(0)); + ON_CALL(*_worldMock, getFloatConfig(_)).WillByDefault(Return(1.0f)); + ON_CALL(*_worldMock, getBoolConfig(_)).WillByDefault(Return(false)); + + sWorld.reset(_worldMock); + + // Insert two mutually hostile DBC faction entries. + // ourMask/hostileMask bitmasks: A.hostileMask & B.ourMask != 0 => hostile + auto* factionA = new FactionTemplateEntry{}; + factionA->ID = 90001; + factionA->faction = 90001; + factionA->factionFlags = 0; + factionA->ourMask = 1; + factionA->friendlyMask = 0; + factionA->hostileMask = 2; + for (auto& e : factionA->enemyFaction) e = 0; + for (auto& f : factionA->friendFaction) f = 0; + sFactionTemplateStore.SetEntry(90001, factionA); + + auto* factionB = new FactionTemplateEntry{}; + factionB->ID = 90002; + factionB->faction = 90002; + factionB->factionFlags = 0; + factionB->ourMask = 2; + factionB->friendlyMask = 0; + factionB->hostileMask = 1; + for (auto& e : factionB->enemyFaction) e = 0; + for (auto& f : factionB->friendFaction) f = 0; + sFactionTemplateStore.SetEntry(90002, factionB); + + TestMap::EnsureDBC(); + _map = new TestMap(); + + _creatureA = new TestCreature(); + _creatureA->SetupForCombatTest(_map, 1, 12345); + _creatureA->SetFaction(90001); + + _creatureB = new TestCreature(); + _creatureB->SetupForCombatTest(_map, 2, 12346); + _creatureB->SetFaction(90002); + } + + void TearDown() override + { + _creatureA->CleanupCombatState(); + _creatureB->CleanupCombatState(); + delete _creatureA; + delete _creatureB; + delete _map; + sWorld = std::move(_previousWorld); + } + + std::unique_ptr _previousWorld; + NiceMock* _worldMock = nullptr; + TestMap* _map = nullptr; + TestCreature* _creatureA = nullptr; + TestCreature* _creatureB = nullptr; +}; + +// ============================================================================ +// Static Method Tests +// ============================================================================ + +// cppcheck-suppress syntaxError +TEST_F(ThreatManagerIntegrationTest, CanHaveThreatList_NullUnit_ReturnsFalse) +{ + EXPECT_FALSE(ThreatManager::CanHaveThreatList(nullptr)); +} + +TEST_F(ThreatManagerIntegrationTest, CanHaveThreatList_InitializedCreature_ReturnsTrue) +{ + EXPECT_TRUE(ThreatManager::CanHaveThreatList(_creatureA)); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().CanHaveThreatList()); +} + +// ============================================================================ +// AddThreat Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, AddThreat_CreatesThreatEntry) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 1u); +} + +TEST_F(ThreatManagerIntegrationTest, AddThreat_ImpliesCombat) +{ + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f); + + EXPECT_TRUE(_creatureA->TestGetCombatMgr().HasCombat()); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); +} + +// ============================================================================ +// ScaleThreat Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, ScaleThreat_DoublesAmount) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().ScaleThreat(_creatureB, 2.0f); + + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 200.0f); +} + +TEST_F(ThreatManagerIntegrationTest, ScaleThreat_ZeroFactor_ZerosThreat) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().ScaleThreat(_creatureB, 0.0f); + + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + // Entry still exists (ScaleThreat doesn't remove) + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); +} + +// ============================================================================ +// ClearThreat Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, ClearThreat_RemovesEntry) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 1u); + + _creatureA->TestGetThreatMgr().ClearThreat(_creatureB); + + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); +} + +TEST_F(ThreatManagerIntegrationTest, ClearAllThreat_EmptiesList) +{ + // Add a third creature for multi-target test + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 2u); + + _creatureA->TestGetThreatMgr().ClearAllThreat(); + + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatListEmpty()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// Victim Selection Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, GetCurrentVictim_ReturnsHighestThreat) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + + // Force a threat update cycle so ReselectVictim() applies the 110%/130% switching rules + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + Unit* victim = _creatureA->TestGetThreatMgr().GetCurrentVictim(); + EXPECT_EQ(victim, creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, FixateTarget_OverridesSelection) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + + // Force a threat update cycle so ReselectVictim() applies the 110%/130% switching rules + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + // Without fixate, highest threat (creatureC) is selected + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + // Fixate on lower-threat target + _creatureA->TestGetThreatMgr().FixateTarget(_creatureB); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // Clear fixate returns to normal selection + _creatureA->TestGetThreatMgr().ClearFixate(); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// ResetAllThreat Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, ResetAllThreat_ZerosAllEntries) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + + _creatureA->TestGetThreatMgr().ResetAllThreat(); + + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(creatureC), 0.0f); + // Entries still exist, just zeroed + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 2u); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// CompareThreatLessThan (static comparator) Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, CompareReferences_OnlineStatePrecedence) +{ + // OnlineState enum ordering: ONLINE > SUPPRESSED > OFFLINE + EXPECT_GT(ThreatReference::ONLINE_STATE_ONLINE, + ThreatReference::ONLINE_STATE_SUPPRESSED); + EXPECT_GT(ThreatReference::ONLINE_STATE_SUPPRESSED, + ThreatReference::ONLINE_STATE_OFFLINE); +} + +// ============================================================================ +// Constants Tests (compacted) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, ThreatConstants) +{ + constexpr uint32 interval = ThreatManager::THREAT_UPDATE_INTERVAL; + EXPECT_EQ(interval, 1000u); + EXPECT_GE(interval, 500u); + EXPECT_LE(interval, 2000u); + + EXPECT_EQ(static_cast(ThreatReference::ONLINE_STATE_OFFLINE), 0); + EXPECT_EQ(static_cast(ThreatReference::ONLINE_STATE_SUPPRESSED), 1); + EXPECT_EQ(static_cast(ThreatReference::ONLINE_STATE_ONLINE), 2); + + EXPECT_EQ(static_cast(ThreatReference::TAUNT_STATE_DETAUNT), 0); + EXPECT_EQ(static_cast(ThreatReference::TAUNT_STATE_NONE), 1); + EXPECT_EQ(static_cast(ThreatReference::TAUNT_STATE_TAUNT), 2); +} + +// ============================================================================ +// GetThreatListPlayerCount Tests (Bug 1 regression) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, GetThreatListPlayerCount_CreatureOnlyList_ReturnsZero) +{ + // All entries are creatures, not players — count should be 0 + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f); + + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 2u); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListPlayerCount(), 0u); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListPlayerCount(true), 0u); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// ResetAllMyThreatOnOthers Tests (Bug 4/5 regression) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, ResetAllMyThreatOnOthers_ZerosThreatOnAllCreatures) +{ + // creatureA and creatureC both add threat against creatureB + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + creatureC->TestGetThreatMgr().AddThreat(_creatureB, 200.0f); + + // creatureB appears on both A's and C's threat lists + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); + EXPECT_FLOAT_EQ(creatureC->TestGetThreatMgr().GetThreat(_creatureB), 200.0f); + + // Reset creatureB's threat on all others' lists + _creatureB->TestGetThreatMgr().ResetAllMyThreatOnOthers(); + + // creatureB's threat should be zeroed on both lists + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + EXPECT_FLOAT_EQ(creatureC->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + // Entries still exist — only zeroed, not removed + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_TRUE(creatureC->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, ResetAllMyThreatOnOthers_NoEntries_DoesNothing) +{ + // creatureB is not on anyone's threat list + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); + + // Should not crash or assert + _creatureB->TestGetThreatMgr().ResetAllMyThreatOnOthers(); +} + +// ============================================================================ +// SendThreatListToClients code path tests +// Exercises SMSG_HIGHEST_THREAT_UPDATE and SMSG_THREAT_UPDATE +// packet building without crashing (no real sessions to receive) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, SendThreatList_HighestThreatUpdate_Path) +{ + // SMSG_HIGHEST_THREAT_UPDATE is sent when victim changes + // (newHighest=true in UpdateVictim) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + + // Update forces ReselectVictim → victim switches from B + // to C → newHighest=true → SendThreatListToClients(true) + // exercises the SMSG_HIGHEST_THREAT_UPDATE packet path + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, SendThreatList_ThreatUpdate_Path) +{ + // SMSG_THREAT_UPDATE is sent when _needClientUpdate=true + // but victim doesn't change (newHighest=false) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Add lower threat — victim stays as B, + // but PutThreatListRef sets _needClientUpdate=true + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 50.0f); + + // Update → victim unchanged (B still highest) → + // newHighest=false, _needClientUpdate=true → + // SendThreatListToClients(false) + // exercises the SMSG_THREAT_UPDATE packet path + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + _creatureB); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// TauntState Victim Selection Tests +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + TauntState_OverridesHigherThreat) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B has low threat, C has high threat + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + + // Taunt B — should override despite lower threat + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_TAUNT); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + _creatureB); + + // Remove taunt — should revert to C + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_NONE); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + TauntState_DoesNotClobberFixate) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 300.0f); + + // Script fixate on C + _creatureA->TestGetThreatMgr().FixateTarget(creatureC); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + + // Taunt from B — fixate should still win + // (fixate is checked before heap in ReselectVictim) + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_TAUNT); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + // Fixate ref should still be intact + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetFixateTarget(), + creatureC); + + // Clear taunt — fixate still holds + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_NONE); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetFixateTarget(), + creatureC); + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + Detaunt_LowersPriority) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B and C have same threat + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 100.0f); + + // Detaunt B — C should be selected + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_DETAUNT); + _creatureA->TestGetThreatMgr().Update( + ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ( + _creatureA->TestGetThreatMgr().GetCurrentVictim(), + creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// CanHaveThreatList Consistency Tests (Issue #6 regression) +// Unit::CanHaveThreatList must agree with ThreatManager::CanHaveThreatList +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + CanHaveThreatList_ConsistentBetweenUnitAndThreatMgr) +{ + // Both should agree for a valid, alive creature + EXPECT_TRUE(_creatureA->CanHaveThreatList()); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().CanHaveThreatList()); + + EXPECT_TRUE(_creatureB->CanHaveThreatList()); + EXPECT_TRUE(_creatureB->TestGetThreatMgr().CanHaveThreatList()); +} + +TEST_F(ThreatManagerIntegrationTest, + CanHaveThreatList_DeadCreature_ReturnsFalse) +{ + _creatureA->SetAlive(false); + + // Unit::CanHaveThreatList checks alive state on top of ThreatMgr + EXPECT_FALSE(_creatureA->CanHaveThreatList()); + // ThreatManager cached value doesn't track alive state + EXPECT_TRUE(_creatureA->TestGetThreatMgr().CanHaveThreatList()); + // But skipAliveCheck should bypass the alive check + EXPECT_TRUE(_creatureA->CanHaveThreatList(true)); +} + +// ============================================================================ +// MatchUnitThreatToHighestThreat Tests (Issue #8 - EffectTaunt fix) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + MatchUnitThreatToHighestThreat_SetsToHighest) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // C has highest threat, B has low threat + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + + // Match B's threat to highest (C's 500) + _creatureA->TestGetThreatMgr().MatchUnitThreatToHighestThreat(_creatureB); + + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), + _creatureA->TestGetThreatMgr().GetThreat(creatureC)); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + MatchUnitThreatToHighestThreat_AlreadyHighest_NoChange) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 500.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 100.0f); + + // B is already highest — should stay at 500 + _creatureA->TestGetThreatMgr().MatchUnitThreatToHighestThreat(_creatureB); + + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 500.0f); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// Per-Spell Redirect Threat Tests (Issue #7 regression) +// UnregisterRedirectThreat(spellId) vs ResetAllRedirects +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_RegisterAndUnregister_PerSpell) +{ + // Register two different spell redirects + _creatureA->TestGetThreatMgr().RegisterRedirectThreat( + 34477, _creatureB->GetGUID(), 70); // Misdirection + EXPECT_TRUE(_creatureA->TestGetThreatMgr().HasRedirects()); + + // Unregister only Misdirection + _creatureA->TestGetThreatMgr().UnregisterRedirectThreat(34477); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().HasRedirects()); +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_UnregisterOneSpell_KeepsOther) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Register two different spell redirects to different targets + _creatureA->TestGetThreatMgr().RegisterRedirectThreat( + 34477, _creatureB->GetGUID(), 70); // Misdirection + _creatureA->TestGetThreatMgr().RegisterRedirectThreat( + 57934, creatureC->GetGUID(), 30); // Tricks of the Trade + + EXPECT_TRUE(_creatureA->TestGetThreatMgr().HasRedirects()); + + // Unregister only Misdirection — Tricks should remain + _creatureA->TestGetThreatMgr().UnregisterRedirectThreat(34477); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().HasRedirects()); + + // Unregister Tricks — now empty + _creatureA->TestGetThreatMgr().UnregisterRedirectThreat(57934); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().HasRedirects()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_ResetAllRedirects_ClearsAll) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().RegisterRedirectThreat( + 34477, _creatureB->GetGUID(), 70); + _creatureA->TestGetThreatMgr().RegisterRedirectThreat( + 57934, creatureC->GetGUID(), 30); + + _creatureA->TestGetThreatMgr().ResetAllRedirects(); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().HasRedirects()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// IsEngagedBy Tests (Unit.h change: threat-based for creatures) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + IsEngagedBy_UsesThreatListForCreatures) +{ + // Before any threat, not engaged + EXPECT_FALSE(_creatureA->IsEngagedBy(_creatureB)); + + // AddThreat creates a threat reference (and combat reference) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // IsEngagedBy should check threat list for creatures with threat lists + EXPECT_TRUE(_creatureA->IsEngagedBy(_creatureB)); + // B is on A's threat list but A is NOT on B's threat list + // (threat is directional: A threatens B, not vice versa unless B also AddThreats A) + EXPECT_FALSE(_creatureB->IsEngagedBy(_creatureA)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsEngagedBy_CombatWithoutThreat_NotEngaged) +{ + // Combat reference without threat entry + _creatureA->TestGetCombatMgr().SetInCombatWith(_creatureB); + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(_creatureB)); + + // IsEngagedBy checks threat list for creatures, not combat refs + EXPECT_FALSE(_creatureA->IsEngagedBy(_creatureB)); +} + +// ============================================================================ +// UpdateMySpellSchoolModifiers Tests (HandleModThreat bug fix) +// Verify that spell school modifiers affect CalculateModifiedThreat +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + SpellSchoolModifiers_DefaultIsUnmodified) +{ + // Default modifier should be 1.0 (no modification) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); +} + +// ============================================================================ +// Phase Change + Threat Offline State Tests (SetPhaseMask order fix) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + PhaseChange_PutsThreatsOffline) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + // B is online on A's threat list + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatListEmpty(false)); + + // Move B to a different phase + _creatureB->SetPhase(2); + + // B should now be offline on A's threat list (different phases) + // The list is not empty if we include offline, but empty if we don't + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB, true)); // includeOffline + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatListEmpty(false)); // online only +} + +// ============================================================================ +// Redirect System - Functional (End-to-End) Tests +// Verifies that AddThreat actually redirects threat to registered targets. +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_AddThreat_SplitsBetweenTargetAndRedirect) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Pre-establish C on A's threat list (ObjectAccessor unavailable in tests) + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 0.0f, nullptr, true, true); + + // B registers 50% redirect to C (e.g. Misdirection) + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 50); + + // A adds 100 threat against B — 50% should redirect to C + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f, nullptr, true); + + // B should have 50 threat (100 - 50% redirected) + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 50.0f); + // C should have 50 threat (the redirected portion) + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(creatureC)); + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureC), 50.0f); + + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_FullRedirect_AllThreatGoesToTarget) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Pre-establish C on A's threat list (ObjectAccessor unavailable in tests) + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 0.0f, nullptr, true, true); + + // B registers 100% redirect to C + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 100); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 200.0f, nullptr, true); + + // B should have 0 threat (all redirected) + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + // C should have all 200 threat + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureC), 200.0f); + + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_TwoSpells_BothRedirect) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // Pre-establish C and D on A's threat list + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 0.0f, nullptr, true, true); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 0.0f, nullptr, true, true); + + // B registers two redirects that total 60% (no cap issue) + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 30); // Misdirection 30% + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 57934, creatureD->GetGUID(), 30); // Tricks of the Trade 30% + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f, nullptr, true); + + // B should have 40 threat (100 - 30 - 30) + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 40.0f); + + // C and D should each have 30 threat + float cThreat = _creatureA->TestGetThreatMgr().GetThreat(creatureC); + float dThreat = _creatureA->TestGetThreatMgr().GetThreat(creatureD); + + // Due to unordered_map iteration, we verify total redirected = 60 + EXPECT_FLOAT_EQ(cThreat + dThreat, 60.0f); + EXPECT_FLOAT_EQ(cThreat, 30.0f); + EXPECT_FLOAT_EQ(dThreat, 30.0f); + + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(57934); + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_NegativeAmount_NoRedirect) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Establish initial threat + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 200.0f, nullptr, true); + + // Register redirect and apply negative threat (threat reduction) + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 50); + + // Negative threat should NOT be redirected (code checks amount > 0) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, -50.0f, nullptr, true); + + // B should have 150 (200 - 50), C should have no entry or 0 + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 150.0f); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(creatureC)); + + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_AfterUnregister_NoMoreRedirect) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Pre-establish C on A's threat list (ObjectAccessor unavailable in tests) + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 0.0f, nullptr, true, true); + + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 50); + + // First add — should redirect + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f, nullptr, true); + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 50.0f); + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureC), 50.0f); + + // Unregister redirect + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + + // Second add — should NOT redirect, all goes to B + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f, nullptr, true); + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 150.0f); + // C should still have 50 from before + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureC), 50.0f); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_UnregisterPerSpellPerVictim) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // Register same spell with two different victims + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 30); + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureD->GetGUID(), 20); + EXPECT_TRUE(_creatureB->TestGetThreatMgr().HasRedirects()); + + // Unregister only the C victim for spell 34477 + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat( + 34477, creatureC->GetGUID()); + // D's redirect should still be active + EXPECT_TRUE(_creatureB->TestGetThreatMgr().HasRedirects()); + + // Unregister D — now empty + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat( + 34477, creatureD->GetGUID()); + EXPECT_FALSE(_creatureB->TestGetThreatMgr().HasRedirects()); + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +// ============================================================================ +// ForwardThreatForAssistingMe Tests +// Verifies that healing/assist threat is distributed among all creatures +// that are threatening the healed unit. +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ForwardThreat_SingleCreature_AllThreatToAssistant) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // A (boss) has B on its threat list + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // B is "healed" — forward 50 assist threat from C (the healer) + // ForwardThreat iterates _threatenedByMe (creatures threatening B) + // and adds threat for the assistant (C) on those creatures' lists + _creatureB->TestGetThreatMgr().ForwardThreatForAssistingMe( + creatureC, 50.0f); + + // C should now have 50 threat on A's threat list + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(creatureC)); + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureC), 50.0f); + // B's threat should be unchanged + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + ForwardThreat_MultipleCreatures_SplitsEvenly) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); // Same faction as A (so it can also threat B) + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); // The healer + + // A (boss1) and C (boss2) both have B on their threat lists + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + creatureC->TestGetThreatMgr().AddThreat(_creatureB, 200.0f); + + // B is "healed" by D — forward 100 assist threat + // Should split evenly: 50 to A, 50 to C + _creatureB->TestGetThreatMgr().ForwardThreatForAssistingMe( + creatureD, 100.0f); + + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureD), 50.0f); + EXPECT_FLOAT_EQ( + creatureC->TestGetThreatMgr().GetThreat(creatureD), 50.0f); + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + ForwardThreat_CCTarget_GetsZeroThreat) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); // Same faction as A + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); // The healer + + // A has B on threat list, C also has B + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + creatureC->TestGetThreatMgr().AddThreat(_creatureB, 200.0f); + + // A is under CC (UNIT_STATE_CONTROLLED) — should get 0 threat + _creatureA->AddUnitState(UNIT_STATE_CONTROLLED); + + _creatureB->TestGetThreatMgr().ForwardThreatForAssistingMe( + creatureD, 100.0f); + + // Only C is not CC'd, so it gets all 100 (100 / 1 non-CC target) + EXPECT_FLOAT_EQ( + creatureC->TestGetThreatMgr().GetThreat(creatureD), 100.0f); + + // A gets 0 threat (CC'd creatures still get combat but 0 threat) + float aThreatForD = _creatureA->TestGetThreatMgr().GetThreat(creatureD); + EXPECT_FLOAT_EQ(aThreatForD, 0.0f); + + _creatureA->ClearUnitState(UNIT_STATE_CONTROLLED); + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + ForwardThreat_NoThreatenedBy_DoesNothing) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B is not on anyone's threat list + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); + + // Forward should be a no-op + _creatureB->TestGetThreatMgr().ForwardThreatForAssistingMe( + creatureC, 100.0f); + + // A should have no threat entries + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + ForwardThreat_ZeroAmount_StillEntersCombat) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // A has B on threat list + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Forward 0 threat — should still create combat reference + _creatureB->TestGetThreatMgr().ForwardThreatForAssistingMe( + creatureC, 0.0f); + + // C should be in combat with A (threat of 0 but combat ref exists) + EXPECT_TRUE(_creatureA->TestGetCombatMgr().IsInCombatWith(creatureC)); + // Threat value should be 0 + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureC), 0.0f); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// EvaluateSuppressed Transition Tests +// Tests online/suppressed state transitions on threat references. +// Uses IMMUNITY_DAMAGE to trigger ShouldBeSuppressed(). +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + EvaluateSuppressed_NoRefs_DoesNotCrash) +{ + // B is not on anyone's threat list — EvaluateSuppressed should be a no-op + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(true); +} + +TEST_F(ThreatManagerIntegrationTest, + EvaluateSuppressed_OnlineRefs_RemainOnline) +{ + // A has B on threat list — B's ref on A's list should be ONLINE + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // EvaluateSuppressed on B (the victim) should not change anything + // since B is not immune to anything + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // Verify ref is still available (ONLINE or SUPPRESSED both pass) + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB, false)); + + // Verify it's specifically online by checking it's not suppressed + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + { + if (ref->GetVictim() == _creatureB) + { + EXPECT_TRUE(ref->IsOnline()); + EXPECT_FALSE(ref->IsSuppressed()); + } + } +} + +TEST_F(ThreatManagerIntegrationTest, + EvaluateSuppressed_DamageImmunity_SetsSuppressed) +{ + // A has B on threat list + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Verify B's ref starts as ONLINE + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsOnline()); + + // Make B immune to physical damage (A's melee school is SPELL_SCHOOL_MASK_NORMAL) + // This triggers ShouldBeSuppressed via IsImmunedToDamage + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, true); + + // EvaluateSuppressed checks each ref in _threatenedByMe + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // B's ref on A's threat list should now be SUPPRESSED + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + { + if (ref->GetVictim() == _creatureB) + { + EXPECT_TRUE(ref->IsSuppressed()); + EXPECT_FALSE(ref->IsOnline()); + EXPECT_TRUE(ref->IsAvailable()); // SUPPRESSED is still available + } + } + + // Threat value should still be retrievable (SUPPRESSED is available) + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); + + // Cleanup immunity + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, false); +} + +TEST_F(ThreatManagerIntegrationTest, + EvaluateSuppressed_CanExpire_RestoresOnline) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Apply immunity to suppress + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, true); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // Verify suppressed + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsSuppressed()); + + // Remove immunity + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, false); + + // EvaluateSuppressed with canExpire=false should NOT restore to online + _creatureB->TestGetThreatMgr().EvaluateSuppressed(false); + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsSuppressed()); // still suppressed + + // EvaluateSuppressed with canExpire=true SHOULD restore to online + _creatureB->TestGetThreatMgr().EvaluateSuppressed(true); + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + { + if (ref->GetVictim() == _creatureB) + { + EXPECT_TRUE(ref->IsOnline()); + EXPECT_FALSE(ref->IsSuppressed()); + } + } +} + +TEST_F(ThreatManagerIntegrationTest, + EvaluateSuppressed_TauntedVictim_NeverSuppressed) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Taunt B + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_TAUNT); + + // Apply damage immunity — normally would suppress + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, true); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // Taunted victims should never be suppressed (ShouldBeSuppressed returns false) + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsOnline()); + + // Cleanup + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_NONE); + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, false); +} + +TEST_F(ThreatManagerIntegrationTest, + EvaluateSuppressed_MultipleRefs_EachEvaluatedIndependently) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); // Same faction as A + + // Both A and C have B on their threat lists + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + creatureC->TestGetThreatMgr().AddThreat(_creatureB, 200.0f); + + // Apply immunity on B + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, true); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // Both refs should be suppressed + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsSuppressed()); + for (ThreatReference const* ref : creatureC->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsSuppressed()); + + // Remove immunity and expire + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, false); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(true); + + // Both should be online again + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsOnline()); + for (ThreatReference const* ref : creatureC->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsOnline()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// GAP COVERAGE: Victim Selection Threshold Tests (ReselectVictim) +// Tests the 110%/130% switching logic, melee vs ranged preference. +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ReselectVictim_Below110Percent_KeepsOldVictim) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B gets threat first and becomes initial victim + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // C gets 109% of B's threat — below 110% threshold, should NOT switch + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 109.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + ReselectVictim_MeleeAt110Percent_Switches) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Both creatures at same position (0,0,0) = within melee range + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // C gets 111% of B's threat and is melee — should switch + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 111.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// NOTE: Ranged (130%) threshold tests cannot be reliably tested in unit tests. +// Relocate()-based distance checks fail because ShouldBeOffline() calls +// CanSeeOrDetect(), which requires map grid infrastructure not available +// in the unit test environment. Creatures placed far away go OFFLINE instead +// of being treated as "ranged". These thresholds are validated by the +// CompareReferencesLT implementation and integration/manual testing. + +TEST_F(ThreatManagerIntegrationTest, + ReselectVictim_MeleeAt130Percent_AlsoSwitches) +{ + // Verify that a melee target above 130% also switches (melee only needs + // 110%, so 130% should certainly work too) + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // C gets 131% threat — well above melee 110% threshold, should switch + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 131.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + ReselectVictim_MeleeThirdTarget_AboveThreshold_Switches) +{ + // Three melee targets — verify correct switching with multiple candidates + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // B is current victim with 100 threat + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // C has 105% — below 110% threshold, won't switch + // D has 115% — above 110% threshold, should switch to D + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 105.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 115.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + // Should pick D (highest above threshold) + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureD); + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + ReselectVictim_NoOldVictim_PicksHighest) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Add both at the same time — no previous victim + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 50.0f); + + // First victim selection — should pick highest regardless of thresholds + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + ReselectVictim_EqualThreat_KeepsCurrentVictim) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B is initial victim + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // C gets exactly equal threat — should NOT switch (need >110%) + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// GAP COVERAGE: ThreatManager::Update() Timer-Driven Path +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + Update_TimerDrivenReselection) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); + + // Add much higher threat to C + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + + // Partial update (less than interval) — timer hasn't fired yet + _creatureA->TestGetThreatMgr().Update(500); + // GetCurrentVictim triggers immediate UpdateVictim so it will still return C + // But the timer-based path hasn't fired yet + // Verify Update doesn't crash on partial timer + EXPECT_NE(_creatureA->TestGetThreatMgr().GetCurrentVictim(), nullptr); + + // Full interval fires timer-based reselection + _creatureA->TestGetThreatMgr().Update(500); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + Update_ZeroDiff_DoesNotCrash) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // 0ms update should not crash or do anything unexpected + _creatureA->TestGetThreatMgr().Update(0); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), _creatureB); +} + +TEST_F(ThreatManagerIntegrationTest, + Update_EmptyThreatList_DoesNotCrash) +{ + // Update with no threat entries — should be safe no-op + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), nullptr); +} + +// ============================================================================ +// GAP COVERAGE: ModifyThreatByPercent +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ModifyThreatByPercent_Increase) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // +50% → 150 + _creatureA->TestGetThreatMgr().ModifyThreatByPercent(_creatureB, 50); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 150.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + ModifyThreatByPercent_Decrease) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // -30% → 70 + _creatureA->TestGetThreatMgr().ModifyThreatByPercent(_creatureB, -30); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 70.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + ModifyThreatByPercent_Minus100_ZerosThreat) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // -100% → 0 + _creatureA->TestGetThreatMgr().ModifyThreatByPercent(_creatureB, -100); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + // Entry should still exist + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); +} + +TEST_F(ThreatManagerIntegrationTest, + ModifyThreatByPercent_ZeroPercent_NoChange) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // 0% change — should be a no-op + _creatureA->TestGetThreatMgr().ModifyThreatByPercent(_creatureB, 0); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + ModifyThreatByPercent_Plus100_Doubles) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // +100% → 200 + _creatureA->TestGetThreatMgr().ModifyThreatByPercent(_creatureB, 100); + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 200.0f); +} + +// ============================================================================ +// GAP COVERAGE: Query Methods (GetThreat, IsThreatListEmpty, IsThreatenedBy) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + GetThreat_NonexistentTarget_ReturnsZero) +{ + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + GetThreat_OfflineTarget_ExcludedByDefault) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Move B offline via phase change + _creatureB->SetPhase(2); + + // Force re-evaluation + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + // Default (includeOffline=false) should return 0 + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB, false), 0.0f); + // includeOffline=true should return the threat value + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB, true), 100.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreatListEmpty_EmptyList_ReturnsTrue) +{ + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatListEmpty()); + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatListEmpty(true)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreatListEmpty_WithEntries_ReturnsFalse) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatListEmpty()); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatListEmpty(true)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreatListEmpty_AllOffline_TrueWithoutOfflineFlag) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Move B offline + _creatureB->SetPhase(2); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + // Online-only should be empty + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatListEmpty(false)); + // Including offline should not be empty + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatListEmpty(true)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreatenedBy_OfflineTarget_Variants) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + _creatureB->SetPhase(2); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + // Default (online only) — should be false + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB, false)); + // Including offline — should be true + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB, true)); +} + +// ============================================================================ +// GAP COVERAGE: IsThreateningAnyone / IsThreateningTo +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + IsThreateningAnyone_NoEntries_ReturnsFalse) +{ + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone(true)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreateningAnyone_WithEntries_ReturnsTrue) +{ + // A adds threat against B → B appears in A's threat list + // B is a "victim" on A's list → B._threatenedByMe has an entry for A + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + EXPECT_TRUE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); + EXPECT_TRUE(_creatureB->TestGetThreatMgr().IsThreateningAnyone(true)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreateningAnyone_AllOffline_FalseWithoutFlag) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Make B's ref on A's list go offline + _creatureB->SetPhase(2); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + // B is still threatening A, but the ref is offline + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone(false)); + EXPECT_TRUE(_creatureB->TestGetThreatMgr().IsThreateningAnyone(true)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreateningTo_SpecificTarget) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // B is threatening A (B appears on A's threat list) + EXPECT_TRUE(_creatureB->TestGetThreatMgr().IsThreateningTo(_creatureA)); + // A is NOT threatening B (A does not appear on B's threat list) + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreateningTo(_creatureB)); +} + +TEST_F(ThreatManagerIntegrationTest, + IsThreateningTo_ObjectGuidVariant) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + EXPECT_TRUE(_creatureB->TestGetThreatMgr().IsThreateningTo(_creatureA->GetGUID())); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreateningTo(_creatureB->GetGUID())); +} + +// ============================================================================ +// GAP COVERAGE: RemoveMeFromThreatLists +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + RemoveMeFromThreatLists_RemovesFromAllLists) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90001); + + // A and C both have B on their threat lists + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + creatureC->TestGetThreatMgr().AddThreat(_creatureB, 200.0f); + + EXPECT_TRUE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_TRUE(creatureC->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + + // B removes itself from all threat lists + _creatureB->TestGetThreatMgr().RemoveMeFromThreatLists(); + + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_FALSE(creatureC->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + RemoveMeFromThreatLists_NoEntries_DoesNotCrash) +{ + // B is not on anyone's threat list — should be safe no-op + _creatureB->TestGetThreatMgr().RemoveMeFromThreatLists(); + EXPECT_FALSE(_creatureB->TestGetThreatMgr().IsThreateningAnyone()); +} + +// ============================================================================ +// GAP COVERAGE: GetAnyTarget (ThreatManager) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + GetAnyTarget_EmptyList_ReturnsNull) +{ + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetAnyTarget(), nullptr); +} + +TEST_F(ThreatManagerIntegrationTest, + GetAnyTarget_WithEntries_ReturnsNonNull) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_NE(_creatureA->TestGetThreatMgr().GetAnyTarget(), nullptr); +} + +TEST_F(ThreatManagerIntegrationTest, + GetAnyTarget_AllOffline_ReturnsNull) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + _creatureB->SetPhase(2); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetAnyTarget(), nullptr); +} + +// ============================================================================ +// GAP COVERAGE: GetLastVictim +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + GetLastVictim_NoVictim_ReturnsNull) +{ + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetLastVictim(), nullptr); +} + +TEST_F(ThreatManagerIntegrationTest, + GetLastVictim_WithVictim_ReturnsCached) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetLastVictim(), _creatureB); +} + +TEST_F(ThreatManagerIntegrationTest, + GetLastVictim_OfflineVictim_ReturnsNull) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + + _creatureB->SetPhase(2); + // GetLastVictim checks ShouldBeOffline on cached ref + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetLastVictim(), nullptr); +} + +// ============================================================================ +// GAP COVERAGE: GetSortedThreatList / GetUnsortedThreatList / GetModifiableThreatList +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + GetSortedThreatList_ReturnsSortedByThreat) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 100.0f); + + float prevThreat = std::numeric_limits::max(); + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetSortedThreatList()) + { + EXPECT_LE(ref->GetThreat(), prevThreat); + prevThreat = ref->GetThreat(); + } + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + GetUnsortedThreatList_ReturnsAllEntries) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f); + + size_t count = 0; + for ([[maybe_unused]] ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + ++count; + + EXPECT_EQ(count, 2u); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + GetModifiableThreatList_ReturnsCopy) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f); + + auto list = _creatureA->TestGetThreatMgr().GetModifiableThreatList(); + EXPECT_EQ(list.size(), 2u); + + // Verify it's sorted (highest first) + EXPECT_GE(list[0]->GetThreat(), list[1]->GetThreat()); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +// ============================================================================ +// GAP COVERAGE: Multiple Concurrent Taunts +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + MultipleTaunts_LastTauntWins) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 50.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 25.0f); + + // Taunt from C + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + creatureC, ThreatReference::TAUNT_STATE_TAUNT); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + // Also taunt from D — both taunted, but D has lower threat + // With equal taunt state, higher threat wins → C still selected + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + creatureD, ThreatReference::TAUNT_STATE_TAUNT); + _creatureA->TestGetThreatMgr().Update(ThreatManager::THREAT_UPDATE_INTERVAL); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetCurrentVictim(), creatureC); + + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +// ============================================================================ +// GAP COVERAGE: AddThreat Edge Cases +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + AddThreat_NegativeAmount_ReducesThreat) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, -30.0f); + + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 70.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + AddThreat_NegativeBeyondZero_ClampsToZero) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f); + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, -100.0f); + + // ThreatReference::GetThreat clamps to 0 via max(baseAmount + tempMod, 0) + EXPECT_FLOAT_EQ(_creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + AddThreat_DeadTarget_NoCombatOrThreat) +{ + _creatureB->SetAlive(false); + + // Dead targets should fail CanBeginCombat and thus not get threat + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); +} + +TEST_F(ThreatManagerIntegrationTest, + AddThreat_DifferentPhaseTarget_NoCombatOrThreat) +{ + _creatureB->SetPhase(2); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); + EXPECT_FALSE(_creatureA->TestGetCombatMgr().HasCombat()); +} + +// ============================================================================ +// GAP COVERAGE: GetFixateTarget +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + GetFixateTarget_NoFixate_ReturnsNull) +{ + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetFixateTarget(), nullptr); +} + +TEST_F(ThreatManagerIntegrationTest, + GetFixateTarget_WithFixate_ReturnsTarget) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().FixateTarget(_creatureB); + + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetFixateTarget(), _creatureB); + + _creatureA->TestGetThreatMgr().ClearFixate(); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetFixateTarget(), nullptr); +} + +// ============================================================================ +// GAP COVERAGE: ThreatReference Direct API +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ThreatReference_GetOwnerAndVictim) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + { + if (ref->GetVictim() == _creatureB) + { + EXPECT_EQ(ref->GetOwner(), _creatureA); + EXPECT_EQ(ref->GetVictim(), _creatureB); + EXPECT_FLOAT_EQ(ref->GetThreat(), 100.0f); + EXPECT_TRUE(ref->IsOnline()); + EXPECT_FALSE(ref->IsOffline()); + EXPECT_FALSE(ref->IsSuppressed()); + EXPECT_TRUE(ref->IsAvailable()); + EXPECT_EQ(ref->GetTauntState(), ThreatReference::TAUNT_STATE_NONE); + EXPECT_FALSE(ref->IsTaunting()); + EXPECT_FALSE(ref->IsDetaunted()); + } + } +} + +TEST_F(ThreatManagerIntegrationTest, + ThreatReference_ScaleThreat_Directly) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + auto list = _creatureA->TestGetThreatMgr().GetModifiableThreatList(); + ASSERT_EQ(list.size(), 1u); + + list[0]->ScaleThreat(3.0f); + EXPECT_FLOAT_EQ(list[0]->GetThreat(), 300.0f); + + list[0]->ModifyThreatByPercent(-50); + EXPECT_FLOAT_EQ(list[0]->GetThreat(), 150.0f); +} + +// ============================================================================ +// GAP COVERAGE: MatchUnitThreatToHighestThreat — taunted-highest skip +// When the heap-top ref is the taunting target, the code peeks at the next +// entry to find the real highest threat value. +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + MatchUnitThreatToHighestThreat_HighestIsTaunted_UsesNextEntry) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // B has low threat (100), C has high threat (500), D has none yet + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 10.0f); + + // Taunt B — B becomes heap top (taunt state wins over threat value) + // but B's actual threat is only 100. + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_TAUNT); + + // MatchUnitThreatToHighestThreat should skip the taunted entry (B=100) + // and use C's 500 as the real highest. + _creatureA->TestGetThreatMgr().MatchUnitThreatToHighestThreat(creatureD); + + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureD), 500.0f); + + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_NONE); + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + MatchUnitThreatToHighestThreat_TauntedIsAlsoHighest_UsesTauntedThreat) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // B has highest threat AND is taunted — no skip needed + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 500.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 100.0f); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 10.0f); + + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_TAUNT); + + _creatureA->TestGetThreatMgr().MatchUnitThreatToHighestThreat(creatureD); + + // B's 500 is the real highest — D should get 500 + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(creatureD), 500.0f); + + _creatureA->TestGetThreatMgr().SetTauntStateForTesting( + _creatureB, ThreatReference::TAUNT_STATE_NONE); + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +// ============================================================================ +// GAP COVERAGE: AddThreat — suppressed-to-online transition +// When adding threat to an existing SUPPRESSED ref that no longer qualifies +// for suppression, the ref transitions back to ONLINE (without needing +// EvaluateSuppressed(true)). +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + AddThreat_SuppressedRef_UnsuppressesOnNewThreat) +{ + // A has B on threat list + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Make B immune → suppress B's ref on A's list + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, true); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // Verify B is SUPPRESSED + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsSuppressed()); + + // Remove immunity (but do NOT call EvaluateSuppressed) + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, false); + + // Add more threat — the AddThreat path should detect that the ref + // no longer ShouldBeSuppressed() and transition it to ONLINE + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f, nullptr, true); + + // Verify B is now ONLINE and threat was applied + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + { + if (ref->GetVictim() == _creatureB) + { + EXPECT_TRUE(ref->IsOnline()); + EXPECT_FALSE(ref->IsSuppressed()); + } + } + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 150.0f); +} + +TEST_F(ThreatManagerIntegrationTest, + AddThreat_SuppressedRef_StillSuppressed_NoTransition) +{ + // A has B on threat list + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Make B immune → suppress + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, true); + _creatureB->TestGetThreatMgr().EvaluateSuppressed(); + + // Add more threat while B is still immune — should stay SUPPRESSED + // and threat should NOT be applied (AddThreat only adds to ONLINE refs) + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 50.0f, nullptr, true); + + for (ThreatReference const* ref : _creatureA->TestGetThreatMgr().GetUnsortedThreatList()) + if (ref->GetVictim() == _creatureB) + EXPECT_TRUE(ref->IsSuppressed()); + + // Threat should be unchanged (AddThreat skips non-online refs) + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 100.0f); + + _creatureB->ApplySpellImmune(1, IMMUNITY_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, false); +} + +// ============================================================================ +// GAP COVERAGE: Redirect total cap at 100% +// UpdateRedirectInfo() caps the total redirect percentage at 100%. +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_TotalExceeds100_CappedAt100) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // Pre-establish C and D on A's threat list + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 0.0f, nullptr, true, true); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 0.0f, nullptr, true, true); + + // Register redirects totaling 130% (70% + 60%) + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 70); + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 57934, creatureD->GetGUID(), 60); + + // Add 100 threat from A to B + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f, nullptr, true); + + // Total redirected should be capped at 100%, so B gets 0 + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + + // C + D together should have exactly 100 (the full amount) + float cThreat = _creatureA->TestGetThreatMgr().GetThreat(creatureC); + float dThreat = _creatureA->TestGetThreatMgr().GetThreat(creatureD); + EXPECT_FLOAT_EQ(cThreat + dThreat, 100.0f); + + // First redirect should get its full 70%, second gets capped to 30% + // (iteration order of unordered_map may vary, so check the sum is correct + // and each individual value is within expected bounds) + EXPECT_GE(cThreat, 0.0f); + EXPECT_LE(cThreat, 100.0f); + EXPECT_GE(dThreat, 0.0f); + EXPECT_LE(dThreat, 100.0f); + + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(57934); + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +TEST_F(ThreatManagerIntegrationTest, + RedirectThreat_Exactly100Percent_AllRedirected) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + TestCreature* creatureD = new TestCreature(); + creatureD->SetupForCombatTest(_map, 4, 12348); + creatureD->SetFaction(90002); + + // Pre-establish C and D on A's threat list + _creatureA->TestGetThreatMgr().AddThreat(creatureC, 0.0f, nullptr, true, true); + _creatureA->TestGetThreatMgr().AddThreat(creatureD, 0.0f, nullptr, true, true); + + // Register redirects totaling exactly 100% (60% + 40%) + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 34477, creatureC->GetGUID(), 60); + _creatureB->TestGetThreatMgr().RegisterRedirectThreat( + 57934, creatureD->GetGUID(), 40); + + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 200.0f, nullptr, true); + + // B should get 0 (all redirected) + EXPECT_FLOAT_EQ( + _creatureA->TestGetThreatMgr().GetThreat(_creatureB), 0.0f); + + // C + D should have 200 total + float cThreat = _creatureA->TestGetThreatMgr().GetThreat(creatureC); + float dThreat = _creatureA->TestGetThreatMgr().GetThreat(creatureD); + EXPECT_FLOAT_EQ(cThreat + dThreat, 200.0f); + + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(34477); + _creatureB->TestGetThreatMgr().UnregisterRedirectThreat(57934); + creatureC->CleanupCombatState(); + creatureD->CleanupCombatState(); + delete creatureC; + delete creatureD; +} + +// ============================================================================ +// GAP COVERAGE: ThreatReference::ScaleThreat with factor=1.0 (no-op) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ThreatReference_ScaleThreat_Factor1_NoOp) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + auto list = _creatureA->TestGetThreatMgr().GetModifiableThreatList(); + ASSERT_EQ(list.size(), 1u); + + // ScaleThreat(1.0f) has an early return — no heap notification + list[0]->ScaleThreat(1.0f); + EXPECT_FLOAT_EQ(list[0]->GetThreat(), 100.0f); +} + +// ============================================================================ +// GAP COVERAGE: ThreatReference::AddThreat with amount=0.0 (no-op) +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ThreatReference_AddThreat_ZeroAmount_NoOp) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + auto list = _creatureA->TestGetThreatMgr().GetModifiableThreatList(); + ASSERT_EQ(list.size(), 1u); + + // AddThreat(0.0f) has an early return — no heap notification + list[0]->AddThreat(0.0f); + EXPECT_FLOAT_EQ(list[0]->GetThreat(), 100.0f); +} + +// ============================================================================ +// GAP COVERAGE: ThreatReference::ClearThreat() via direct method +// The ThreatReference self-deallocation path (ref->ClearThreat delegates +// to ThreatManager::ClearThreat(this)). +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + ThreatReference_ClearThreat_DirectCall_RemovesEntry) +{ + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 1u); + + // Get the reference via modifiable list and call ClearThreat() on it + auto list = _creatureA->TestGetThreatMgr().GetModifiableThreatList(); + ASSERT_EQ(list.size(), 1u); + list[0]->ClearThreat(); // self-deallocation — pointer is now invalid + + // Entry should be gone + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetThreatListSize(), 0u); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(_creatureB)); +} + +// ============================================================================ +// GAP COVERAGE: FixateTarget on a unit not on the threat list +// Should fall through to _fixateRef = nullptr. +// ============================================================================ + +TEST_F(ThreatManagerIntegrationTest, + FixateTarget_NotOnThreatList_ClearsFixate) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // B is on the threat list, C is NOT + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + EXPECT_FALSE(_creatureA->TestGetThreatMgr().IsThreatenedBy(creatureC)); + + // Fixate on C (not on threat list) — should result in nullptr fixate + _creatureA->TestGetThreatMgr().FixateTarget(creatureC); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetFixateTarget(), nullptr); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +TEST_F(ThreatManagerIntegrationTest, + FixateTarget_NotOnThreatList_ClearsExistingFixate) +{ + TestCreature* creatureC = new TestCreature(); + creatureC->SetupForCombatTest(_map, 3, 12347); + creatureC->SetFaction(90002); + + // Both B and C are on the threat list + _creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f); + + // Set a valid fixate first + _creatureA->TestGetThreatMgr().FixateTarget(_creatureB); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetFixateTarget(), _creatureB); + + // Now fixate on C (not on threat list) — should clear the existing fixate + _creatureA->TestGetThreatMgr().FixateTarget(creatureC); + EXPECT_EQ(_creatureA->TestGetThreatMgr().GetFixateTarget(), nullptr); + + creatureC->CleanupCombatState(); + delete creatureC; +} + +} // namespace