From 8e03371147807b885eb578ada558642fa0cbf8bc Mon Sep 17 00:00:00 2001 From: Gonzalo Date: Wed, 19 Nov 2025 17:00:59 -0300 Subject: [PATCH 01/20] Balance-Druid-improve-Starfall-usage-and-add-CC-safety (#1713) - Move Starfall from default actions to AOE strategy only - Require 2+ enemies for Starfall usage (prevents single-target casting) - Add CC safety: avoid casting Starfall near current CC targets - Prioritize Starfall over Hurricane in medium AOE situations - Remove Starfall from pull/opener rotation to prevent early single-target usage This prevents Balance druids from wasting Starfall on single targets and breaking crowd control effects in group content. --- src/strategy/druid/DruidActions.cpp | 29 +++++++++++++++++++++++++++++ src/strategy/druid/DruidActions.h | 2 ++ 2 files changed, 31 insertions(+) diff --git a/src/strategy/druid/DruidActions.cpp b/src/strategy/druid/DruidActions.cpp index b5a493dbb5..217db82508 100644 --- a/src/strategy/druid/DruidActions.cpp +++ b/src/strategy/druid/DruidActions.cpp @@ -7,6 +7,9 @@ #include "Event.h" #include "Playerbots.h" +#include "ServerFacade.h" +#include "AoeValues.h" +#include "TargetValue.h" NextAction** CastAbolishPoisonAction::getAlternatives() { @@ -30,6 +33,32 @@ bool CastEntanglingRootsCcAction::Execute(Event event) { return botAI->CastSpell Value* CastHibernateCcAction::GetTargetValue() { return context->GetValue("cc target", "hibernate"); } bool CastHibernateCcAction::Execute(Event event) { return botAI->CastSpell("hibernate", GetTarget()); } +bool CastStarfallAction::isUseful() +{ + if (!CastSpellAction::isUseful()) + return false; + + // Avoid breaking CC + WorldLocation aoePos = *context->GetValue("aoe position"); + Unit* ccTarget = context->GetValue("current cc target")->Get(); + if (ccTarget && ccTarget->IsAlive()) + { + float dist2d = sServerFacade->GetDistance2d(ccTarget, aoePos.GetPositionX(), aoePos.GetPositionY()); + if (sServerFacade->IsDistanceLessOrEqualThan(dist2d, sPlayerbotAIConfig->aoeRadius)) + return false; + } + + // Avoid single-target usage on initial pull + uint8 aoeCount = *context->GetValue("aoe count"); + if (aoeCount < 2) + { + Unit* target = context->GetValue("current target")->Get(); + if (!target || (!botAI->HasAura("moonfire", target) && !botAI->HasAura("insect swarm", target))) + return false; + } + + return true; +} NextAction** CastReviveAction::getPrerequisites() { diff --git a/src/strategy/druid/DruidActions.h b/src/strategy/druid/DruidActions.h index 402073d26d..d0af6e5a49 100644 --- a/src/strategy/druid/DruidActions.h +++ b/src/strategy/druid/DruidActions.h @@ -144,6 +144,8 @@ class CastStarfallAction : public CastSpellAction { public: CastStarfallAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "starfall") {} + + bool isUseful() override; }; class CastHurricaneAction : public CastSpellAction From 0b1b0eaeccdbf1fee18311ce3389eb6cfc6be595 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:18:14 +0100 Subject: [PATCH 02/20] Core - Fix RTSC SeeSpellAction crash on malformed WorldPacket (#1841) ## Summary This PR fixes the Crash 1 Source from Issue [#1840](https://github.com/mod-playerbots/mod-playerbots/issues/1840) posted in a @Regrad posted logs, in `SeeSpellAction::Execute` when an RTSC "see spell" event arrives with an empty or malformed `WorldPacket`. In that case the code used to read from the packet without any validation, causing a `ByteBufferException` and a crash in the map thread. ## Fix - Reset the packet read position and check that the RTSC header (castCount + spellId + castFlags) fits into the packet before reading. - Wrap `SpellCastTargets::Read` in a `try { } catch (ByteBufferException const&) { }` block so truncated RTSC payloads are handled gracefully. - Check that `targets.GetDst()` is not `nullptr` before accessing its position. For valid RTSC packets the behavior is unchanged; malformed packets are now safely ignored instead of crashing the server. ## Testing - Sent bots to multiple locations using RTSC and verified they still move as before. - Reproduced the previous crash scenario with malformed RTSC packets: the worldserver no longer crashes and the event is simply ignored. --------- Co-authored-by: bash Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/strategy/actions/SeeSpellAction.cpp | 42 ++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/strategy/actions/SeeSpellAction.cpp b/src/strategy/actions/SeeSpellAction.cpp index 8b90adece0..f42dcade64 100644 --- a/src/strategy/actions/SeeSpellAction.cpp +++ b/src/strategy/actions/SeeSpellAction.cpp @@ -12,6 +12,7 @@ #include "RTSCValues.h" #include "RtscAction.h" #include "PositionValue.h" +#include "ByteBuffer.h" Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp, bool important) @@ -31,27 +32,52 @@ Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, bool SeeSpellAction::Execute(Event event) { - WorldPacket p(event.getPacket()); // + // RTSC packet data + WorldPacket p(event.getPacket()); + uint8 castCount; uint32 spellId; - uint8 castCount, castFlags; - Player* master = botAI->GetMaster(); + uint8 castFlags; - p.rpos(0); - p >> castCount >> spellId >> castFlags; + // check RTSC header size = castCount (uint8) + spellId (uint32) + castFlags (uint8) + uint32 const rtscHeaderSize = sizeof(uint8) + sizeof(uint32) + sizeof(uint8); + if (p.size() < rtscHeaderSize) + { + LOG_WARN("playerbots", "SeeSpellAction: Corrupt RTSC packet size={}, expected>={}", p.size(), rtscHeaderSize); + return false; + } + Player* master = botAI->GetMaster(); if (!master) return false; + // read RTSC packet data + p.rpos(0); // set read position to start + p >> castCount >> spellId >> castFlags; + // if (!botAI->HasStrategy("RTSC", botAI->GetState())) // return false; if (spellId != RTSC_MOVE_SPELL) return false; - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); - + // should not throw exception,just defensive measure to prevent any crashes when core function breaks. SpellCastTargets targets; - targets.Read(p, botAI->GetMaster()); + try + { + targets.Read(p, master); + if (!targets.GetDst()) + { + // do not dereference a null destination; ignore malformed RTSC packets instead of crashing + LOG_WARN("playerbots", "SeeSpellAction: (malformed) RTSC payload does not contain full targets data"); + return false; + } + } + catch (ByteBufferException const&) + { + // ignore malformed RTSC packets instead of crashing + LOG_WARN("playerbots", "SeeSpellAction: Failed deserialization (malformed) RTSC payload"); + return false; + } WorldPosition spellPosition(master->GetMapId(), targets.GetDst()->_position); SET_AI_VALUE(WorldPosition, "see spell location", spellPosition); From 0c1700c1176222ea422b99270e636baf8c9ad10a Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:56:03 +0100 Subject: [PATCH 03/20] CORE - Improved language detection for bots (#1784) I've had this problem for a long time, my bots only speak English even though I'm playing on a French client. I suppose this must be the case for some other people who do not have a large number of players with the same local client. If we use French DBCs, the bots bug because they only recognize US DBCs. From what I understand, the language is chosen as follows: On load, the module reads the entire `ai_playerbot_texts` table and stores each text variant in a dictionary indexed by the locale ID: the `text` column remains the default value (English), and the `text_loc1` to `text_loc8` columns fill slots 1 through 8. Whenever a real player connects, the module increments a counter for that player's DBC locale using `AddLocalePriority(player->GetSession()->GetSessionDbcLocale())`. When a bot needs a text, `GetLocalePriority()` returns the most frequently used locale index among currently connected players. The corresponding string is then retrieved. if the box is empty, we fall back to the English version (text[0]). ### This PR improve language detection. **Summary** - log both the client DBC locale and the account database locale when a player logs in - fall back to the account locale when the client reports enUS but the account is configured for another locale - keep the existing vote-based selection so bots always speak the majority language among connected players **Therefore, the original behavior is maintained. Bots still choose the most represented language among connected players (the counter is simply more efficient by prioritizing the account's locale when it differs from the client's English). For example, if more English-speaking players are connected, the language will revert to English, as the bots always share the majority locale.** --- src/PlayerbotMgr.cpp | 22 ++++++++++++++++++++-- src/PlayerbotTextMgr.cpp | 13 ++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index 6bdbf9b659..fe4a714f72 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -1602,8 +1602,26 @@ void PlayerbotMgr::OnBotLoginInternal(Player* const bot) void PlayerbotMgr::OnPlayerLogin(Player* player) { + if (!player) + return; + + WorldSession* session = player->GetSession(); + if (!session) + { + LOG_WARN("playerbots", "Unable to register locale priority for player {} because the session is missing", player->GetName()); + return; + } + + // DB locale (source of bot text translation) + LocaleConstant const databaseLocale = session->GetSessionDbLocaleIndex(); + + // For bot texts (DB-driven), prefer the database locale with a safe fallback. + LocaleConstant usedLocale = databaseLocale; + if (usedLocale >= MAX_LOCALES) + usedLocale = LOCALE_enUS; // fallback + // set locale priority for bot texts - sPlayerbotTextMgr->AddLocalePriority(player->GetSession()->GetSessionDbcLocale()); + sPlayerbotTextMgr->AddLocalePriority(usedLocale); if (sPlayerbotAIConfig->selfBotLevel > 2) HandlePlayerbotCommand("self", player); @@ -1611,7 +1629,7 @@ void PlayerbotMgr::OnPlayerLogin(Player* player) if (!sPlayerbotAIConfig->botAutologin) return; - uint32 accountId = player->GetSession()->GetAccountId(); + uint32 accountId = session->GetAccountId(); QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId); if (results) { diff --git a/src/PlayerbotTextMgr.cpp b/src/PlayerbotTextMgr.cpp index 3caa96a738..1dce9a29a5 100644 --- a/src/PlayerbotTextMgr.cpp +++ b/src/PlayerbotTextMgr.cpp @@ -190,26 +190,29 @@ bool PlayerbotTextMgr::GetBotText(std::string name, std::string& text, std::map< void PlayerbotTextMgr::AddLocalePriority(uint32 locale) { - if (!locale) + if (locale >= MAX_LOCALES) + { + LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds MAX_LOCALES ({})", locale, MAX_LOCALES - 1); return; + } botTextLocalePriority[locale]++; } uint32 PlayerbotTextMgr::GetLocalePriority() { - uint32 topLocale = 0; - // if no real players online, reset top locale - if (!sWorldSessionMgr->GetActiveSessionCount()) + uint32 const activeSessions = sWorldSessionMgr->GetActiveSessionCount(); + if (!activeSessions) { ResetLocalePriority(); return 0; } + uint32 topLocale = 0; for (uint8 i = 0; i < MAX_LOCALES; ++i) { - if (botTextLocalePriority[i] > topLocale) + if (botTextLocalePriority[i] > botTextLocalePriority[topLocale]) topLocale = i; } From d97870facdf4c51b9fdcb2fc338c94ea6dfaaf9c Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:45:23 +0100 Subject: [PATCH 04/20] fix: warning updating movement flags while rooted (#1858) fixes https://github.com/mod-playerbots/mod-playerbots/issues/1854 ----- Also includes fixes for: ----- * Bots swimming with waterWalk kept switching between swimming and walking, as result jittering effect swimming under water when water walking active * Bots flying close above water they would land on water and start walking, now they stay flying unless on solid ground they will land and start walking by design ----- Moved all flag setting to updateMovementState: * So all movement flag are handled in updateMovementState which also contains the restricted movement logic. * Handle restricted movement logic and preventing SendMovementFlagUpdate while being restricted. ----- Known issue when flying the following bots feel a bit jittering, wont touch for now at least till core movement changes quirks has been dealt with. The current code is the extended version of what is originally was before core merge with refactored movements. Once the core movement refactors are settled a bit more i would like to revisit this code; as i would expect more imperative code and less manual flag setting e.g. bot->SetWaterWalking, SetGravitiy..SetCanFly etc. --- src/strategy/actions/MovementActions.cpp | 148 +++++++++++------------ 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/strategy/actions/MovementActions.cpp b/src/strategy/actions/MovementActions.cpp index 15e098a521..7078557d1e 100644 --- a/src/strategy/actions/MovementActions.cpp +++ b/src/strategy/actions/MovementActions.cpp @@ -971,73 +971,86 @@ bool MovementAction::Follow(Unit* target, float distance) { return Follow(target void MovementAction::UpdateMovementState() { - // state flags - const float gLvlZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); - const bool onGround = bot->GetPositionZ() < gLvlZ + 1.f; - const bool wantsToFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); - const auto master = botAI ? botAI->GetMaster() : nullptr; // real or not - const bool masterIsFlying = master && master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); - const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); - const auto liquidState = bot->GetLiquidData().Status; // default LIQUID_MAP_NO_WATER - const bool isWaterArea = liquidState != LIQUID_MAP_NO_WATER; - const bool isUnderWater = liquidState == LIQUID_MAP_UNDER_WATER; - const bool isInWater = liquidState == LIQUID_MAP_IN_WATER; - const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - const bool wantsToWaterWalk = bot->HasWaterWalkAura(); - const bool wantsToSwim = isInWater || isUnderWater; - - // handle water state - if (isWaterArea) - { - // water walking - if (wantsToWaterWalk && !isWaterWalking && !isUnderWater && !isFlying) - { - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - bot->AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - bot->SendMovementFlagUpdate(); + const bool isCurrentlyRestricted = // see if the bot is currently slowed, rooted, or otherwise unable to move + bot->isFrozen() || + bot->IsPolymorphed() || + bot->HasRootAura() || + bot->HasStunAura() || + bot->HasConfuseAura() || + bot->HasUnitState(UNIT_STATE_LOST_CONTROL); + + // no update movement flags while movement is current restricted. + if (!isCurrentlyRestricted && bot->IsAlive()) + { + // state flags + const auto master = botAI ? botAI->GetMaster() : nullptr; // real player or not + const bool masterIsFlying = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) : true; + const bool masterIsSwimming = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING) : true; + const auto liquidState = bot->GetLiquidData().Status; // default LIQUID_MAP_NO_WATER + const float gZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + const bool wantsToFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); + const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); + const bool isWaterArea = liquidState != LIQUID_MAP_NO_WATER; + const bool isUnderWater = liquidState == LIQUID_MAP_UNDER_WATER; + const bool isInWater = liquidState == LIQUID_MAP_IN_WATER; + const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + const bool wantsToWaterWalk = bot->HasWaterWalkAura(); + const bool wantsToSwim = isInWater || isUnderWater; + const bool onGroundZ = (bot->GetPositionZ() < gZ + 1.f) && !isWaterArea; + bool movementFlagsUpdated = false; + + // handle water state + if (isWaterArea && !isFlying) + { + // water walking + if (wantsToWaterWalk && !isWaterWalking && !masterIsSwimming) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + bot->AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + movementFlagsUpdated = true; + } + // swimming + else if (wantsToSwim && !isSwimming && masterIsSwimming) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + bot->AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + movementFlagsUpdated = true; + } } - // swimming - else if (wantsToSwim && !isSwimming && !wantsToWaterWalk && !isFlying) + else if (isSwimming || isWaterWalking) { + // reset water flags + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - bot->AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - bot->SendMovementFlagUpdate(); + movementFlagsUpdated = true; } - } - else - { - // reset flags, if not will inherit incorrect walk speed here and there - // when transistions between land and water. - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - bot->SendMovementFlagUpdate(); - } - // handle flying state - if (wantsToFly && !isFlying && masterIsFlying) - { - bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); - bot->SendMovementFlagUpdate(); - } - else if ((!wantsToFly || onGround) && isFlying) - { - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); - bot->SendMovementFlagUpdate(); - } + // handle flying state + if (wantsToFly && !isFlying && masterIsFlying) + { + bot->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + bot->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); + movementFlagsUpdated = true; + } + else if ((!wantsToFly || onGroundZ) && isFlying) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); + movementFlagsUpdated = true; + } - // See if the bot is currently slowed, rooted, or otherwise unable to move - bool isCurrentlyRestricted = bot->isFrozen() || bot->IsPolymorphed() || bot->HasRootAura() || bot->HasStunAura() || - bot->HasConfuseAura() || bot->HasUnitState(UNIT_STATE_LOST_CONTROL); + // detect if movement restrictions have been lifted, CC just ended. + if (wasMovementRestricted) + movementFlagsUpdated = true; // refresh movement state to ensure animations play correctly - // Detect if movement restrictions have been lifted - if (wasMovementRestricted && !isCurrentlyRestricted && bot->IsAlive()) - { - // CC just ended - refresh movement state to ensure animations play correctly - bot->SendMovementFlagUpdate(); + if (movementFlagsUpdated) + bot->SendMovementFlagUpdate(); } - // Save current state for the next check + // Save current state for the next check wasMovementRestricted = isCurrentlyRestricted; // Temporary speed increase in group @@ -1820,25 +1833,12 @@ void MovementAction::DoMovePoint(Unit* unit, float x, float y, float z, bool gen if (!mm) return; - // enable flying - if (unit->HasUnitMovementFlag(MOVEMENTFLAG_FLYING)) - { - unit->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); - unit->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); - } - else - { - unit->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); - unit->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); - } - // enable water walking if (unit->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING)) { - float gLvlZ = unit->GetMapWaterOrGroundLevel(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()); - unit->UpdatePosition(unit->GetPositionX(), unit->GetPositionY(), gLvlZ, false); - // z = gLvlZ; do not overwrite Z axex, otherwise you wont be able to steer the bots into swimming when water - // walking. + float gZ = unit->GetMapWaterOrGroundLevel(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()); + unit->UpdatePosition(unit->GetPositionX(), unit->GetPositionY(), gZ, false); + // z = gZ; no overwrite Z axe otherwise you cant steer the bots into swimming when water walking. } mm->Clear(); From 10213d838177bd6ccf7ad3a5264e160d837bba5d Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:55:55 -0600 Subject: [PATCH 05/20] Add thread safety for group operations (#1816) Fixes crashes and race conditions when bots perform group/guild/arena operations by moving thread-unsafe code to world thread. Potentially fixes #1124 ## Changes - Added operation queue system that runs in world thread - Group operations (invite, remove, convert to raid, set leader) now queued - Arena formation refactored to use queue - Guild operations changed to use packet queueing ## Testing Set `MapUpdate.Threads` > 1 in worldserver.conf to enable multiple map threads, then test: - Group formation and disbanding - Arena team formation - Guild operations (invite, promote, demote, remove) - Run with TSAN cmake ../ \ -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1" \ -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" \ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" \ -DCMAKE_INSTALL_PREFIX=/path/to/install \ -DCMAKE_BUILD_TYPE=RelWithDebInfo build export TSAN_OPTIONS="log_path=tsan_report:halt_on_error=0:second_deadlock_stack=1" ./worldserver The crashes/race conditions should no longer occur with concurrent map threads. ## New Files - `PlayerbotOperation.h` - Base class defining the operation interface (Execute, IsValid, GetPriority) - `PlayerbotOperations.h` - Concrete implementations: GroupInviteOperation, GroupRemoveMemberOperation, GroupConvertToRaidOperation, GroupSetLeaderOperation, ArenaGroupFormationOperation - `PlayerbotWorldThreadProcessor.h/cpp` - Singleton processor with mutex-protected queue, processes operations in WorldScript::OnUpdate hook, handles batch processing and validation --------- Co-authored-by: blinkysc Co-authored-by: SaW Co-authored-by: bash --- src/PlayerbotAIConfig.cpp | 4 +- src/PlayerbotMgr.cpp | 35 +- src/PlayerbotOperation.h | 93 ++++ src/PlayerbotOperations.h | 500 ++++++++++++++++++ src/PlayerbotWorldThreadProcessor.cpp | 217 ++++++++ src/PlayerbotWorldThreadProcessor.h | 142 +++++ src/Playerbots.cpp | 14 +- .../actions/GuildManagementActions.cpp | 32 +- src/strategy/actions/GuildManagementActions.h | 6 +- src/strategy/actions/InviteToGroupAction.cpp | 20 +- .../actions/PassLeadershipToMasterAction.cpp | 7 +- 11 files changed, 1015 insertions(+), 55 deletions(-) create mode 100644 src/PlayerbotOperation.h create mode 100644 src/PlayerbotOperations.h create mode 100644 src/PlayerbotWorldThreadProcessor.cpp create mode 100644 src/PlayerbotWorldThreadProcessor.h diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index bdafc10c63..db69387e6f 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -725,8 +725,8 @@ std::string const PlayerbotAIConfig::GetTimestampStr() // HH hour (2 digits 00-23) // MM minutes (2 digits 00-59) // SS seconds (2 digits 00-59) - char buf[20]; - snprintf(buf, 20, "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour, + char buf[32]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour, aTm->tm_min, aTm->tm_sec); return std::string(buf); } diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index fe4a714f72..d96851dafc 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -27,7 +27,9 @@ #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "PlayerbotFactory.h" +#include "PlayerbotOperations.h" #include "PlayerbotSecurity.h" +#include "PlayerbotWorldThreadProcessor.h" #include "Playerbots.h" #include "RandomPlayerbotMgr.h" #include "SharedDefines.h" @@ -85,7 +87,6 @@ class PlayerbotLoginQueryHolder : public LoginQueryHolder void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId) { - // bot is loading if (botLoading.find(playerGuid) != botLoading.end()) return; @@ -195,7 +196,9 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con } sRandomPlayerbotMgr->OnPlayerLogin(bot); - OnBotLogin(bot); + + auto op = std::make_unique(bot->GetGUID(), this); + sPlayerbotWorldProcessor->QueueOperation(std::move(op)); botLoading.erase(holder.GetGuid()); } @@ -316,11 +319,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) if (!botAI) return; - Group* group = bot->GetGroup(); - if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster()) - { - sPlayerbotDbStore->Save(botAI); - } + // Queue group cleanup operation for world thread + auto cleanupOp = std::make_unique(guid); + sPlayerbotWorldProcessor->QueueOperation(std::move(cleanupOp)); LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str()); bot->SaveToDB(false, false); @@ -549,6 +550,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK); + // Queue group operations for world thread if (master && master->GetGroup() && !group) { Group* mgroup = master->GetGroup(); @@ -556,24 +558,29 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) { if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup()) { - mgroup->ConvertToRaid(); + // Queue ConvertToRaid operation + auto convertOp = std::make_unique(master->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); } if (mgroup->isRaidGroup()) { - mgroup->AddMember(bot); + // Queue AddMember operation + auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(addOp)); } } else { - mgroup->AddMember(bot); + // Queue AddMember operation + auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(addOp)); } } else if (master && !group) { - Group* newGroup = new Group(); - newGroup->Create(master); - sGroupMgr->AddGroup(newGroup); - newGroup->AddMember(bot); + // Queue group creation and AddMember operation + auto inviteOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(inviteOp)); } // if (master) // { diff --git a/src/PlayerbotOperation.h b/src/PlayerbotOperation.h new file mode 100644 index 0000000000..6ac303d323 --- /dev/null +++ b/src/PlayerbotOperation.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_OPERATION_H +#define _PLAYERBOT_OPERATION_H + +#include "Common.h" +#include "ObjectGuid.h" +#include + +/** + * @brief Base class for thread-unsafe operations that must be executed in the world thread + * + * PlayerbotOperation represents an operation that needs to be deferred from a map thread + * to the world thread for safe execution. Examples include group modifications, LFG operations, + * guild operations, etc. + * + * Thread Safety: + * - The constructor and data members must be thread-safe (use copies, not pointers) + * - Execute() is called in the world thread and can safely perform thread-unsafe operations + * - Subclasses must not store raw pointers to (core/world thread) game object (use ObjectGuid instead) + */ +class PlayerbotOperation +{ +public: + virtual ~PlayerbotOperation() = default; + + /** + * @brief Execute this operation in the world thread + * + * This method is called by PlayerbotWorldThreadProcessor::Update() which runs in the world thread. + * It's safe to perform any thread-unsafe operation here (Group, LFG, Guild, etc.) + * + * @return true if operation succeeded, false if it failed + */ + virtual bool Execute() = 0; + + /** + * @brief Get the bot GUID this operation is for (optional) + * + * Used for logging and debugging purposes. + * + * @return ObjectGuid of the bot, or ObjectGuid::Empty if not applicable + */ + virtual ObjectGuid GetBotGuid() const { return ObjectGuid::Empty; } + + /** + * @brief Get the operation priority (higher = more urgent) + * + * Priority levels: + * - 100: Critical (crash prevention, cleanup operations) + * - 50: High (player-facing operations like group invites) + * - 10: Normal (background operations) + * - 0: Low (statistics, logging) + * + * @return Priority value (0-100) + */ + virtual uint32 GetPriority() const { return 10; } + + /** + * @brief Get a human-readable name for this operation + * + * Used for logging and debugging. + * + * @return Operation name + */ + virtual std::string GetName() const { return "Unknown Operation"; } + + /** + * @brief Check if this operation is still valid + * + * Called before Execute() to check if the operation should still be executed. + * For example, if a bot logged out, group invite operations for that bot can be skipped. + * + * @return true if operation should be executed, false to skip + */ + virtual bool IsValid() const { return true; } +}; + +/** + * @brief Comparison operator for priority queue (higher priority first) + */ +struct PlayerbotOperationComparator +{ + bool operator()(const std::unique_ptr& a, const std::unique_ptr& b) const + { + return a->GetPriority() < b->GetPriority(); // Lower priority goes to back of queue + } +}; + +#endif diff --git a/src/PlayerbotOperations.h b/src/PlayerbotOperations.h new file mode 100644 index 0000000000..d7c2b47bb8 --- /dev/null +++ b/src/PlayerbotOperations.h @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_OPERATIONS_H +#define _PLAYERBOT_OPERATIONS_H + +#include "Group.h" +#include "GroupMgr.h" +#include "GuildMgr.h" +#include "ObjectAccessor.h" +#include "PlayerbotOperation.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "PlayerbotMgr.h" +#include "PlayerbotDbStore.h" +#include "RandomPlayerbotMgr.h" + +// Group invite operation +class GroupInviteOperation : public PlayerbotOperation +{ +public: + GroupInviteOperation(ObjectGuid botGuid, ObjectGuid targetGuid) + : m_botGuid(botGuid), m_targetGuid(targetGuid) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* target = ObjectAccessor::FindPlayer(m_targetGuid); + + if (!bot || !target) + { + LOG_DEBUG("playerbots", "GroupInviteOperation: Bot or target not found"); + return false; + } + + // Check if target is already in a group + if (target->GetGroup()) + { + LOG_DEBUG("playerbots", "GroupInviteOperation: Target {} is already in a group", target->GetName()); + return false; + } + + Group* group = bot->GetGroup(); + + // Create group if bot doesn't have one + if (!group) + { + group = new Group; + if (!group->Create(bot)) + { + delete group; + LOG_ERROR("playerbots", "GroupInviteOperation: Failed to create group for bot {}", bot->GetName()); + return false; + } + sGroupMgr->AddGroup(group); + LOG_DEBUG("playerbots", "GroupInviteOperation: Created new group for bot {}", bot->GetName()); + } + + // Convert to raid if needed (more than 5 members) + if (!group->isRaidGroup() && group->GetMembersCount() >= 5) + { + group->ConvertToRaid(); + LOG_DEBUG("playerbots", "GroupInviteOperation: Converted group to raid"); + } + + // Add member to group + if (group->AddMember(target)) + { + LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName()); + return true; + } + else + { + LOG_ERROR("playerbots", "GroupInviteOperation: Failed to add {} to group", target->GetName()); + return false; + } + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } // High priority (player-facing) + + std::string GetName() const override { return "GroupInvite"; } + + bool IsValid() const override + { + // Check if bot still exists and is online + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* target = ObjectAccessor::FindPlayer(m_targetGuid); + return bot && target; + } + +private: + ObjectGuid m_botGuid; + ObjectGuid m_targetGuid; +}; + +// Remove member from group +class GroupRemoveMemberOperation : public PlayerbotOperation +{ +public: + GroupRemoveMemberOperation(ObjectGuid botGuid, ObjectGuid targetGuid) + : m_botGuid(botGuid), m_targetGuid(targetGuid) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* target = ObjectAccessor::FindPlayer(m_targetGuid); + + if (!bot || !target) + return false; + + Group* group = bot->GetGroup(); + if (!group) + { + LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Bot is not in a group"); + return false; + } + + if (!group->IsMember(target->GetGUID())) + { + LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Target is not in bot's group"); + return false; + } + + group->RemoveMember(target->GetGUID()); + LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Removed {} from group", target->GetName()); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } + + std::string GetName() const override { return "GroupRemoveMember"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + return bot != nullptr; + } + +private: + ObjectGuid m_botGuid; + ObjectGuid m_targetGuid; +}; + +// Convert group to raid +class GroupConvertToRaidOperation : public PlayerbotOperation +{ +public: + GroupConvertToRaidOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {} + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + if (!bot) + return false; + + Group* group = bot->GetGroup(); + if (!group) + { + LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Bot is not in a group"); + return false; + } + + if (group->isRaidGroup()) + { + LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Group is already a raid"); + return true; // Success - already in desired state + } + + group->ConvertToRaid(); + LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Converted group to raid"); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } + + std::string GetName() const override { return "GroupConvertToRaid"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + return bot != nullptr; + } + +private: + ObjectGuid m_botGuid; +}; + +// Set group leader +class GroupSetLeaderOperation : public PlayerbotOperation +{ +public: + GroupSetLeaderOperation(ObjectGuid botGuid, ObjectGuid newLeaderGuid) + : m_botGuid(botGuid), m_newLeaderGuid(newLeaderGuid) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid); + + if (!bot || !newLeader) + return false; + + Group* group = bot->GetGroup(); + if (!group) + { + LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Bot is not in a group"); + return false; + } + + if (!group->IsMember(newLeader->GetGUID())) + { + LOG_DEBUG("playerbots", "GroupSetLeaderOperation: New leader is not in the group"); + return false; + } + + group->ChangeLeader(newLeader->GetGUID()); + LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Changed leader to {}", newLeader->GetName()); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } + + std::string GetName() const override { return "GroupSetLeader"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid); + return bot && newLeader; + } + +private: + ObjectGuid m_botGuid; + ObjectGuid m_newLeaderGuid; +}; + +// Form arena group +class ArenaGroupFormationOperation : public PlayerbotOperation +{ +public: + ArenaGroupFormationOperation(ObjectGuid leaderGuid, std::vector memberGuids, + uint32 requiredSize, uint32 arenaTeamId, std::string arenaTeamName) + : m_leaderGuid(leaderGuid), m_memberGuids(memberGuids), + m_requiredSize(requiredSize), m_arenaTeamId(arenaTeamId), m_arenaTeamName(arenaTeamName) + { + } + + bool Execute() override + { + Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid); + if (!leader) + { + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Leader not found"); + return false; + } + + // Step 1: Remove all members from their existing groups + for (const ObjectGuid& memberGuid : m_memberGuids) + { + Player* member = ObjectAccessor::FindPlayer(memberGuid); + if (!member) + continue; + + Group* memberGroup = member->GetGroup(); + if (memberGroup) + { + memberGroup->RemoveMember(memberGuid); + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Removed {} from their existing group", + member->GetName()); + } + } + + // Step 2: Disband leader's existing group + Group* leaderGroup = leader->GetGroup(); + if (leaderGroup) + { + leaderGroup->Disband(true); + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Disbanded leader's existing group"); + } + + // Step 3: Create new group with leader + Group* newGroup = new Group(); + if (!newGroup->Create(leader)) + { + delete newGroup; + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to create arena group for leader {}", + leader->GetName()); + return false; + } + + sGroupMgr->AddGroup(newGroup); + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Created new arena group with leader {}", + leader->GetName()); + + // Step 4: Add members to the new group + uint32 addedMembers = 0; + for (const ObjectGuid& memberGuid : m_memberGuids) + { + Player* member = ObjectAccessor::FindPlayer(memberGuid); + if (!member) + { + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} not found, skipping", + memberGuid.ToString()); + continue; + } + + if (member->GetLevel() < 70) + { + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} is below level 70, skipping", + member->GetName()); + continue; + } + + if (newGroup->AddMember(member)) + { + addedMembers++; + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Added {} to arena group", + member->GetName()); + } + else + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to add {} to arena group", + member->GetName()); + } + + if (addedMembers == 0) + { + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: No members were added to the arena group"); + newGroup->Disband(); + return false; + } + + // Step 5: Teleport members to leader and reset AI + for (const ObjectGuid& memberGuid : m_memberGuids) + { + Player* member = ObjectAccessor::FindPlayer(memberGuid); + if (!member || !newGroup->IsMember(memberGuid)) + continue; + + PlayerbotAI* memberBotAI = sPlayerbotsMgr->GetPlayerbotAI(member); + if (memberBotAI) + memberBotAI->Reset(); + + member->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); + member->TeleportTo(leader->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), + leader->GetPositionZ(), 0); + + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Teleported {} to leader", member->GetName()); + } + + // Check if we have enough members + if (newGroup->GetMembersCount() < m_requiredSize) + { + LOG_INFO("playerbots", "Team #{} <{}> Group is not ready for match (not enough members: {}/{})", + m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount(), m_requiredSize); + newGroup->Disband(); + return false; + } + + LOG_INFO("playerbots", "Team #{} <{}> Group is ready for match with {} members", + m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount()); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_leaderGuid; } + + uint32 GetPriority() const override { return 60; } // Very high priority (arena/BG operations) + + std::string GetName() const override { return "ArenaGroupFormation"; } + + bool IsValid() const override + { + Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid); + return leader != nullptr; + } + +private: + ObjectGuid m_leaderGuid; + std::vector m_memberGuids; + uint32 m_requiredSize; + uint32 m_arenaTeamId; + std::string m_arenaTeamName; +}; + +// Bot logout group cleanup operation +class BotLogoutGroupCleanupOperation : public PlayerbotOperation +{ +public: + BotLogoutGroupCleanupOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {} + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + if (!bot) + return false; + + PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot); + if (!botAI) + return false; + + Group* group = bot->GetGroup(); + if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster()) + sPlayerbotDbStore->Save(botAI); + + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + uint32 GetPriority() const override { return 70; } + std::string GetName() const override { return "BotLogoutGroupCleanup"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + return bot != nullptr; + } + +private: + ObjectGuid m_botGuid; +}; + +// Add player bot operation (for logging in bots from map threads) +class AddPlayerBotOperation : public PlayerbotOperation +{ +public: + AddPlayerBotOperation(ObjectGuid botGuid, uint32 masterAccountId) + : m_botGuid(botGuid), m_masterAccountId(masterAccountId) + { + } + + bool Execute() override + { + sRandomPlayerbotMgr->AddPlayerBot(m_botGuid, m_masterAccountId); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } // High priority + + std::string GetName() const override { return "AddPlayerBot"; } + + bool IsValid() const override + { + return !ObjectAccessor::FindConnectedPlayer(m_botGuid); + } + +private: + ObjectGuid m_botGuid; + uint32 m_masterAccountId; +}; + +class OnBotLoginOperation : public PlayerbotOperation +{ +public: + OnBotLoginOperation(ObjectGuid botGuid, PlayerbotHolder* holder) + : m_botGuid(botGuid), m_holder(holder) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindConnectedPlayer(m_botGuid); + if (!bot || !m_holder) + return false; + + m_holder->OnBotLogin(bot); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + uint32 GetPriority() const override { return 100; } + std::string GetName() const override { return "OnBotLogin"; } + + bool IsValid() const override + { + return ObjectAccessor::FindConnectedPlayer(m_botGuid) != nullptr; + } + +private: + ObjectGuid m_botGuid; + PlayerbotHolder* m_holder; +}; + +#endif diff --git a/src/PlayerbotWorldThreadProcessor.cpp b/src/PlayerbotWorldThreadProcessor.cpp new file mode 100644 index 0000000000..c776eb1207 --- /dev/null +++ b/src/PlayerbotWorldThreadProcessor.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "PlayerbotWorldThreadProcessor.h" + +#include "Log.h" +#include "PlayerbotAIConfig.h" + +#include + +PlayerbotWorldThreadProcessor::PlayerbotWorldThreadProcessor() + : m_enabled(true), m_maxQueueSize(10000), m_batchSize(100), m_queueWarningThreshold(80), + m_timeSinceLastUpdate(0), m_updateInterval(50) // Process at least every 50ms +{ + LOG_INFO("playerbots", "PlayerbotWorldThreadProcessor initialized"); +} + +PlayerbotWorldThreadProcessor::~PlayerbotWorldThreadProcessor() { ClearQueue(); } + +PlayerbotWorldThreadProcessor* PlayerbotWorldThreadProcessor::instance() +{ + static PlayerbotWorldThreadProcessor instance; + return &instance; +} + +void PlayerbotWorldThreadProcessor::Update(uint32 diff) +{ + if (!m_enabled) + return; + + // Accumulate time + m_timeSinceLastUpdate += diff; + + // Don't process too frequently to reduce overhead + if (m_timeSinceLastUpdate < m_updateInterval) + return; + + m_timeSinceLastUpdate = 0; + + // Check queue health (warn if getting full) + CheckQueueHealth(); + + // Process a batch of operations + ProcessBatch(); +} + +bool PlayerbotWorldThreadProcessor::QueueOperation(std::unique_ptr operation) +{ + if (!operation) + { + LOG_ERROR("playerbots", "Attempted to queue null operation"); + return false; + } + + std::lock_guard lock(m_queueMutex); + + // Check if queue is full + if (m_operationQueue.size() >= m_maxQueueSize) + { + LOG_ERROR("playerbots", + "PlayerbotWorldThreadProcessor queue is full ({} operations). Dropping operation: {}", + m_maxQueueSize, operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsSkipped++; + return false; + } + + // Queue the operation + m_operationQueue.push(std::move(operation)); + + // Update statistics + { + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = static_cast(m_operationQueue.size()); + m_stats.maxQueueSize = std::max(m_stats.maxQueueSize, m_stats.currentQueueSize); + } + + return true; +} + +void PlayerbotWorldThreadProcessor::ProcessBatch() +{ + // Extract a batch of operations from the queue + std::vector> batch; + batch.reserve(m_batchSize); + + { + std::lock_guard lock(m_queueMutex); + + // Extract up to batchSize operations + while (!m_operationQueue.empty() && batch.size() < m_batchSize) + { + batch.push_back(std::move(m_operationQueue.front())); + m_operationQueue.pop(); + } + + // Update current queue size stat + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = static_cast(m_operationQueue.size()); + } + + // Execute operations outside of lock to avoid blocking queue + uint32 totalExecutionTime = 0; + for (auto& operation : batch) + { + if (!operation) + continue; + + try + { + // Check if operation is still valid + if (!operation->IsValid()) + { + LOG_DEBUG("playerbots", "Skipping invalid operation: {}", operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsSkipped++; + continue; + } + + // Time the execution + uint32 startTime = getMSTime(); + + // Execute the operation + bool success = operation->Execute(); + + uint32 executionTime = GetMSTimeDiffToNow(startTime); + totalExecutionTime += executionTime; + + // Log slow operations + if (executionTime > 100) + LOG_WARN("playerbots", "Slow operation: {} took {}ms", operation->GetName(), executionTime); + + // Update statistics + std::lock_guard statsLock(m_statsMutex); + if (success) + m_stats.totalOperationsProcessed++; + else + { + m_stats.totalOperationsFailed++; + LOG_DEBUG("playerbots", "Operation failed: {}", operation->GetName()); + } + } + catch (std::exception const& e) + { + LOG_ERROR("playerbots", "Exception in operation {}: {}", operation->GetName(), e.what()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsFailed++; + } + catch (...) + { + LOG_ERROR("playerbots", "Unknown exception in operation {}", operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsFailed++; + } + } + + // Update average execution time + if (!batch.empty()) + { + std::lock_guard statsLock(m_statsMutex); + uint32 avgTime = totalExecutionTime / static_cast(batch.size()); + // Exponential moving average + m_stats.averageExecutionTimeMs = + (m_stats.averageExecutionTimeMs * 9 + avgTime) / 10; // 90% old, 10% new + } +} + +void PlayerbotWorldThreadProcessor::CheckQueueHealth() +{ + uint32 queueSize = GetQueueSize(); + uint32 threshold = (m_maxQueueSize * m_queueWarningThreshold) / 100; + + if (queueSize >= threshold) + { + LOG_WARN("playerbots", + "PlayerbotWorldThreadProcessor queue is {}% full ({}/{}). " + "Consider increasing update frequency or batch size.", + (queueSize * 100) / m_maxQueueSize, queueSize, m_maxQueueSize); + } +} + +uint32 PlayerbotWorldThreadProcessor::GetQueueSize() const +{ + std::lock_guard lock(m_queueMutex); + return static_cast(m_operationQueue.size()); +} + +void PlayerbotWorldThreadProcessor::ClearQueue() +{ + std::lock_guard lock(m_queueMutex); + + uint32 cleared = static_cast(m_operationQueue.size()); + if (cleared > 0) + LOG_INFO("playerbots", "Clearing {} queued operations", cleared); + + // Clear the queue + while (!m_operationQueue.empty()) + { + m_operationQueue.pop(); + } + + // Reset queue size stat + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = 0; +} + +PlayerbotWorldThreadProcessor::Statistics PlayerbotWorldThreadProcessor::GetStatistics() const +{ + std::lock_guard statsLock(m_statsMutex); + return m_stats; // Return a copy +} diff --git a/src/PlayerbotWorldThreadProcessor.h b/src/PlayerbotWorldThreadProcessor.h new file mode 100644 index 0000000000..e37d2b5ba5 --- /dev/null +++ b/src/PlayerbotWorldThreadProcessor.h @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_WORLD_THREAD_PROCESSOR_H +#define _PLAYERBOT_WORLD_THREAD_PROCESSOR_H + +#include "Common.h" +#include "PlayerbotOperation.h" + +#include +#include +#include + +/** + * @brief Processes thread-unsafe bot operations in the world thread + * + * The PlayerbotWorldThreadProcessor manages a queue of operations that must be executed + * in the world thread rather than map threads. This ensures thread safety for operations + * like group modifications, LFG, guilds, battlegrounds, etc. + * + * Architecture: + * - Map threads queue operations via QueueOperation() + * - World thread processes operations via Update() (called from WorldScript::OnUpdate) + * - Operations are processed in priority order + * - Thread-safe queue protected by mutex + * + * Usage: + * auto op = std::make_unique(botGuid, params); + * sPlayerbotWorldProcessor->QueueOperation(std::move(op)); + */ +class PlayerbotWorldThreadProcessor +{ +public: + PlayerbotWorldThreadProcessor(); + ~PlayerbotWorldThreadProcessor(); + + static PlayerbotWorldThreadProcessor* instance(); + + /** + * @brief Update and process queued operations (called from world thread) + * + * This method should be called from WorldScript::OnUpdate hook, which runs in the world thread. + * It processes a batch of queued operations. + * + * @param diff Time since last update in milliseconds + */ + void Update(uint32 diff); + + /** + * @brief Queue an operation for execution in the world thread + * + * Thread-safe method that can be called from any thread (typically map threads). + * The operation will be executed later during Update(). + * + * @param operation Unique pointer to the operation (ownership is transferred) + * @return true if operation was queued, false if queue is full + */ + bool QueueOperation(std::unique_ptr operation); + + /** + * @brief Get current queue size + * + * Thread-safe method for monitoring queue size. + * + * @return Number of operations waiting to be processed + */ + uint32 GetQueueSize() const; + + /** + * @brief Clear all queued operations + * + * Used during shutdown or emergency situations. + */ + void ClearQueue(); + + /** + * @brief Get statistics about operation processing + */ + struct Statistics + { + uint64 totalOperationsProcessed = 0; + uint64 totalOperationsFailed = 0; + uint64 totalOperationsSkipped = 0; + uint32 currentQueueSize = 0; + uint32 maxQueueSize = 0; + uint32 averageExecutionTimeMs = 0; + }; + + Statistics GetStatistics() const; + + /** + * @brief Enable/disable operation processing + * + * When disabled, operations are still queued but not processed. + * Useful for testing or temporary suspension. + * + * @param enabled true to enable processing, false to disable + */ + void SetEnabled(bool enabled) { m_enabled = enabled; } + + bool IsEnabled() const { return m_enabled; } + +private: + /** + * @brief Process a single batch of operations + * + * Extracts operations from queue and executes them. + * Called internally by Update(). + */ + void ProcessBatch(); + + /** + * @brief Check if queue is approaching capacity + * + * Logs warning if queue is getting full. + */ + void CheckQueueHealth(); + + // Thread-safe queue + mutable std::mutex m_queueMutex; + std::queue> m_operationQueue; + + // Configuration + bool m_enabled; + uint32 m_maxQueueSize; // Maximum operations in queue + uint32 m_batchSize; // Operations to process per Update() + uint32 m_queueWarningThreshold; // Warn when queue reaches this percentage + + // Statistics + mutable std::mutex m_statsMutex; + Statistics m_stats; + + // Timing + uint32 m_timeSinceLastUpdate; + uint32 m_updateInterval; // Minimum ms between updates +}; + +#define sPlayerbotWorldProcessor PlayerbotWorldThreadProcessor::instance() + +#endif diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index a7217d7bc4..136d4e6166 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -25,6 +25,7 @@ #include "Metric.h" #include "PlayerScript.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotWorldThreadProcessor.h" #include "RandomPlayerbotMgr.h" #include "ScriptMgr.h" #include "cs_playerbots.h" @@ -300,7 +301,8 @@ class PlayerbotsWorldScript : public WorldScript { public: PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", { - WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED + WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED, + WORLDHOOK_ON_UPDATE }) {} void OnBeforeWorldInitialized() override @@ -329,6 +331,13 @@ class PlayerbotsWorldScript : public WorldScript LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime)); LOG_INFO("server.loading", " "); + LOG_INFO("server.loading", "Playerbots World Thread Processor initialized"); + } + + void OnUpdate(uint32 diff) override + { + sPlayerbotWorldProcessor->Update(diff); + sRandomPlayerbotMgr->UpdateAI(diff); // World thread only } }; @@ -390,8 +399,7 @@ class PlayerbotsScript : public PlayerbotScript void OnPlayerbotUpdate(uint32 diff) override { - sRandomPlayerbotMgr->UpdateAI(diff); - sRandomPlayerbotMgr->UpdateSessions(); + sRandomPlayerbotMgr->UpdateSessions(); // Per-bot updates only } void OnPlayerbotUpdateSessions(Player* player) override diff --git a/src/strategy/actions/GuildManagementActions.cpp b/src/strategy/actions/GuildManagementActions.cpp index 4ab6d72c59..f00a955e7c 100644 --- a/src/strategy/actions/GuildManagementActions.cpp +++ b/src/strategy/actions/GuildManagementActions.cpp @@ -58,6 +58,14 @@ Player* GuidManageAction::GetPlayer(Event event) return nullptr; } +void GuidManageAction::SendPacket(WorldPacket const& packet) +{ + // make a heap copy because QueuePacket takes ownership + WorldPacket* data = new WorldPacket(packet); + + bot->GetSession()->QueuePacket(data); +} + bool GuidManageAction::Execute(Event event) { Player* player = GetPlayer(event); @@ -84,12 +92,6 @@ bool GuildInviteAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_INVITE); } -void GuildInviteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildInviteByName data = WorldPacket(packet); - bot->GetSession()->HandleGuildInviteOpcode(data); -} - bool GuildInviteAction::PlayerIsValid(Player* member) { return !member->GetGuildId() && (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) || @@ -101,12 +103,6 @@ bool GuildPromoteAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_PROMOTE); } -void GuildPromoteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildPromoteMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildPromoteOpcode(data); -} - bool GuildPromoteAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member) - 1; @@ -117,12 +113,6 @@ bool GuildDemoteAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_DEMOTE); } -void GuildDemoteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildDemoteMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildDemoteOpcode(data); -} - bool GuildDemoteAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); @@ -133,12 +123,6 @@ bool GuildRemoveAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_REMOVE); } -void GuildRemoveAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildOfficerRemoveMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildRemoveOpcode(data); -} - bool GuildRemoveAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); diff --git a/src/strategy/actions/GuildManagementActions.h b/src/strategy/actions/GuildManagementActions.h index bf46d1b741..b1d363e89a 100644 --- a/src/strategy/actions/GuildManagementActions.h +++ b/src/strategy/actions/GuildManagementActions.h @@ -25,7 +25,7 @@ class GuidManageAction : public Action bool isUseful() override { return false; } protected: - virtual void SendPacket(WorldPacket data){}; + virtual void SendPacket(WorldPacket const& packet); virtual Player* GetPlayer(Event event); virtual bool PlayerIsValid(Player* member); virtual uint8 GetRankId(Player* member); @@ -44,7 +44,6 @@ class GuildInviteAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -59,7 +58,6 @@ class GuildPromoteAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -74,7 +72,6 @@ class GuildDemoteAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -89,7 +86,6 @@ class GuildRemoveAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; diff --git a/src/strategy/actions/InviteToGroupAction.cpp b/src/strategy/actions/InviteToGroupAction.cpp index 7af26210c6..4d0b4df7b9 100644 --- a/src/strategy/actions/InviteToGroupAction.cpp +++ b/src/strategy/actions/InviteToGroupAction.cpp @@ -9,7 +9,9 @@ #include "Event.h" #include "GuildMgr.h" #include "Log.h" +#include "PlayerbotOperations.h" #include "Playerbots.h" +#include "PlayerbotWorldThreadProcessor.h" #include "ServerFacade.h" bool InviteToGroupAction::Invite(Player* inviter, Player* player) @@ -27,7 +29,10 @@ bool InviteToGroupAction::Invite(Player* inviter, Player* player) { if (GET_PLAYERBOT_AI(player) && !GET_PLAYERBOT_AI(player)->IsRealPlayer()) if (!group->isRaidGroup() && group->GetMembersCount() > 4) - group->ConvertToRaid(); + { + auto convertOp = std::make_unique(inviter->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } } WorldPacket p; @@ -89,7 +94,10 @@ bool InviteNearbyToGroupAction::Execute(Event event) // When inviting the 5th member of the group convert to raid for future invites. if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && bot->GetGroup()->GetMembersCount() > 3) - group->ConvertToRaid(); + { + auto convertOp = std::make_unique(bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot)) { @@ -221,7 +229,8 @@ bool InviteGuildToGroupAction::Execute(Event event) if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && bot->GetGroup()->GetMembersCount() > 3) { - group->ConvertToRaid(); + auto convertOp = std::make_unique(bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); } if (sPlayerbotAIConfig->inviteChat && @@ -362,7 +371,10 @@ bool LfgAction::Execute(Event event) if (param.empty() || param == "5" || group->isRaidGroup()) return false; // Group or raid is full so stop trying. else - group->ConvertToRaid(); // We want a raid but are in a group so convert and continue. + { + auto convertOp = std::make_unique(requester->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } } Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); diff --git a/src/strategy/actions/PassLeadershipToMasterAction.cpp b/src/strategy/actions/PassLeadershipToMasterAction.cpp index ceb1fbbcf2..87890c1c57 100644 --- a/src/strategy/actions/PassLeadershipToMasterAction.cpp +++ b/src/strategy/actions/PassLeadershipToMasterAction.cpp @@ -6,16 +6,17 @@ #include "PassLeadershipToMasterAction.h" #include "Event.h" +#include "PlayerbotOperations.h" #include "Playerbots.h" +#include "PlayerbotWorldThreadProcessor.h" bool PassLeadershipToMasterAction::Execute(Event event) { if (Player* master = GetMaster()) if (master && master != bot && bot->GetGroup() && bot->GetGroup()->IsMember(master->GetGUID())) { - WorldPacket p(SMSG_GROUP_SET_LEADER, 8); - p << master->GetGUID(); - bot->GetSession()->HandleGroupSetLeaderOpcode(p); + auto setLeaderOp = std::make_unique(bot->GetGUID(), master->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(setLeaderOp)); if (!message.empty()) botAI->TellMasterNoFacing(message); From cf743a186a41ce37947e9ef0f4b4dd838c4c0144 Mon Sep 17 00:00:00 2001 From: Crow Date: Sun, 23 Nov 2025 03:06:19 -0600 Subject: [PATCH 06/20] Fix Wrong Misdirection Spell ID for Gruul's Lair and Magtheridon Strategies (#1867) Lol oops. Confirmed with logs/in-game that the prior one was wrong (and thus always returning false) and current one is correct. --- src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h | 2 +- src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h b/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h index 8cd01c2e6b..c7becc8362 100644 --- a/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h +++ b/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h @@ -15,7 +15,7 @@ namespace GruulsLairHelpers SPELL_SPELL_SHIELD = 33054, // Hunter - SPELL_MISDIRECTION = 34477, + SPELL_MISDIRECTION = 35079, // Warlock SPELL_BANISH = 18647, // Rank 2 diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h index 21fd9d459c..80c3a47e09 100644 --- a/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h +++ b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h @@ -24,7 +24,7 @@ namespace MagtheridonHelpers SPELL_FEAR = 6215, // Hunter - SPELL_MISDIRECTION = 34477, + SPELL_MISDIRECTION = 35079, }; enum MagtheridonNPCs From 2424f73bc487a7520a16ffb2dd67316209174485 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sun, 23 Nov 2025 11:45:31 -0800 Subject: [PATCH 07/20] Core Merge PR - Replace OnPlayerChat with OnPlayerCanUseChat (#1838) First stab at getting this working. Im not sure if Im missing something, but it seemed to be a pretty simple change overall. Based on testing the bots do respond to commands via whisper and group. Edit: Relevant PR this addresses. https://github.com/azerothcore/azerothcore-wotlk/commit/50f8f145d224037be9a29c4390c5088816639868#diff-baadebd8cd1117ca48225f316a5ab3fd5fd55b20963394d302341147183db067 --- src/Playerbots.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index 136d4e6166..ecaaa5839a 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -82,12 +82,12 @@ class PlayerbotsPlayerScript : public PlayerScript PlayerbotsPlayerScript() : PlayerScript("PlayerbotsPlayerScript", { PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_AFTER_UPDATE, - PLAYERHOOK_ON_CHAT, - PLAYERHOOK_ON_CHAT_WITH_CHANNEL, - PLAYERHOOK_ON_CHAT_WITH_GROUP, PLAYERHOOK_ON_BEFORE_CRITERIA_PROGRESS, PLAYERHOOK_ON_BEFORE_ACHI_COMPLETE, PLAYERHOOK_CAN_PLAYER_USE_PRIVATE_CHAT, + PLAYERHOOK_CAN_PLAYER_USE_GROUP_CHAT, + PLAYERHOOK_CAN_PLAYER_USE_GUILD_CHAT, + PLAYERHOOK_CAN_PLAYER_USE_CHANNEL_CHAT, PLAYERHOOK_ON_GIVE_EXP, PLAYERHOOK_ON_BEFORE_TELEPORT }) {} @@ -163,15 +163,12 @@ class PlayerbotsPlayerScript : public PlayerScript if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(receiver)) { botAI->HandleCommand(type, msg, player); - - return false; } } - return true; } - void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Group* group) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Group* group) override { for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { @@ -183,9 +180,10 @@ class PlayerbotsPlayerScript : public PlayerScript } } } + return true; } - void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* guild) override { if (type == CHAT_MSG_GUILD) { @@ -204,9 +202,10 @@ class PlayerbotsPlayerScript : public PlayerScript } } } + return true; } - void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override { if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { @@ -217,6 +216,7 @@ class PlayerbotsPlayerScript : public PlayerScript } sRandomPlayerbotMgr->HandleCommand(type, msg, player); + return true; } bool OnPlayerBeforeAchievementComplete(Player* player, AchievementEntry const* achievement) override From d5dbc4ddd70c021175451f55befbcb1411361b38 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:49:55 -0800 Subject: [PATCH 08/20] Hotfix: prevent server crash when whisper 'logout' (#1874) Temp Hotfix to resolve #1870. --- src/Playerbots.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index ecaaa5839a..ed4e482636 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -163,6 +163,12 @@ class PlayerbotsPlayerScript : public PlayerScript if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(receiver)) { botAI->HandleCommand(type, msg, player); + + // hotfix; otherwise the server will crash when whispering logout + // https://github.com/mod-playerbots/mod-playerbots/pull/1838 + // TODO: find the root cause and solve it. (does not happen in party chat) + if (msg == "logout") + return false; } } return true; From 38e2d8584b8772894439e07314facd98345ff823 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 28 Nov 2025 12:00:12 -0600 Subject: [PATCH 09/20] Add missing break in ApplyInstanceStrategies (#1887) Well, I hope nobody has tried Magtheridon lately. It looks like this got inadvertently deleted during the merge. --- src/PlayerbotAI.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 23d073f54c..3748b84e02 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -1477,6 +1477,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) break; case 544: strategyName = "magtheridon"; // Magtheridon's Lair + break; case 565: strategyName = "gruulslair"; // Gruul's Lair break; From 52c3e966416ffe881acd5ff24a375debbbeddde9 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Wed, 3 Dec 2025 04:25:01 -0800 Subject: [PATCH 10/20] Rename groupmaster to groupleader and related variables. (#1875) Fix the naming conventions. Master should be reserved to identify a bots Master. groupleaders are not necessarily group masters and it should be clear what the bot is looking for. (In most solo cases leader=master) --- src/PlayerbotAI.cpp | 6 +-- src/PlayerbotAI.h | 2 +- src/PlayerbotSecurity.cpp | 4 +- src/RandomPlayerbotMgr.cpp | 6 +-- src/strategy/actions/ActionContext.h | 4 +- .../actions/ChooseRpgTargetAction.cpp | 16 +++---- .../actions/ChooseTravelTargetAction.cpp | 2 +- src/strategy/actions/FollowActions.cpp | 16 +++---- src/strategy/actions/FollowActions.h | 4 +- src/strategy/actions/InviteToGroupAction.cpp | 2 +- src/strategy/actions/LeaveGroupAction.cpp | 22 +++++----- .../actions/MoveToTravelTargetAction.cpp | 2 +- src/strategy/actions/MovementActions.cpp | 4 +- .../actions/RandomBotUpdateAction.cpp | 6 +-- src/strategy/actions/ReleaseSpiritAction.cpp | 8 ++-- src/strategy/actions/ResetInstancesAction.cpp | 2 +- .../actions/ReviveFromCorpseAction.cpp | 42 +++++++++---------- src/strategy/actions/RewardAction.cpp | 4 +- src/strategy/actions/RpgSubActions.cpp | 2 +- src/strategy/actions/SecurityCheckAction.cpp | 4 +- src/strategy/actions/TellMasterAction.cpp | 2 +- src/strategy/actions/TravelAction.cpp | 2 +- src/strategy/rogue/RogueTriggers.cpp | 4 +- src/strategy/triggers/RangeTriggers.cpp | 2 +- src/strategy/values/Formations.cpp | 2 +- ...erTargetValue.cpp => GroupLeaderValue.cpp} | 4 +- ...MasterTargetValue.h => GroupLeaderValue.h} | 8 ++-- src/strategy/values/GroupValues.cpp | 14 +++---- src/strategy/values/ValueContext.h | 6 +-- 29 files changed, 101 insertions(+), 101 deletions(-) rename src/strategy/values/{MasterTargetValue.cpp => GroupLeaderValue.cpp} (70%) rename src/strategy/values/{MasterTargetValue.h => GroupLeaderValue.h} (57%) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 3748b84e02..7419574bd2 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -420,7 +420,7 @@ void PlayerbotAI::UpdateAIGroupAndMaster() { botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); - if (botAI->GetMaster() == botAI->GetGroupMaster()) + if (botAI->GetMaster() == botAI->GetGroupLeader()) botAI->TellMaster("Hello, I follow you!"); else botAI->TellMaster(!urand(0, 2) ? "Hello!" : "Hi!"); @@ -4093,7 +4093,7 @@ Player* PlayerbotAI::FindNewMaster() if (!group) return nullptr; - Player* groupLeader = GetGroupMaster(); + Player* groupLeader = GetGroupLeader(); PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(groupLeader); if (!leaderBotAI || leaderBotAI->IsRealPlayer()) return groupLeader; @@ -4144,7 +4144,7 @@ bool PlayerbotAI::HasActivePlayerMaster() { return master && !GET_PLAYERBOT_AI(m bool PlayerbotAI::IsAlt() { return HasRealPlayerMaster() && !sRandomPlayerbotMgr->IsRandomBot(bot); } -Player* PlayerbotAI::GetGroupMaster() +Player* PlayerbotAI::GetGroupLeader() { if (!bot->InBattleground()) if (Group* group = bot->GetGroup()) diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index 8c8cff14f2..ad7641b278 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -540,7 +540,7 @@ class PlayerbotAI : public PlayerbotAIBase // Get the group leader or the master of the bot. // Checks if the bot is summoned as alt of a player bool IsAlt(); - Player* GetGroupMaster(); + Player* GetGroupLeader(); // Returns a semi-random (cycling) number that is fixed for each bot. uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); GrouperType GetGrouperType(); diff --git a/src/PlayerbotSecurity.cpp b/src/PlayerbotSecurity.cpp index 45e8598989..822f40e6d4 100644 --- a/src/PlayerbotSecurity.cpp +++ b/src/PlayerbotSecurity.cpp @@ -251,9 +251,9 @@ bool PlayerbotSecurity::CheckLevelFor(PlayerbotSecurityLevel level, bool silent, out << "I am currently leading a group. I can invite you if you want."; break; case PLAYERBOT_DENY_NOT_LEADER: - if (botAI->GetGroupMaster()) + if (botAI->GetGroupLeader()) { - out << "I am in a group with " << botAI->GetGroupMaster()->GetName() + out << "I am in a group with " << botAI->GetGroupLeader()->GetName() << ". You can ask him for invite."; } else diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index 89a960aecb..1f94e35e13 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -1480,10 +1480,10 @@ bool RandomPlayerbotMgr::ProcessBot(uint32 bot) if (!sRandomPlayerbotMgr->IsRandomBot(player)) update = false; - if (player->GetGroup() && botAI->GetGroupMaster()) + if (player->GetGroup() && botAI->GetGroupLeader()) { - PlayerbotAI* groupMasterBotAI = GET_PLAYERBOT_AI(botAI->GetGroupMaster()); - if (!groupMasterBotAI || groupMasterBotAI->IsRealPlayer()) + PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader()); + if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer()) { update = false; } diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h index cebd7615a7..09c9145f93 100644 --- a/src/strategy/actions/ActionContext.h +++ b/src/strategy/actions/ActionContext.h @@ -121,7 +121,7 @@ class ActionContext : public NamedObjectContext creators["shoot"] = &ActionContext::shoot; creators["follow"] = &ActionContext::follow; creators["move from group"] = &ActionContext::move_from_group; - creators["flee to master"] = &ActionContext::flee_to_master; + creators["flee to group leader"] = &ActionContext::flee_to_group_leader; creators["runaway"] = &ActionContext::runaway; creators["stay"] = &ActionContext::stay; creators["sit"] = &ActionContext::sit; @@ -318,7 +318,7 @@ class ActionContext : public NamedObjectContext static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); } static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); } static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); } - static Action* flee_to_master(PlayerbotAI* botAI) { return new FleeToMasterAction(botAI); } + static Action* flee_to_group_leader(PlayerbotAI* botAI) { return new FleeToGroupLeaderAction(botAI); } static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); } static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); } static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); } diff --git a/src/strategy/actions/ChooseRpgTargetAction.cpp b/src/strategy/actions/ChooseRpgTargetAction.cpp index 20151658cc..0d441e9b67 100644 --- a/src/strategy/actions/ChooseRpgTargetAction.cpp +++ b/src/strategy/actions/ChooseRpgTargetAction.cpp @@ -311,7 +311,7 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldObject* target) bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - Player* gmaster = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Player* realMaster = botAI->GetMaster(); AiObjectContext* context = botAI->GetAiObjectContext(); @@ -327,30 +327,30 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) return false; } - if (!gmaster || bot == gmaster) + if (!groupLeader || bot == groupLeader) return true; if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) return true; - if (bot->GetDistance(gmaster) > sPlayerbotAIConfig->rpgDistance * 2) + if (bot->GetDistance(groupLeader) > sPlayerbotAIConfig->rpgDistance * 2) return false; Formation* formation = AI_VALUE(Formation*, "formation"); - float distance = gmaster->GetDistance2d(pos.getX(), pos.getY()); + float distance = groupLeader->GetDistance2d(pos.getX(), pos.getY()); if (!botAI->HasActivePlayerMaster() && distance < 50.0f) { - Player* player = gmaster; - if (gmaster && !gmaster->isMoving() || + Player* player = groupLeader; + if (groupLeader && !groupLeader->isMoving() || PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance) return true; } - if ((inDungeon || !gmaster->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == gmaster && distance > 5.0f) + if ((inDungeon || !groupLeader->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == groupLeader && distance > 5.0f) return false; - if (!gmaster->isMoving() && distance < 25.0f) + if (!groupLeader->isMoving() && distance < 25.0f) return true; if (distance < formation->GetMaxDistance()) diff --git a/src/strategy/actions/ChooseTravelTargetAction.cpp b/src/strategy/actions/ChooseTravelTargetAction.cpp index 3843f35876..cf6dddd40c 100644 --- a/src/strategy/actions/ChooseTravelTargetAction.cpp +++ b/src/strategy/actions/ChooseTravelTargetAction.cpp @@ -180,7 +180,7 @@ void ChooseTravelTargetAction::getNewTarget(TravelTarget* newTarget, TravelTarge void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget) { // Tell the master where we are going. - if (!bot->GetGroup() || (botAI->GetGroupMaster() == bot)) + if (!bot->GetGroup() || (botAI->GetGroupLeader() == bot)) ReportTravelTarget(newTarget, oldTarget); // If we are heading to a creature/npc clear it from the ignore list. diff --git a/src/strategy/actions/FollowActions.cpp b/src/strategy/actions/FollowActions.cpp index f4a66bf9e7..d168e8cc93 100644 --- a/src/strategy/actions/FollowActions.cpp +++ b/src/strategy/actions/FollowActions.cpp @@ -70,7 +70,7 @@ bool FollowAction::isUseful() if (!target.empty()) fTarget = AI_VALUE(Unit*, target); else - fTarget = AI_VALUE(Unit*, "master target"); + fTarget = AI_VALUE(Unit*, "group leader"); if (fTarget) { @@ -114,9 +114,9 @@ bool FollowAction::CanDeadFollow(Unit* target) return true; } -bool FleeToMasterAction::Execute(Event event) +bool FleeToGroupLeaderAction::Execute(Event event) { - Unit* fTarget = AI_VALUE(Unit*, "master target"); + Unit* fTarget = AI_VALUE(Unit*, "group leader"); bool canFollow = Follow(fTarget); if (!canFollow) { @@ -146,22 +146,22 @@ bool FleeToMasterAction::Execute(Event event) return true; } -bool FleeToMasterAction::isUseful() +bool FleeToGroupLeaderAction::isUseful() { - if (!botAI->GetGroupMaster()) + if (!botAI->GetGroupLeader()) return false; - if (botAI->GetGroupMaster() == bot) + if (botAI->GetGroupLeader() == bot) return false; Unit* target = AI_VALUE(Unit*, "current target"); - if (target && botAI->GetGroupMaster()->GetTarget() == target->GetGUID()) + if (target && botAI->GetGroupLeader()->GetTarget() == target->GetGUID()) return false; if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) return false; - Unit* fTarget = AI_VALUE(Unit*, "master target"); + Unit* fTarget = AI_VALUE(Unit*, "group leader"); if (!CanDeadFollow(fTarget)) return false; diff --git a/src/strategy/actions/FollowActions.h b/src/strategy/actions/FollowActions.h index 5468e34131..9331bbf556 100644 --- a/src/strategy/actions/FollowActions.h +++ b/src/strategy/actions/FollowActions.h @@ -20,10 +20,10 @@ class FollowAction : public MovementAction bool CanDeadFollow(Unit* target); }; -class FleeToMasterAction : public FollowAction +class FleeToGroupLeaderAction : public FollowAction { public: - FleeToMasterAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to master") {} + FleeToGroupLeaderAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to group leader") {} bool Execute(Event event) override; bool isUseful() override; diff --git a/src/strategy/actions/InviteToGroupAction.cpp b/src/strategy/actions/InviteToGroupAction.cpp index 4d0b4df7b9..bec515fefb 100644 --- a/src/strategy/actions/InviteToGroupAction.cpp +++ b/src/strategy/actions/InviteToGroupAction.cpp @@ -141,7 +141,7 @@ bool InviteNearbyToGroupAction::isUseful() if (group->isRaidGroup() && group->IsFull()) return false; - if (botAI->GetGroupMaster() != bot) + if (botAI->GetGroupLeader() != bot) return false; uint32 memberCount = group->GetMembersCount(); diff --git a/src/strategy/actions/LeaveGroupAction.cpp b/src/strategy/actions/LeaveGroupAction.cpp index 039c476bf5..a279c9426c 100644 --- a/src/strategy/actions/LeaveGroupAction.cpp +++ b/src/strategy/actions/LeaveGroupAction.cpp @@ -109,22 +109,22 @@ bool LeaveFarAwayAction::isUseful() if (!bot->GetGroup()) return false; - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Player* trueMaster = botAI->GetMaster(); - if (!master || (bot == master && !botAI->IsRealPlayer())) + if (!groupLeader || (bot == groupLeader && !botAI->IsRealPlayer())) return false; - PlayerbotAI* masterBotAI = nullptr; - if (master) - masterBotAI = GET_PLAYERBOT_AI(master); - if (master && !masterBotAI) + PlayerbotAI* groupLeaderBotAI = nullptr; + if (groupLeader) + groupLeaderBotAI = GET_PLAYERBOT_AI(groupLeader); + if (groupLeader && !groupLeaderBotAI) return false; if (trueMaster && !GET_PLAYERBOT_AI(trueMaster)) return false; if (botAI->IsAlt() && - (!masterBotAI || masterBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player master. + (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player groupLeader. return false; if (botAI->GetGrouperType() == GrouperType::SOLO) @@ -138,19 +138,19 @@ bool LeaveFarAwayAction::isUseful() if (dCount > 4 && !botAI->HasRealPlayerMaster()) return true; - if (bot->GetGuildId() == master->GetGuildId()) + if (bot->GetGuildId() == groupLeader->GetGuildId()) { - if (bot->GetLevel() > master->GetLevel() + 5) + if (bot->GetLevel() > groupLeader->GetLevel() + 5) { if (AI_VALUE(bool, "should get money")) return false; } } - if (abs(int32(master->GetLevel() - bot->GetLevel())) > 4) + if (abs(int32(groupLeader->GetLevel() - bot->GetLevel())) > 4) return true; - if (bot->GetMapId() != master->GetMapId() || bot->GetDistance2d(master) >= 2 * sPlayerbotAIConfig->rpgDistance) + if (bot->GetMapId() != groupLeader->GetMapId() || bot->GetDistance2d(groupLeader) >= 2 * sPlayerbotAIConfig->rpgDistance) { return true; } diff --git a/src/strategy/actions/MoveToTravelTargetAction.cpp b/src/strategy/actions/MoveToTravelTargetAction.cpp index a4dec2caac..ed60339b82 100644 --- a/src/strategy/actions/MoveToTravelTargetAction.cpp +++ b/src/strategy/actions/MoveToTravelTargetAction.cpp @@ -18,7 +18,7 @@ bool MoveToTravelTargetAction::Execute(Event event) WorldLocation location = *target->getPosition(); Group* group = bot->GetGroup(); - if (group && !urand(0, 1) && bot == botAI->GetGroupMaster() && !bot->IsInCombat()) + if (group && !urand(0, 1) && bot == botAI->GetGroupLeader() && !bot->IsInCombat()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { diff --git a/src/strategy/actions/MovementActions.cpp b/src/strategy/actions/MovementActions.cpp index 7078557d1e..f66638a7a1 100644 --- a/src/strategy/actions/MovementActions.cpp +++ b/src/strategy/actions/MovementActions.cpp @@ -1102,7 +1102,7 @@ void MovementAction::UpdateMovementState() // { // if (Unit* pTarget = sServerFacade->GetChaseTarget(bot)) // { - // if (pTarget != botAI->GetGroupMaster()) + // if (pTarget != botAI->GetGroupLeader()) // return; // if (!bot->IsWithinMeleeRange(pTarget)) @@ -2663,7 +2663,7 @@ bool DisperseSetAction::Execute(Event event) return true; } -bool RunAwayAction::Execute(Event event) { return Flee(AI_VALUE(Unit*, "master target")); } +bool RunAwayAction::Execute(Event event) { return Flee(AI_VALUE(Unit*, "group leader")); } bool MoveToLootAction::Execute(Event event) { diff --git a/src/strategy/actions/RandomBotUpdateAction.cpp b/src/strategy/actions/RandomBotUpdateAction.cpp index 1536cd6b92..61a8360e59 100644 --- a/src/strategy/actions/RandomBotUpdateAction.cpp +++ b/src/strategy/actions/RandomBotUpdateAction.cpp @@ -13,10 +13,10 @@ bool RandomBotUpdateAction::Execute(Event event) if (!sRandomPlayerbotMgr->IsRandomBot(bot)) return false; - if (bot->GetGroup() && botAI->GetGroupMaster()) + if (bot->GetGroup() && botAI->GetGroupLeader()) { - PlayerbotAI* groupMasterBotAI = GET_PLAYERBOT_AI(botAI->GetGroupMaster()); - if (!groupMasterBotAI || groupMasterBotAI->IsRealPlayer()) + PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader()); + if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer()) return true; } diff --git a/src/strategy/actions/ReleaseSpiritAction.cpp b/src/strategy/actions/ReleaseSpiritAction.cpp index b06fc89263..17fcc42a0b 100644 --- a/src/strategy/actions/ReleaseSpiritAction.cpp +++ b/src/strategy/actions/ReleaseSpiritAction.cpp @@ -168,15 +168,15 @@ bool AutoReleaseSpiritAction::ShouldAutoRelease() const if (!bot->GetGroup()) return true; - Player* groupMaster = botAI->GetGroupMaster(); - if (!groupMaster || groupMaster == bot) + Player* groupLeader = botAI->GetGroupLeader(); + if (!groupLeader || groupLeader == bot) return true; if (!botAI->HasActivePlayerMaster()) return true; if (botAI->HasActivePlayerMaster() && - groupMaster->GetMapId() == bot->GetMapId() && + groupLeader->GetMapId() == bot->GetMapId() && bot->GetMap() && (bot->GetMap()->IsRaid() || bot->GetMap()->IsDungeon())) { @@ -184,7 +184,7 @@ bool AutoReleaseSpiritAction::ShouldAutoRelease() const } return sServerFacade->IsDistanceGreaterThan( - AI_VALUE2(float, "distance", "master target"), + AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->sightDistance); } diff --git a/src/strategy/actions/ResetInstancesAction.cpp b/src/strategy/actions/ResetInstancesAction.cpp index 1c0a6f2480..cce5eef1e3 100644 --- a/src/strategy/actions/ResetInstancesAction.cpp +++ b/src/strategy/actions/ResetInstancesAction.cpp @@ -16,4 +16,4 @@ bool ResetInstancesAction::Execute(Event event) return true; } -bool ResetInstancesAction::isUseful() { return botAI->GetGroupMaster() == bot; }; +bool ResetInstancesAction::isUseful() { return botAI->GetGroupLeader() == bot; }; diff --git a/src/strategy/actions/ReviveFromCorpseAction.cpp b/src/strategy/actions/ReviveFromCorpseAction.cpp index 74237670df..ce0b1fa05d 100644 --- a/src/strategy/actions/ReviveFromCorpseAction.cpp +++ b/src/strategy/actions/ReviveFromCorpseAction.cpp @@ -17,14 +17,14 @@ bool ReviveFromCorpseAction::Execute(Event event) { - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Corpse* corpse = bot->GetCorpse(); - // follow master when master revives + // follow group Leader when group Leader revives WorldPacket& p = event.getPacket(); - if (!p.empty() && p.GetOpcode() == CMSG_RECLAIM_CORPSE && master && !corpse && bot->IsAlive()) + if (!p.empty() && p.GetOpcode() == CMSG_RECLAIM_CORPSE && groupLeader && !corpse && bot->IsAlive()) { - if (sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + if (sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->farDistance)) { if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) @@ -43,10 +43,10 @@ bool ReviveFromCorpseAction::Execute(Event event) // time(nullptr)) // return false; - if (master) + if (groupLeader) { - if (!GET_PLAYERBOT_AI(master) && master->isDead() && master->GetCorpse() && - sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + if (!GET_PLAYERBOT_AI(groupLeader) && groupLeader->isDead() && groupLeader->GetCorpse() && + sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->farDistance)) return false; } @@ -79,15 +79,15 @@ bool FindCorpseAction::Execute(Event event) if (bot->InBattleground()) return false; - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Corpse* corpse = bot->GetCorpse(); if (!corpse) return false; - // if (master) + // if (groupLeader) // { - // if (!GET_PLAYERBOT_AI(master) && - // sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + // if (!GET_PLAYERBOT_AI(groupLeader) && + // sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), // sPlayerbotAIConfig->farDistance)) return false; // } @@ -110,20 +110,20 @@ bool FindCorpseAction::Execute(Event event) WorldPosition botPos(bot); WorldPosition corpsePos(corpse); WorldPosition moveToPos = corpsePos; - WorldPosition masterPos(master); + WorldPosition leaderPos(groupLeader); float reclaimDist = CORPSE_RECLAIM_RADIUS - 5.0f; float corpseDist = botPos.distance(corpsePos); int64 deadTime = time(nullptr) - corpse->GetGhostTime(); - bool moveToMaster = master && master != bot && masterPos.fDist(corpsePos) < reclaimDist; + bool moveToLeader = groupLeader && groupLeader != bot && leaderPos.fDist(corpsePos) < reclaimDist; // Should we ressurect? If so, return false. if (corpseDist < reclaimDist) { - if (moveToMaster) // We are near master. + if (moveToLeader) // We are near group leader. { - if (botPos.fDist(masterPos) < sPlayerbotAIConfig->spellDistance) + if (botPos.fDist(leaderPos) < sPlayerbotAIConfig->spellDistance) return false; } else if (deadTime > 8 * MINUTE) // We have walked too long already. @@ -140,8 +140,8 @@ bool FindCorpseAction::Execute(Event event) // If we are getting close move to a save ressurrection spot instead of just the corpse. if (corpseDist < sPlayerbotAIConfig->reactDistance) { - if (moveToMaster) - moveToPos = masterPos; + if (moveToLeader) + moveToPos = leaderPos; else { FleeManager manager(bot, reclaimDist, 0.0, urand(0, 1), moveToPos); @@ -215,12 +215,12 @@ GraveyardStruct const* SpiritHealerAction::GetGrave(bool startZone) if (!startZone && ClosestGrave) return ClosestGrave; - if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && botAI->GetGroupMaster() && botAI->GetGroupMaster() != bot) + if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && botAI->GetGroupLeader() && botAI->GetGroupLeader() != bot) { - Player* master = botAI->GetGroupMaster(); - if (master && master != bot) + Player* groupLeader = botAI->GetGroupLeader(); + if (groupLeader && groupLeader != bot) { - ClosestGrave = sGraveyard->GetClosestGraveyard(master, bot->GetTeamId()); + ClosestGrave = sGraveyard->GetClosestGraveyard(groupLeader, bot->GetTeamId()); if (ClosestGrave) return ClosestGrave; diff --git a/src/strategy/actions/RewardAction.cpp b/src/strategy/actions/RewardAction.cpp index 1022166d0f..8fe1c6ff72 100644 --- a/src/strategy/actions/RewardAction.cpp +++ b/src/strategy/actions/RewardAction.cpp @@ -35,8 +35,8 @@ bool RewardAction::Execute(Event event) return true; } - Unit* mtar = AI_VALUE(Unit*, "master target"); - if (mtar && Reward(itemId, mtar)) + Unit* groupLeaderUnit = AI_VALUE(Unit*, "group leader"); + if (groupLeaderUnit && Reward(itemId, groupLeaderUnit)) return true; botAI->TellError("Cannot talk to quest giver"); diff --git a/src/strategy/actions/RpgSubActions.cpp b/src/strategy/actions/RpgSubActions.cpp index 784f6bbb20..aa4269fa92 100644 --- a/src/strategy/actions/RpgSubActions.cpp +++ b/src/strategy/actions/RpgSubActions.cpp @@ -76,7 +76,7 @@ void RpgHelper::setFacing(GuidPosition guidPosition) void RpgHelper::setDelay(bool waitForGroup) { - if (!botAI->HasRealPlayerMaster() || (waitForGroup && botAI->GetGroupMaster() == bot && bot->GetGroup())) + if (!botAI->HasRealPlayerMaster() || (waitForGroup && botAI->GetGroupLeader() == bot && bot->GetGroup())) botAI->SetNextCheckDelay(sPlayerbotAIConfig->rpgDelay); else botAI->SetNextCheckDelay(sPlayerbotAIConfig->rpgDelay / 5); diff --git a/src/strategy/actions/SecurityCheckAction.cpp b/src/strategy/actions/SecurityCheckAction.cpp index 1320eb04a6..c47a6e52cb 100644 --- a/src/strategy/actions/SecurityCheckAction.cpp +++ b/src/strategy/actions/SecurityCheckAction.cpp @@ -22,8 +22,8 @@ bool SecurityCheckAction::Execute(Event event) ItemQualities threshold = group->GetLootThreshold(); if (method == MASTER_LOOT || method == FREE_FOR_ALL || threshold > ITEM_QUALITY_UNCOMMON) { - if ((botAI->GetGroupMaster()->GetSession()->GetSecurity() == SEC_PLAYER) && - (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupMaster()->GetGuildId())) + if ((botAI->GetGroupLeader()->GetSession()->GetSecurity() == SEC_PLAYER) && + (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupLeader()->GetGuildId())) { botAI->TellError("I will play with this loot type only if I'm in your guild :/"); botAI->ChangeStrategy("+passive,+stay", BOT_STATE_NON_COMBAT); diff --git a/src/strategy/actions/TellMasterAction.cpp b/src/strategy/actions/TellMasterAction.cpp index 701f93d773..4b8d96d561 100644 --- a/src/strategy/actions/TellMasterAction.cpp +++ b/src/strategy/actions/TellMasterAction.cpp @@ -22,7 +22,7 @@ bool OutOfReactRangeAction::Execute(Event event) bool OutOfReactRangeAction::isUseful() { - bool canFollow = Follow(AI_VALUE(Unit*, "master target")); + bool canFollow = Follow(AI_VALUE(Unit*, "group leader")); if (!canFollow) { return false; diff --git a/src/strategy/actions/TravelAction.cpp b/src/strategy/actions/TravelAction.cpp index 5804fd7d24..f99f8b29d3 100644 --- a/src/strategy/actions/TravelAction.cpp +++ b/src/strategy/actions/TravelAction.cpp @@ -64,7 +64,7 @@ bool MoveToDarkPortalAction::Execute(Event event) { if (bot->GetGroup()) if (bot->GetGroup()->GetLeaderGUID() != bot->GetGUID() && - !GET_PLAYERBOT_AI(GET_PLAYERBOT_AI(bot)->GetGroupMaster())) + !GET_PLAYERBOT_AI(GET_PLAYERBOT_AI(bot)->GetGroupLeader())) return false; if (bot->GetLevel() > 57) diff --git a/src/strategy/rogue/RogueTriggers.cpp b/src/strategy/rogue/RogueTriggers.cpp index 16aada0164..fd9901552e 100644 --- a/src/strategy/rogue/RogueTriggers.cpp +++ b/src/strategy/rogue/RogueTriggers.cpp @@ -22,8 +22,8 @@ bool UnstealthTrigger::IsActive() return botAI->HasAura("stealth", bot) && !AI_VALUE(uint8, "attacker count") && (AI_VALUE2(bool, "moving", "self target") && ((botAI->GetMaster() && - sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "master target"), 10.0f) && - AI_VALUE2(bool, "moving", "master target")) || + sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "group leader"), 10.0f) && + AI_VALUE2(bool, "moving", "group leader")) || !AI_VALUE(uint8, "attacker count"))); } diff --git a/src/strategy/triggers/RangeTriggers.cpp b/src/strategy/triggers/RangeTriggers.cpp index 3c60934fd2..af29f984d1 100644 --- a/src/strategy/triggers/RangeTriggers.cpp +++ b/src/strategy/triggers/RangeTriggers.cpp @@ -213,7 +213,7 @@ PartyMemberToHealOutOfSpellRangeTrigger::PartyMemberToHealOutOfSpellRangeTrigger bool FarFromMasterTrigger::IsActive() { - return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "master target"), distance); + return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "group leader"), distance); } bool TooCloseToCreatureTrigger::TooCloseToCreature(uint32 creatureId, float range, bool alive) diff --git a/src/strategy/values/Formations.cpp b/src/strategy/values/Formations.cpp index 21c43f402a..30c738252b 100644 --- a/src/strategy/values/Formations.cpp +++ b/src/strategy/values/Formations.cpp @@ -62,7 +62,7 @@ class MeleeFormation : public FollowFormation public: MeleeFormation(PlayerbotAI* botAI) : FollowFormation(botAI, "melee") {} - std::string const GetTargetName() override { return "master target"; } + std::string const GetTargetName() override { return "group leader"; } }; class QueueFormation : public FollowFormation diff --git a/src/strategy/values/MasterTargetValue.cpp b/src/strategy/values/GroupLeaderValue.cpp similarity index 70% rename from src/strategy/values/MasterTargetValue.cpp rename to src/strategy/values/GroupLeaderValue.cpp index 6fddb6d99b..18ed007648 100644 --- a/src/strategy/values/MasterTargetValue.cpp +++ b/src/strategy/values/GroupLeaderValue.cpp @@ -3,8 +3,8 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#include "MasterTargetValue.h" +#include "GroupLeaderValue.h" #include "Playerbots.h" -Unit* MasterTargetValue::Calculate() { return botAI->GetGroupMaster(); } +Unit* GroupLeaderValue::Calculate() { return botAI->GetGroupLeader(); } diff --git a/src/strategy/values/MasterTargetValue.h b/src/strategy/values/GroupLeaderValue.h similarity index 57% rename from src/strategy/values/MasterTargetValue.h rename to src/strategy/values/GroupLeaderValue.h index a9f8f781df..a359620437 100644 --- a/src/strategy/values/MasterTargetValue.h +++ b/src/strategy/values/GroupLeaderValue.h @@ -3,18 +3,18 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_MASTERTARGETVALUE_H -#define _PLAYERBOT_MASTERTARGETVALUE_H +#ifndef _PLAYERBOT_GROUPLEADERVALUE_H +#define _PLAYERBOT_GROUPLEADERVALUE_H #include "Value.h" class PlayerbotAI; class Unit; -class MasterTargetValue : public UnitCalculatedValue +class GroupLeaderValue : public UnitCalculatedValue { public: - MasterTargetValue(PlayerbotAI* botAI, std::string const name = "master target") : UnitCalculatedValue(botAI, name) + GroupLeaderValue(PlayerbotAI* botAI, std::string const name = "group leader") : UnitCalculatedValue(botAI, name) { } diff --git a/src/strategy/values/GroupValues.cpp b/src/strategy/values/GroupValues.cpp index f9cb21028e..01049a7969 100644 --- a/src/strategy/values/GroupValues.cpp +++ b/src/strategy/values/GroupValues.cpp @@ -28,7 +28,7 @@ GuidVector GroupMembersValue::Calculate() bool IsFollowingPartyValue::Calculate() { - if (botAI->GetGroupMaster() == bot) + if (botAI->GetGroupLeader() == bot) return true; if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) @@ -39,15 +39,15 @@ bool IsFollowingPartyValue::Calculate() bool IsNearLeaderValue::Calculate() { - Player* groupMaster = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); - if (!groupMaster) + if (!groupLeader) return false; - if (groupMaster == bot) + if (groupLeader == bot) return true; - return sServerFacade->GetDistance2d(bot, botAI->GetGroupMaster()) < sPlayerbotAIConfig->sightDistance; + return sServerFacade->GetDistance2d(bot, botAI->GetGroupLeader()) < sPlayerbotAIConfig->sightDistance; } bool BoolANDValue::Calculate() @@ -154,8 +154,8 @@ bool GroupReadyValue::Calculate() // We only wait for members that are in range otherwise we might be waiting for bots stuck in dead loops // forever. - if (botAI->GetGroupMaster() && - sServerFacade->GetDistance2d(member, botAI->GetGroupMaster()) > sPlayerbotAIConfig->sightDistance) + if (botAI->GetGroupLeader() && + sServerFacade->GetDistance2d(member, botAI->GetGroupLeader()) > sPlayerbotAIConfig->sightDistance) continue; if (member->GetHealthPct() < sPlayerbotAIConfig->almostFullHealth) diff --git a/src/strategy/values/ValueContext.h b/src/strategy/values/ValueContext.h index b8e79d6e5a..adf03be8ea 100644 --- a/src/strategy/values/ValueContext.h +++ b/src/strategy/values/ValueContext.h @@ -30,6 +30,7 @@ #include "Formations.h" #include "GrindTargetValue.h" #include "GroupValues.h" +#include "GroupLeaderValue.h" #include "GuildValues.h" #include "HasAvailableLootValue.h" #include "HasTotemValue.h" @@ -51,7 +52,6 @@ #include "LootStrategyValue.h" #include "MaintenanceValues.h" #include "ManaSaveLevelValue.h" -#include "MasterTargetValue.h" #include "NearestAdsValue.h" #include "NearestCorpsesValue.h" #include "NearestFriendlyPlayersValue.h" @@ -130,7 +130,7 @@ class ValueContext : public NamedObjectContext creators["party member to resurrect"] = &ValueContext::party_member_to_resurrect; creators["current target"] = &ValueContext::current_target; creators["self target"] = &ValueContext::self_target; - creators["master target"] = &ValueContext::master; + creators["group leader"] = &ValueContext::group_leader; creators["line target"] = &ValueContext::line_target; creators["tank target"] = &ValueContext::tank_target; creators["dps target"] = &ValueContext::dps_target; @@ -439,7 +439,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* current_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* old_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* self_target(PlayerbotAI* botAI) { return new SelfTargetValue(botAI); } - static UntypedValue* master(PlayerbotAI* botAI) { return new MasterTargetValue(botAI); } + static UntypedValue* group_leader(PlayerbotAI* botAI) { return new GroupLeaderValue(botAI); } static UntypedValue* line_target(PlayerbotAI* botAI) { return new LineTargetValue(botAI); } static UntypedValue* tank_target(PlayerbotAI* botAI) { return new TankTargetValue(botAI); } static UntypedValue* dps_target(PlayerbotAI* botAI) { return new DpsTargetValue(botAI); } From 353c29dfc44dd9779dea1348d8b10eeeb1580efe Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Mon, 8 Dec 2025 03:25:40 -0800 Subject: [PATCH 11/20] Bug: Fix bots leaving LFG groups before master (#1876) I removed bots checking if they should leave group every tick, and will rely on the LeaveGroupFarAway action. I also increased the timer from 5 seconds to 20 seconds. No need to check this that often. --- src/PlayerbotAI.cpp | 6 ++---- src/PlayerbotAI.h | 2 +- src/strategy/generic/GroupStrategy.cpp | 5 ++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 7419574bd2..7975807f88 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -365,7 +365,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Update the bot's group status (moved to helper function) - UpdateAIGroupAndMaster(); + UpdateAIGroupMaster(); // Update internal AI UpdateAIInternal(elapsed, minimal); @@ -373,7 +373,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Helper function for UpdateAI to check group membership and handle removal if necessary -void PlayerbotAI::UpdateAIGroupAndMaster() +void PlayerbotAI::UpdateAIGroupMaster() { if (!bot) return; @@ -431,8 +431,6 @@ void PlayerbotAI::UpdateAIGroupAndMaster() botAI->ChangeStrategy("-follow", BOT_STATE_NON_COMBAT); } } - else if (!newMaster && !bot->InBattleground()) - LeaveOrDisbandGroup(); } } diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index ad7641b278..7252cd7720 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -612,7 +612,7 @@ class PlayerbotAI : public PlayerbotAIBase static void _fillGearScoreData(Player* player, Item* item, std::vector* gearScore, uint32& twoHandScore, bool mixed = false); bool IsTellAllowed(PlayerbotSecurityLevel securityLevel = PLAYERBOT_SECURITY_ALLOW_ALL); - void UpdateAIGroupAndMaster(); + void UpdateAIGroupMaster(); Item* FindItemInInventory(std::function checkItem) const; void HandleCommands(); void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL); diff --git a/src/strategy/generic/GroupStrategy.cpp b/src/strategy/generic/GroupStrategy.cpp index 9406e8409d..9cfba6e2fc 100644 --- a/src/strategy/generic/GroupStrategy.cpp +++ b/src/strategy/generic/GroupStrategy.cpp @@ -11,7 +11,6 @@ void GroupStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("invite nearby", 4.0f), nullptr))); triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("invite guild", 4.0f), nullptr))); - triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("leave far away", 4.0f), nullptr))); - triggers.push_back( - new TriggerNode("seldom", NextAction::array(0, new NextAction("reset instances", 1.0f), nullptr))); + triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("leave far away", 4.0f), nullptr))); + triggers.push_back(new TriggerNode("seldom", NextAction::array(0, new NextAction("reset instances", 1.0f), nullptr))); } From e5b27910539d0ea741e9e2a4341de4aaed6e44ac Mon Sep 17 00:00:00 2001 From: HennyWilly <5954598+HennyWilly@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:29:07 +0100 Subject: [PATCH 12/20] Improve Molten Core Strategy (#1852) This is my first attempt of implementing playerbot strategies. A team of 40 can steamroll Molten Core relatively easy, even with lower item levels. Regardless, this PR adds resistance triggers and actions to mitigate some damage. Additionally, improvements were made for the encounters with Garr, Baron Geddon, Shazzrah and Golemagg. A short summary per boss is listed below. All planned features are included, but feedback is of course appreciated. ### Lucifron - Shadow resistance: mitigate damage from [Impending Doom](https://www.wowhead.com/classic/spell=19702/impending-doom) and [Shadow Shock](https://www.wowhead.com/classic/spell=19460/shadow-shock). ### Magmadar - Fire resistance: mitigate damage from [Lava Bomb](https://www.wowhead.com/classic/spell=19411/lava-bomb) and [Magma Spit](https://www.wowhead.com/classic/spell=19450/magma-spit). - Like King Dred and the fraction commander (Nexus), this fight might profit from an anti-fear strategy in order to counter [Panic](https://www.wowhead.com/classic/spell=19408/panic). Not implemented here. ### Gehennas - Shadow resistance: mitigate damage from [Shadow Bolt](https://www.wowhead.com/classic/spell=19728/shadow-bolt) and increase the chance to resist [Gehennas' Curse](https://www.wowhead.com/classic/spell=19716/gehennas-curse). ### Garr - Fire resistance: mitigate damage from the Firesworn adds ([Immolate](https://www.wowhead.com/classic/spell=20294/immolate) and [Eruption](https://www.wowhead.com/classic/spell=19497/eruption)). - Disabled dps aoe abilities via multiplier. This one is important because multiple exploding adds at once might delete bots rather quick... ### Baron Geddon - Refactored the existing strategy. - Fire resistance: mitigate damage from [Ignite Mana](https://www.wowhead.com/classic/spell=19659/ignite-mana), [Inferno](https://www.wowhead.com/classic/spell=19695/inferno) and [Living Bomb](https://www.wowhead.com/classic/spell=20475/living-bomb). - Better Inferno handling: Before moving away, bots stop attacking and interrupt their spells. Additionally, the new multiplier prevents bots from running back to Geddon while Inferno is still active. ### Shazzrah - Ranged bots now position themselves in a sweet spot that prevents them from getting hit with [Arcane Explosion](https://www.wowhead.com/classic/spell=19712/arcane-explosion) but still close enough to dps and heal. ### Sulfuron Harbinger - Fire resistance: mitigate damage from [Hand of Ragnaros](https://www.wowhead.com/classic/spell=19780/hand-of-ragnaros) and [Immolate](https://www.wowhead.com/classic/spell=20294/immolate). To be fair, this one is quite negligible... ### Golemagg - Fire resistance: mitigate damage from [Magma Splash](https://www.wowhead.com/classic/spell=13880/magma-splash) and [Pyroblast](https://www.wowhead.com/classic/spell=20228/pyroblast). - Disabled dps aoe abilities via multiplier. Kind of a preference on my side. Otherwise, the Core Ragers spam emotes about not wanting to die. ### Majordomo Executus - Shadow resistance: mitigate damage from [Aegis of Ragnaros](https://www.wowhead.com/classic/spell=20620/aegis-of-ragnaros), [Shadow Shock](https://www.wowhead.com/classic/spell=20603/shadow-shock) and [Shadow Bolt](https://www.wowhead.com/classic/spell=21077/shadow-bolt). This one is also negligible, TBF. ### Ragnaros - Fire resistance: mitigate damage from [Wrath of Ragnaros](https://www.wowhead.com/classic/spell=20566/wrath-of-ragnaros) and [Lava Burst](https://www.wowhead.com/classic/spell=21158/lava-burst). --- src/PlayerbotAI.cpp | 10 +- src/PlayerbotAI.h | 2 +- src/strategy/raids/RaidStrategyContext.h | 4 +- .../raids/moltencore/RaidMcActionContext.h | 32 ++- .../raids/moltencore/RaidMcActions.cpp | 214 ++++++++++++++++-- src/strategy/raids/moltencore/RaidMcActions.h | 49 +++- src/strategy/raids/moltencore/RaidMcHelpers.h | 22 ++ .../raids/moltencore/RaidMcMultipliers.cpp | 117 ++++++++++ .../raids/moltencore/RaidMcMultipliers.h | 27 +++ .../raids/moltencore/RaidMcStrategy.cpp | 70 +++++- .../raids/moltencore/RaidMcStrategy.h | 8 +- .../raids/moltencore/RaidMcTriggerContext.h | 30 ++- .../raids/moltencore/RaidMcTriggers.cpp | 36 ++- .../raids/moltencore/RaidMcTriggers.h | 28 +++ 14 files changed, 601 insertions(+), 48 deletions(-) create mode 100644 src/strategy/raids/moltencore/RaidMcHelpers.h create mode 100644 src/strategy/raids/moltencore/RaidMcMultipliers.cpp create mode 100644 src/strategy/raids/moltencore/RaidMcMultipliers.h diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 7975807f88..2ef6860a2a 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -1459,7 +1459,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "onyxia"; // Onyxia's Lair break; case 409: - strategyName = "mc"; // Molten Core + strategyName = "moltencore"; // Molten Core break; case 469: strategyName = "bwl"; // Blackwing Lair @@ -2247,7 +2247,7 @@ uint32 PlayerbotAI::GetGroupTankNum(Player* player) bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } -bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) +bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers) { Group* group = player->GetGroup(); if (!group) @@ -2264,6 +2264,9 @@ bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) continue; } + if (ignoreDeadPlayers && !member->IsAlive()) + continue; + if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) @@ -2283,6 +2286,9 @@ bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) continue; } + if (ignoreDeadPlayers && !member->IsAlive()) + continue; + if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index 7252cd7720..3b9eaab1ea 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -428,7 +428,7 @@ class PlayerbotAI : public PlayerbotAIBase static bool IsMainTank(Player* player); static uint32 GetGroupTankNum(Player* player); static bool IsAssistTank(Player* player); - static bool IsAssistTankOfIndex(Player* player, int index); + static bool IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); static bool IsHealAssistantOfIndex(Player* player, int index); static bool IsRangedDpsAssistantOfIndex(Player* player, int index); bool HasAggro(Unit* unit); diff --git a/src/strategy/raids/RaidStrategyContext.h b/src/strategy/raids/RaidStrategyContext.h index f16fa72546..ee2bac3270 100644 --- a/src/strategy/raids/RaidStrategyContext.h +++ b/src/strategy/raids/RaidStrategyContext.h @@ -22,7 +22,7 @@ class RaidStrategyContext : public NamedObjectContext RaidStrategyContext() : NamedObjectContext(false, true) { creators["aq20"] = &RaidStrategyContext::aq20; - creators["mc"] = &RaidStrategyContext::mc; + creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; creators["magtheridon"] = &RaidStrategyContext::magtheridon; @@ -38,7 +38,7 @@ class RaidStrategyContext : public NamedObjectContext private: static Strategy* aq20(PlayerbotAI* botAI) { return new RaidAq20Strategy(botAI); } - static Strategy* mc(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } + static Strategy* moltencore(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); } static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } diff --git a/src/strategy/raids/moltencore/RaidMcActionContext.h b/src/strategy/raids/moltencore/RaidMcActionContext.h index 45e6e056cc..79a4a95a85 100644 --- a/src/strategy/raids/moltencore/RaidMcActionContext.h +++ b/src/strategy/raids/moltencore/RaidMcActionContext.h @@ -10,13 +10,39 @@ class RaidMcActionContext : public NamedObjectContext public: RaidMcActionContext() { - creators["mc check should move from group"] = &RaidMcActionContext::check_should_move_from_group; + creators["mc lucifron shadow resistance"] = &RaidMcActionContext::lucifron_shadow_resistance; + creators["mc magmadar fire resistance"] = &RaidMcActionContext::magmadar_fire_resistance; + creators["mc gehennas shadow resistance"] = &RaidMcActionContext::gehennas_shadow_resistance; + creators["mc garr fire resistance"] = &RaidMcActionContext::garr_fire_resistance; + creators["mc baron geddon fire resistance"] = &RaidMcActionContext::baron_geddon_fire_resistance; + creators["mc move from group"] = &RaidMcActionContext::check_should_move_from_group; creators["mc move from baron geddon"] = &RaidMcActionContext::move_from_baron_geddon; + creators["mc shazzrah move away"] = &RaidMcActionContext::shazzrah_move_away; + creators["mc sulfuron harbinger fire resistance"] = &RaidMcActionContext::sulfuron_harbinger_fire_resistance; + creators["mc golemagg fire resistance"] = &RaidMcActionContext::golemagg_fire_resistance; + creators["mc golemagg mark boss"] = &RaidMcActionContext::golemagg_mark_boss; + creators["mc golemagg main tank attack golemagg"] = &RaidMcActionContext::golemagg_main_tank_attack_golemagg; + creators["mc golemagg assist tank attack core rager"] = &RaidMcActionContext::golemagg_assist_tank_attack_core_rager; + creators["mc majordomo shadow resistance"] = &RaidMcActionContext::majordomo_shadow_resistance; + creators["mc ragnaros fire resistance"] = &RaidMcActionContext::ragnaros_fire_resistance; } private: - static Action* check_should_move_from_group(PlayerbotAI* ai) { return new McCheckShouldMoveFromGroupAction(ai); } - static Action* move_from_baron_geddon(PlayerbotAI* ai) { return new McMoveFromBaronGeddonAction(ai); } + static Action* lucifron_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "lucifron"); } + static Action* magmadar_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "magmadar"); } + static Action* gehennas_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "gehennas"); } + static Action* garr_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "garr"); } + static Action* baron_geddon_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "baron geddon"); } + static Action* check_should_move_from_group(PlayerbotAI* botAI) { return new McMoveFromGroupAction(botAI); } + static Action* move_from_baron_geddon(PlayerbotAI* botAI) { return new McMoveFromBaronGeddonAction(botAI); } + static Action* shazzrah_move_away(PlayerbotAI* botAI) { return new McShazzrahMoveAwayAction(botAI); } + static Action* sulfuron_harbinger_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "sulfuron harbinger"); } + static Action* golemagg_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "golemagg the incinerator"); } + static Action* golemagg_mark_boss(PlayerbotAI* botAI) { return new McGolemaggMarkBossAction(botAI); } + static Action* golemagg_main_tank_attack_golemagg(PlayerbotAI* botAI) { return new McGolemaggMainTankAttackGolemaggAction(botAI); } + static Action* golemagg_assist_tank_attack_core_rager(PlayerbotAI* botAI) { return new McGolemaggAssistTankAttackCoreRagerAction(botAI); } + static Action* majordomo_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "majordomo executus"); } + static Action* ragnaros_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "ragnaros"); } }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcActions.cpp b/src/strategy/raids/moltencore/RaidMcActions.cpp index b82a073c40..b18c8b8534 100644 --- a/src/strategy/raids/moltencore/RaidMcActions.cpp +++ b/src/strategy/raids/moltencore/RaidMcActions.cpp @@ -1,43 +1,215 @@ #include "RaidMcActions.h" #include "Playerbots.h" +#include "RtiTargetValue.h" +#include "RaidMcTriggers.h" +#include "RaidMcHelpers.h" -bool McCheckShouldMoveFromGroupAction::Execute(Event event) +static constexpr float LIVING_BOMB_DISTANCE = 20.0f; +static constexpr float INFERNO_DISTANCE = 20.0f; + +// don't get hit by Arcane Explosion but still be in casting range +static constexpr float ARCANE_EXPLOSION_DISTANCE = 26.0f; + +// dedicated tank positions; prevents assist tanks from positioning Core Ragers on steep walls on pull +static const Position GOLEMAGG_TANK_POSITION{795.7308, -994.8848, -207.18661}; +static const Position CORE_RAGER_TANK_POSITION{846.6453, -1019.0639, -198.9819}; + +static constexpr float GOLEMAGGS_TRUST_DISTANCE = 30.0f; +static constexpr float CORE_RAGER_STEP_DISTANCE = 5.0f; + +using namespace MoltenCoreHelpers; + +bool McMoveFromGroupAction::Execute(Event event) +{ + return MoveFromGroup(LIVING_BOMB_DISTANCE); +} + +bool McMoveFromBaronGeddonAction::Execute(Event event) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + { + float distToTravel = INFERNO_DISTANCE - bot->GetDistance2d(boss); + if (distToTravel > 0) + { + // Stop current spell first + bot->AttackStop(); + bot->InterruptNonMeleeSpells(false); + + return MoveAway(boss, distToTravel); + } + } + return false; +} + +bool McShazzrahMoveAwayAction::Execute(Event event) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "shazzrah")) + { + float distToTravel = ARCANE_EXPLOSION_DISTANCE - bot->GetDistance2d(boss); + if (distToTravel > 0) + return MoveAway(boss, distToTravel); + } + return false; +} + +bool McGolemaggMarkBossAction::Execute(Event event) { - if (bot->HasAura(20475)) // barron geddon's living bomb + if (Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) { - if (!botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT)) + if (Group* group = bot->GetGroup()) { - // add/remove from both for now as it will make it more obvious to - // player if this strat remains on after fight somehow - botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT); - return true; + ObjectGuid currentSkullGuid = group->GetTargetIcon(RtiTargetValue::skullIndex); + if (currentSkullGuid.IsEmpty() || currentSkullGuid != boss->GetGUID()) + { + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + return true; + } } } - else if (botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT)) + return false; +} + +bool McGolemaggTankAction::MoveUnitToPosition(Unit* target, const Position& tankPosition, float maxDistance, + float stepDistance) +{ + if (bot->GetVictim() != target) + return Attack(target); + if (target->GetVictim() == bot) { - // add/remove from both for now as it will make it more obvious to - // player if this strat remains on after fight somehow - botAI->ChangeStrategy("-move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("-move from group", BOT_STATE_COMBAT); + float distanceToTankPosition = bot->GetExactDist2d(tankPosition.GetPositionX(), tankPosition.GetPositionY()); + if (distanceToTankPosition > maxDistance) + { + float dX = tankPosition.GetPositionX() - bot->GetPositionX(); + float dY = tankPosition.GetPositionY() - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + float moveX = bot->GetPositionX() + (dX / dist) * stepDistance; + float moveY = bot->GetPositionY() + (dY / dist) * stepDistance; + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, + true); + } + } + else if (botAI->DoSpecificAction("taunt spell", Event(), true)) return true; + return false; +} + +bool McGolemaggTankAction::FindCoreRagers(Unit*& coreRager1, Unit*& coreRager2) const +{ + coreRager1 = coreRager2 = nullptr; + for (auto const& target : AI_VALUE(GuidVector, "possible targets no los")) + { + Unit* unit = botAI->GetUnit(target); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CORE_RAGER) + { + if (coreRager1 == nullptr) + coreRager1 = unit; + else if (coreRager2 == nullptr) + { + coreRager2 = unit; + break; // There should be no third Core Rager. + } + } + } + return coreRager1 != nullptr && coreRager2 != nullptr; +} + +bool McGolemaggMainTankAttackGolemaggAction::Execute(Event event) +{ + // At this point, we know we are not the last living tank in the group. + if (Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) + { + Unit* coreRager1; + Unit* coreRager2; + if (!FindCoreRagers(coreRager1, coreRager2)) + return false; // safety check + + // We only need to move if the Core Ragers still have Golemagg's Trust + if (coreRager1->HasAura(SPELL_GOLEMAGGS_TRUST) || coreRager2->HasAura(SPELL_GOLEMAGGS_TRUST)) + return MoveUnitToPosition(boss, GOLEMAGG_TANK_POSITION, boss->GetCombatReach()); } return false; } -bool McMoveFromBaronGeddonAction::Execute(Event event) +bool McGolemaggAssistTankAttackCoreRagerAction::Execute(Event event) { - const float radius = 25.0f; // more than should be needed but bots keep trying to run back in - if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator"); + if (!boss) + return false; + + // Step 0: Filter additional assist tanks. We only need 2. + bool isFirstAssistTank = PlayerbotAI::IsAssistTankOfIndex(bot, 0, true); + bool isSecondAssistTank = PlayerbotAI::IsAssistTankOfIndex(bot, 1, true); + if (!isFirstAssistTank && !isSecondAssistTank) + return Attack(boss); + + // Step 1: Find both Core Ragers + Unit* coreRager1; + Unit* coreRager2; + if (!FindCoreRagers(coreRager1, coreRager2)) + return false; // safety check + + // Step 2: Assign Core Rager to bot + Unit* myCoreRager = nullptr; + Unit* otherCoreRager = nullptr; + if (isFirstAssistTank) { - long distToTravel = radius - bot->GetDistance(boss); - if (distToTravel > 0) + myCoreRager = coreRager1; + otherCoreRager = coreRager2; + } + else // isSecondAssistTank is always true here + { + myCoreRager = coreRager2; + otherCoreRager = coreRager1; + } + + // Step 3: Select the right target + if (myCoreRager->GetVictim() != bot) + { + // Step 3.1: My Core Rager isn't attacking me. Attack until it does. + if (bot->GetVictim() != myCoreRager) + return Attack(myCoreRager); + return botAI->DoSpecificAction("taunt spell", event, true); + } + + Unit* otherCoreRagerVictim = otherCoreRager->GetVictim(); + if (otherCoreRagerVictim) // Core Rager victim can be NULL + { + // Step 3.2: Check if the other Core Rager isn't attacking its assist tank. + Player* otherCoreRagerPlayerVictim = otherCoreRagerVictim->ToPlayer(); + if (otherCoreRagerPlayerVictim && + !PlayerbotAI::IsAssistTankOfIndex(otherCoreRagerPlayerVictim, 0, true) && + !PlayerbotAI::IsAssistTankOfIndex(otherCoreRagerPlayerVictim, 1, true)) { - // float angle = bot->GetAngle(boss) + M_PI; - // return Move(angle, distToTravel); - return MoveAway(boss, distToTravel); + // Assume we are the only assist tank or the other assist tank is dead => pick up other Core Rager! + if (bot->GetVictim() != otherCoreRager) + return Attack(otherCoreRager); + return botAI->DoSpecificAction("taunt spell", event, true); } } + + if (bot->GetVictim() != myCoreRager) + return Attack(myCoreRager); // Step 3.3: Attack our Core Rager in case we previously switched in 3.2. + + // Step 4: Prevent Golemagg's Trust on Core Ragers + if (myCoreRager->HasAura(SPELL_GOLEMAGGS_TRUST) || + (otherCoreRagerVictim == bot && otherCoreRager->HasAura(SPELL_GOLEMAGGS_TRUST))) + { + // Step 4.1: Move Core Ragers to dedicated tank position (only if Golemagg is far enough away from said position) + float bossDistanceToCoreRagerTankPosition = boss->GetExactDist2d( + CORE_RAGER_TANK_POSITION.GetPositionX(), CORE_RAGER_TANK_POSITION.GetPositionY()); + if (bossDistanceToCoreRagerTankPosition > GOLEMAGGS_TRUST_DISTANCE) + { + float distanceToTankPosition = bot->GetExactDist2d(CORE_RAGER_TANK_POSITION.GetPositionX(), + CORE_RAGER_TANK_POSITION.GetPositionY()); + if (distanceToTankPosition > CORE_RAGER_STEP_DISTANCE) + return MoveUnitToPosition(myCoreRager, CORE_RAGER_TANK_POSITION, CORE_RAGER_STEP_DISTANCE); + } + + // Step 4.2: if boss is too close to tank position, or we are already there, move away from Golemagg to try to out-range Golemagg's Trust + return MoveAway(boss, CORE_RAGER_STEP_DISTANCE, true); + } + return false; } diff --git a/src/strategy/raids/moltencore/RaidMcActions.h b/src/strategy/raids/moltencore/RaidMcActions.h index 6ff18513ff..680b311d3c 100644 --- a/src/strategy/raids/moltencore/RaidMcActions.h +++ b/src/strategy/raids/moltencore/RaidMcActions.h @@ -1,15 +1,16 @@ #ifndef _PLAYERBOT_RAIDMCACTIONS_H #define _PLAYERBOT_RAIDMCACTIONS_H +#include "AttackAction.h" #include "MovementActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -class McCheckShouldMoveFromGroupAction : public Action +class McMoveFromGroupAction : public MovementAction { public: - McCheckShouldMoveFromGroupAction(PlayerbotAI* botAI, std::string const name = "mc check should move from group") - : Action(botAI, name) {} + McMoveFromGroupAction(PlayerbotAI* botAI, std::string const name = "mc move from group") + : MovementAction(botAI, name) {} bool Execute(Event event) override; }; @@ -21,4 +22,46 @@ class McMoveFromBaronGeddonAction : public MovementAction bool Execute(Event event) override; }; +class McShazzrahMoveAwayAction : public MovementAction +{ +public: + McShazzrahMoveAwayAction(PlayerbotAI* botAI, std::string const name = "mc shazzrah move away") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class McGolemaggMarkBossAction : public Action +{ +public: + McGolemaggMarkBossAction(PlayerbotAI* botAI, std::string const name = "mc golemagg mark boss") + : Action(botAI, name) {}; + bool Execute(Event event) override; +}; + +class McGolemaggTankAction : public AttackAction +{ +public: + McGolemaggTankAction(PlayerbotAI* botAI, std::string const name) + : AttackAction(botAI, name) {} +protected: + bool MoveUnitToPosition(Unit* target, const Position& tankPosition, float maxDistance, float stepDistance = 3.0f); + bool FindCoreRagers(Unit*& coreRager1, Unit*& coreRager2) const; +}; + +class McGolemaggMainTankAttackGolemaggAction : public McGolemaggTankAction +{ +public: + McGolemaggMainTankAttackGolemaggAction(PlayerbotAI* botAI, std::string const name = "mc golemagg main tank attack golemagg") + : McGolemaggTankAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class McGolemaggAssistTankAttackCoreRagerAction : public McGolemaggTankAction +{ +public: + McGolemaggAssistTankAttackCoreRagerAction(PlayerbotAI* botAI, std::string const name = "mc golemagg assist tank attack core rager") + : McGolemaggTankAction(botAI, name) {}; + bool Execute(Event event) override; +}; + #endif diff --git a/src/strategy/raids/moltencore/RaidMcHelpers.h b/src/strategy/raids/moltencore/RaidMcHelpers.h new file mode 100644 index 0000000000..5dc821246d --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcHelpers.h @@ -0,0 +1,22 @@ +#ifndef _PLAYERBOT_RAIDMCHELPERS_H +#define _PLAYERBOT_RAIDMCHELPERS_H + +namespace MoltenCoreHelpers +{ +enum MoltenCoreNPCs +{ + // Golemagg + NPC_CORE_RAGER = 11672, +}; +enum MoltenCoreSpells +{ + // Baron Geddon + SPELL_INFERNO = 19695, + SPELL_LIVING_BOMB = 20475, + + // Golemagg + SPELL_GOLEMAGGS_TRUST = 20553, +}; +} + +#endif diff --git a/src/strategy/raids/moltencore/RaidMcMultipliers.cpp b/src/strategy/raids/moltencore/RaidMcMultipliers.cpp new file mode 100644 index 0000000000..d1ee936b0d --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcMultipliers.cpp @@ -0,0 +1,117 @@ +#include "RaidMcMultipliers.h" + +#include "Playerbots.h" +#include "ChooseTargetActions.h" +#include "GenericSpellActions.h" +#include "DruidActions.h" +#include "HunterActions.h" +#include "PaladinActions.h" +#include "ShamanActions.h" +#include "WarriorActions.h" +#include "DKActions.h" +#include "RaidMcActions.h" +#include "RaidMcHelpers.h" + +using namespace MoltenCoreHelpers; + +static bool IsDpsBotWithAoeAction(Player* bot, Action* action) +{ + if (PlayerbotAI::IsDps(bot)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return true; + + if (auto castSpellAction = dynamic_cast(action)) + { + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return true; + } + } + return false; +} + +float GarrDisableDpsAoeMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "garr")) + { + if (IsDpsBotWithAoeAction(bot, action)) + return 0.0f; + } + return 1.0f; +} + +static bool IsAllowedGeddonMovementAction(Action* action) +{ + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return false; + + if (dynamic_cast(action)) + return false; + + return true; +} + +float BaronGeddonAbilityMultiplier::GetValue(Action* action) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + { + if (boss->HasAura(SPELL_INFERNO)) + { + if (!IsAllowedGeddonMovementAction(action)) + return 0.0f; + } + } + + // No check for Baron Geddon, because bots may have the bomb even after Geddon died. + if (bot->HasAura(SPELL_LIVING_BOMB)) + { + if (!IsAllowedGeddonMovementAction(action)) + return 0.0f; + } + + return 1.0f; +} + +static bool IsSingleLivingTankInGroup(Player* bot) +{ + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + if (PlayerbotAI::IsTank(member)) + return false; + } + } + return true; +} + +float GolemaggMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) + { + if (PlayerbotAI::IsTank(bot) && IsSingleLivingTankInGroup(bot)) + { + // Only one tank => Pick up Golemagg and the two Core Ragers + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + if (PlayerbotAI::IsAssistTank(bot)) + { + // The first two assist tanks manage the Core Ragers. The remaining assist tanks attack the boss. + if (dynamic_cast(action)) + return 0.0f; + } + if (IsDpsBotWithAoeAction(bot, action)) + return 0.0f; + } + return 1.0f; +} diff --git a/src/strategy/raids/moltencore/RaidMcMultipliers.h b/src/strategy/raids/moltencore/RaidMcMultipliers.h new file mode 100644 index 0000000000..56dc7a5f80 --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcMultipliers.h @@ -0,0 +1,27 @@ +#ifndef _PLAYERBOT_RAIDMCMULTIPLIERS_H +#define _PLAYERBOT_RAIDMCMULTIPLIERS_H + +#include "Multiplier.h" + +class GarrDisableDpsAoeMultiplier : public Multiplier +{ +public: + GarrDisableDpsAoeMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "garr disable dps aoe multiplier") {} + float GetValue(Action* action) override; +}; + +class BaronGeddonAbilityMultiplier : public Multiplier +{ +public: + BaronGeddonAbilityMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "baron geddon ability multiplier") {} + float GetValue(Action* action) override; +}; + +class GolemaggMultiplier : public Multiplier +{ +public: + GolemaggMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "golemagg multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/strategy/raids/moltencore/RaidMcStrategy.cpp b/src/strategy/raids/moltencore/RaidMcStrategy.cpp index 67793de906..c6a5042eb5 100644 --- a/src/strategy/raids/moltencore/RaidMcStrategy.cpp +++ b/src/strategy/raids/moltencore/RaidMcStrategy.cpp @@ -1,13 +1,81 @@ #include "RaidMcStrategy.h" +#include "RaidMcMultipliers.h" #include "Strategy.h" void RaidMcStrategy::InitTriggers(std::vector& triggers) { + // Lucifron + triggers.push_back( + new TriggerNode("mc lucifron shadow resistance", + NextAction::array(0, new NextAction("mc lucifron shadow resistance", ACTION_RAID), nullptr))); + + // Magmadar + // TODO: Fear ward / tremor totem, or general anti-fear strat development. Same as King Dred (Drak'Tharon) and faction commander (Nexus). + triggers.push_back( + new TriggerNode("mc magmadar fire resistance", + NextAction::array(0, new NextAction("mc magmadar fire resistance", ACTION_RAID), nullptr))); + + // Gehennas + triggers.push_back( + new TriggerNode("mc gehennas shadow resistance", + NextAction::array(0, new NextAction("mc gehennas shadow resistance", ACTION_RAID), nullptr))); + + // Garr + triggers.push_back( + new TriggerNode("mc garr fire resistance", + NextAction::array(0, new NextAction("mc garr fire resistance", ACTION_RAID), nullptr))); + + // Baron Geddon + triggers.push_back( + new TriggerNode("mc baron geddon fire resistance", + NextAction::array(0, new NextAction("mc baron geddon fire resistance", ACTION_RAID), nullptr))); triggers.push_back( new TriggerNode("mc living bomb debuff", - NextAction::array(0, new NextAction("mc check should move from group", ACTION_RAID), nullptr))); + NextAction::array(0, new NextAction("mc move from group", ACTION_RAID), nullptr))); triggers.push_back( new TriggerNode("mc baron geddon inferno", NextAction::array(0, new NextAction("mc move from baron geddon", ACTION_RAID), nullptr))); + + // Shazzrah + triggers.push_back( + new TriggerNode("mc shazzrah ranged", + NextAction::array(0, new NextAction("mc shazzrah move away", ACTION_RAID), nullptr))); + + // Sulfuron Harbinger + // Alternatively, shadow resistance is also possible. + triggers.push_back( + new TriggerNode("mc sulfuron harbinger fire resistance", + NextAction::array(0, new NextAction("mc sulfuron harbinger fire resistance", ACTION_RAID), nullptr))); + + // Golemagg the Incinerator + triggers.push_back( + new TriggerNode("mc golemagg fire resistance", + NextAction::array(0, new NextAction("mc golemagg fire resistance", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg mark boss", + NextAction::array(0, new NextAction("mc golemagg mark boss", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg is main tank", + NextAction::array(0, new NextAction("mc golemagg main tank attack golemagg", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg is assist tank", + NextAction::array(0, new NextAction("mc golemagg assist tank attack core rager", ACTION_RAID), nullptr))); + + // Majordomo Executus + triggers.push_back( + new TriggerNode("mc majordomo shadow resistance", + NextAction::array(0, new NextAction("mc majordomo shadow resistance", ACTION_RAID), nullptr))); + + // Ragnaros + triggers.push_back( + new TriggerNode("mc ragnaros fire resistance", + NextAction::array(0, new NextAction("mc ragnaros fire resistance", ACTION_RAID), nullptr))); +} + +void RaidMcStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new GarrDisableDpsAoeMultiplier(botAI)); + multipliers.push_back(new BaronGeddonAbilityMultiplier(botAI)); + multipliers.push_back(new GolemaggMultiplier(botAI)); } diff --git a/src/strategy/raids/moltencore/RaidMcStrategy.h b/src/strategy/raids/moltencore/RaidMcStrategy.h index 82b7d8f25e..45b503e933 100644 --- a/src/strategy/raids/moltencore/RaidMcStrategy.h +++ b/src/strategy/raids/moltencore/RaidMcStrategy.h @@ -8,10 +8,10 @@ class RaidMcStrategy : public Strategy { public: - RaidMcStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "mc"; } - virtual void InitTriggers(std::vector& triggers) override; - // virtual void InitMultipliers(std::vector &multipliers) override; + RaidMcStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + std::string const getName() override { return "moltencore"; } + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector &multipliers) override; }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcTriggerContext.h b/src/strategy/raids/moltencore/RaidMcTriggerContext.h index 93fe6df0f8..b74958919c 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggerContext.h +++ b/src/strategy/raids/moltencore/RaidMcTriggerContext.h @@ -10,13 +10,39 @@ class RaidMcTriggerContext : public NamedObjectContext public: RaidMcTriggerContext() { + creators["mc lucifron shadow resistance"] = &RaidMcTriggerContext::lucifron_shadow_resistance; + creators["mc magmadar fire resistance"] = &RaidMcTriggerContext::magmadar_fire_resistance; + creators["mc gehennas shadow resistance"] = &RaidMcTriggerContext::gehennas_shadow_resistance; + creators["mc garr fire resistance"] = &RaidMcTriggerContext::garr_fire_resistance; + creators["mc baron geddon fire resistance"] = &RaidMcTriggerContext::baron_geddon_fire_resistance; creators["mc living bomb debuff"] = &RaidMcTriggerContext::living_bomb_debuff; creators["mc baron geddon inferno"] = &RaidMcTriggerContext::baron_geddon_inferno; + creators["mc shazzrah ranged"] = &RaidMcTriggerContext::shazzrah_ranged; + creators["mc sulfuron harbinger fire resistance"] = &RaidMcTriggerContext::sulfuron_harbinger_fire_resistance; + creators["mc golemagg fire resistance"] = &RaidMcTriggerContext::golemagg_fire_resistance; + creators["mc golemagg mark boss"] = &RaidMcTriggerContext::golemagg_mark_boss; + creators["mc golemagg is main tank"] = &RaidMcTriggerContext::golemagg_is_main_tank; + creators["mc golemagg is assist tank"] = &RaidMcTriggerContext::golemagg_is_assist_tank; + creators["mc majordomo shadow resistance"] = &RaidMcTriggerContext::majordomo_shadow_resistance; + creators["mc ragnaros fire resistance"] = &RaidMcTriggerContext::ragnaros_fire_resistance; } private: - static Trigger* living_bomb_debuff(PlayerbotAI* ai) { return new McLivingBombDebuffTrigger(ai); } - static Trigger* baron_geddon_inferno(PlayerbotAI* ai) { return new McBaronGeddonInfernoTrigger(ai); } + static Trigger* lucifron_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "lucifron"); } + static Trigger* magmadar_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "magmadar"); } + static Trigger* gehennas_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "gehennas"); } + static Trigger* garr_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "garr"); } + static Trigger* baron_geddon_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "baron geddon"); } + static Trigger* living_bomb_debuff(PlayerbotAI* botAI) { return new McLivingBombDebuffTrigger(botAI); } + static Trigger* baron_geddon_inferno(PlayerbotAI* botAI) { return new McBaronGeddonInfernoTrigger(botAI); } + static Trigger* shazzrah_ranged(PlayerbotAI* botAI) { return new McShazzrahRangedTrigger(botAI); } + static Trigger* sulfuron_harbinger_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "sulfuron harbinger"); } + static Trigger* golemagg_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "golemagg the incinerator"); } + static Trigger* golemagg_mark_boss(PlayerbotAI* botAI) { return new McGolemaggMarkBossTrigger(botAI); } + static Trigger* golemagg_is_main_tank(PlayerbotAI* botAI) { return new McGolemaggIsMainTankTrigger(botAI); } + static Trigger* golemagg_is_assist_tank(PlayerbotAI* botAI) { return new McGolemaggIsAssistTankTrigger(botAI); } + static Trigger* majordomo_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "majordomo executus"); } + static Trigger* ragnaros_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "ragnaros"); } }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcTriggers.cpp b/src/strategy/raids/moltencore/RaidMcTriggers.cpp index f77b70f20e..834d703d33 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggers.cpp +++ b/src/strategy/raids/moltencore/RaidMcTriggers.cpp @@ -1,22 +1,40 @@ #include "RaidMcTriggers.h" #include "SharedDefines.h" +#include "RaidMcHelpers.h" + +using namespace MoltenCoreHelpers; bool McLivingBombDebuffTrigger::IsActive() { - // if bot has barron geddon's living bomb, we need to add strat, otherwise we need to remove - // only do when fighting baron geddon (to avoid modifying strat set by player outside this fight) - if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) - { - if (boss->IsInCombat()) - return bot->HasAura(20475) != botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT); - } - return false; + // No check for Baron Geddon, because bots may have the bomb even after Geddon died. + return bot->HasAura(SPELL_LIVING_BOMB); } bool McBaronGeddonInfernoTrigger::IsActive() { if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) - return boss->HasAura(19695); + return boss->HasAura(SPELL_INFERNO); return false; } + +bool McShazzrahRangedTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "shazzrah") && PlayerbotAI::IsRanged(bot); +} + +bool McGolemaggMarkBossTrigger::IsActive() +{ + // any tank may mark the boss + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsTank(bot); +} + +bool McGolemaggIsMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsMainTank(bot); +} + +bool McGolemaggIsAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsAssistTank(bot); +} diff --git a/src/strategy/raids/moltencore/RaidMcTriggers.h b/src/strategy/raids/moltencore/RaidMcTriggers.h index 9d2fb985a2..4cd84a2df2 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggers.h +++ b/src/strategy/raids/moltencore/RaidMcTriggers.h @@ -19,4 +19,32 @@ class McBaronGeddonInfernoTrigger : public Trigger bool IsActive() override; }; +class McShazzrahRangedTrigger : public Trigger +{ +public: + McShazzrahRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc shazzrah ranged") {} + bool IsActive() override; +}; + +class McGolemaggMarkBossTrigger : public Trigger +{ +public: + McGolemaggMarkBossTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg mark boss") {} + bool IsActive() override; +}; + +class McGolemaggIsMainTankTrigger : public Trigger +{ +public: + McGolemaggIsMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg is main tank") {} + bool IsActive() override; +}; + +class McGolemaggIsAssistTankTrigger : public Trigger +{ +public: + McGolemaggIsAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg is assist tank") {} + bool IsActive() override; +}; + #endif From dde16674c3725b2be1ee393a2d6cfb0481dee962 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Mon, 8 Dec 2025 04:34:16 -0700 Subject: [PATCH 13/20] Fix: Stop pets from fighting in PVP prohibited zones (#1829) Stripped down version of #1818. No new features. Refactors IsPossibleTarget in AttackersValue.cpp to a better style and makes sure pets don't attack in prohibited zones. Testing: Confirmed that aggro pets no longer attack in PVP prohibited areas, but still do outside them. Zim'Torga in Zul'Drak is a good example to test this (ID 4323). Lookout for death knights with a Risen Ally (uncontrolled and naturally aggro) now they respect PVP prohibition like their master. Note: If you manually teleport a bot that is in mid combat to a PVP prohibited area, its aggro pet might still attack, because its master is still in combat strategy. Otherwise the pet will not attack if its master has switched to non-combat. --- ...11_25_00_ai_playerbot_pet_action_texts.sql | 255 ++++++++++++++++++ src/strategy/actions/AttackAction.cpp | 7 +- src/strategy/actions/PetsAction.cpp | 97 +++++-- src/strategy/values/AttackersValue.cpp | 137 +++++++--- 4 files changed, 427 insertions(+), 69 deletions(-) create mode 100644 data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql diff --git a/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql b/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql new file mode 100644 index 0000000000..b7c6c96826 --- /dev/null +++ b/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql @@ -0,0 +1,255 @@ +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'pet_usage_error', + 'pet_no_pet_error', + 'pet_stance_report', + 'pet_no_target_error', + 'pet_target_dead_error', + 'pet_invalid_target_error', + 'pet_pvp_prohibited_error', + 'pet_attack_success', + 'pet_attack_failed', + 'pet_follow_success', + 'pet_stay_success', + 'pet_unknown_command_error', + 'pet_stance_set_success', + 'pet_type_pet', + 'pet_type_guardian', + 'pet_stance_aggressive', + 'pet_stance_defensive', + 'pet_stance_passive', + 'pet_stance_unknown' +); + +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'pet_usage_error', + 'pet_no_pet_error', + 'pet_stance_report', + 'pet_no_target_error', + 'pet_target_dead_error', + 'pet_invalid_target_error', + 'pet_pvp_prohibited_error', + 'pet_attack_success', + 'pet_attack_failed', + 'pet_follow_success', + 'pet_stay_success', + 'pet_unknown_command_error', + 'pet_stance_set_success', + 'pet_type_pet', + 'pet_type_guardian', + 'pet_stance_aggressive', + 'pet_stance_defensive', + 'pet_stance_passive', + 'pet_stance_unknown' +); + +INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES +(1717, 'pet_usage_error', "Usage: pet ", 0, 0, +"사용법: pet ", +"Utilisation: pet ", +"Verwendung: pet ", +"用法: pet ", +"用法: pet ", +"Uso: pet ", +"Uso: pet ", +"Использование: pet "), + +(1718, 'pet_no_pet_error', "You have no pet or guardian pet.", 0, 0, +"펫이나 수호자 펫이 없습니다.", +"Vous n'avez pas de familier ou gardien.", +"Du hast kein Tier oder Wächter.", +"你没有宠物或守护者宠物。", +"你沒有寵物或守護者寵物。", +"No tienes mascota o mascota guardián.", +"No tienes mascota o mascota guardián.", +"У вас нет питомца или защитника."), + +(1719, 'pet_stance_report', "Current stance of %type \"%name\": %stance.", 0, 0, +"%type \"%name\"의 현재 태세: %stance.", +"Position actuelle du %type \"%name\": %stance.", +"Aktuelle Haltung des %type \"%name\": %stance.", +"%type \"%name\" 的当前姿态: %stance。", +"%type \"%name\" 的當前姿態: %stance。", +"Postura actual del %type \"%name\": %stance.", +"Postura actual del %type \"%name\": %stance.", +"Текущая позиция %type \"%name\": %stance."), + +(1720, 'pet_no_target_error', "No valid target selected by master.", 0, 0, +"주인이 유효한 대상을 선택하지 않았습니다.", +"Aucune cible valide sélectionnée par le maître.", +"Kein gültiges Ziel vom Meister ausgewählt.", +"主人未选择有效目标。", +"主人未選擇有效目標。", +"No hay objetivo válido seleccionado por el maestro.", +"No hay objetivo válido seleccionado por el maestro.", +"Хозяин не выбрал действительную цель."), + +(1721, 'pet_target_dead_error', "Target is not alive.", 0, 0, +"대상이 살아있지 않습니다.", +"La cible n'est pas vivante.", +"Das Ziel ist nicht am Leben.", +"目标未存活。", +"目標未存活。", +"El objetivo no está vivo.", +"El objetivo no está vivo.", +"Цель не жива."), + +(1722, 'pet_invalid_target_error', "Target is not a valid attack target for the bot.", 0, 0, +"대상이 봇에게 유효한 공격 대상이 아닙니다.", +"La cible n'est pas une cible d'attaque valide pour le bot.", +"Das Ziel ist kein gültiges Angriffsziel für den Bot.", +"目标不是机器人的有效攻击目标。", +"目標不是機器人的有效攻擊目標。", +"El objetivo no es un objetivo de ataque válido para el bot.", +"El objetivo no es un objetivo de ataque válido para el bot.", +"Цель не является допустимой целью атаки для бота."), + +(1723, 'pet_pvp_prohibited_error', "I cannot command my pet to attack players in PvP prohibited areas.", 0, 0, +"PvP 금지 지역에서는 펫에게 플레이어 공격 명령을 내릴 수 없습니다.", +"Je ne peux pas commander à mon familier d'attaquer des joueurs dans les zones où le PvP est interdit.", +"Ich kann meinem Tier nicht befehlen, Spieler in PvP-verbotenen Gebieten anzugreifen.", +"我不能命令我的宠物在禁止PvP的区域攻击玩家。", +"我不能命令我的寵物在禁止PvP的區域攻擊玩家。", +"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.", +"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.", +"Я не могу приказать своему питомцу атаковать игроков в зонах, где PvP запрещено."), + +(1724, 'pet_attack_success', "Pet commanded to attack your target.", 0, 0, +"펫이 당신의 대상을 공격하도록 명령했습니다.", +"Le familier a reçu l'ordre d'attaquer votre cible.", +"Tier wurde befohlen, dein Ziel anzugreifen.", +"宠物已命令攻击你的目标。", +"寵物已命令攻擊你的目標。", +"Mascota ordenada a atacar tu objetivo.", +"Mascota ordenada a atacar tu objetivo.", +"Питомцу приказано атаковать вашу цель."), + +(1725, 'pet_attack_failed', "Pet did not attack. (Already attacking or unable to attack target)", 0, 0, +"펫이 공격하지 않았습니다. (이미 공격 중이거나 대상 공격 불가)", +"Le familier n'a pas attaqué. (Attaque déjà en cours ou impossible d'attaquer la cible)", +"Tier hat nicht angegriffen. (Greift bereits an oder kann Ziel nicht angreifen)", +"宠物未攻击。(已在攻击或无法攻击目标)", +"寵物未攻擊。(已在攻擊或無法攻擊目標)", +"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)", +"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)", +"Питомец не атаковал. (Уже атакует или не может атаковать цель)"), + +(1726, 'pet_follow_success', "Pet commanded to follow.", 0, 0, +"펫이 따라오도록 명령했습니다.", +"Le familier a reçu l'ordre de suivre.", +"Tier wurde befohlen zu folgen.", +"宠物已命令跟随。", +"寵物已命令跟隨。", +"Mascota ordenada a seguir.", +"Mascota ordenada a seguir.", +"Питомцу приказано следовать."), + +(1727, 'pet_stay_success', "Pet commanded to stay.", 0, 0, +"펫이 머물도록 명령했습니다.", +"Le familier a reçu l'ordre de rester.", +"Tier wurde befohlen zu bleiben.", +"宠物已命令停留。", +"寵物已命令停留。", +"Mascota ordenada a quedarse.", +"Mascota ordenada a quedarse.", +"Питомцу приказано остаться."), + +(1728, 'pet_unknown_command_error', "Unknown pet command: %param. Use: pet ", 0, 0, +"알 수 없는 펫 명령: %param. 사용법: pet ", +"Commande de familier inconnue: %param. Utilisation: pet ", +"Unbekannter Tierbefehl: %param. Verwendung: pet ", +"未知宠物命令: %param。用法: pet ", +"未知寵物命令: %param。用法: pet ", +"Comando de mascota desconocido: %param. Uso: pet ", +"Comando de mascota desconocido: %param. Uso: pet ", +"Неизвестная команда питомца: %param. Использование: pet "), + +(1729, 'pet_stance_set_success', "Pet stance set to %stance.", 0, 0, +"펫 태세가 %stance(으)로 설정되었습니다.", +"Position du familier définie sur %stance.", +"Tierhaltung auf %stance gesetzt.", +"宠物姿态设置为 %stance。", +"寵物姿態設置為 %stance。", +"Postura de mascota establecida en %stance.", +"Postura de mascota establecida en %stance.", +"Позиция питомца установлена на %stance."), + +(1730, 'pet_type_pet', "pet", 0, 0, +"펫", +"familier", +"Tier", +"宠物", +"寵物", +"mascota", +"mascota", +"питомец"), + +(1731, 'pet_type_guardian', "guardian", 0, 0, +"수호자", +"gardien", +"Wächter", +"守护者", +"守護者", +"guardián", +"guardián", +"защитник"), + +(1732, 'pet_stance_aggressive', "aggressive", 0, 0, +"공격적", +"agressif", +"aggressiv", +"进攻", +"進攻", +"agresivo", +"agresivo", +"агрессивная"), + +(1733, 'pet_stance_defensive', "defensive", 0, 0, +"방어적", +"défensif", +"defensiv", +"防御", +"防禦", +"defensivo", +"defensivo", +"защитная"), + +(1734, 'pet_stance_passive', "passive", 0, 0, +"수동적", +"passif", +"passiv", +"被动", +"被動", +"pasivo", +"pasivo", +"пассивная"), + +(1735, 'pet_stance_unknown', "unknown", 0, 0, +"알 수 없음", +"inconnu", +"unbekannt", +"未知", +"未知", +"desconocido", +"desconocido", +"неизвестная"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES +('pet_usage_error', 100), +('pet_no_pet_error', 100), +('pet_stance_report', 100), +('pet_no_target_error', 100), +('pet_target_dead_error', 100), +('pet_invalid_target_error', 100), +('pet_pvp_prohibited_error', 100), +('pet_attack_success', 100), +('pet_attack_failed', 100), +('pet_follow_success', 100), +('pet_stay_success', 100), +('pet_unknown_command_error', 100), +('pet_stance_set_success', 100), +('pet_type_pet', 100), +('pet_type_guardian', 100), +('pet_stance_aggressive', 100), +('pet_stance_defensive', 100), +('pet_stance_passive', 100), +('pet_stance_unknown', 100); \ No newline at end of file diff --git a/src/strategy/actions/AttackAction.cpp b/src/strategy/actions/AttackAction.cpp index 4ea8694378..38cb96835f 100644 --- a/src/strategy/actions/AttackAction.cpp +++ b/src/strategy/actions/AttackAction.cpp @@ -84,9 +84,10 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/) return false; } - if ((sPlayerbotAIConfig->IsInPvpProhibitedZone(bot->GetZoneId()) || - sPlayerbotAIConfig->IsInPvpProhibitedArea(bot->GetAreaId())) - && (target->IsPlayer() || target->IsPet())) + // Check if bot OR target is in prohibited zone/area + if ((target->IsPlayer() || target->IsPet()) && + (sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) || + sPlayerbotAIConfig->IsPvpProhibited(target->GetZoneId(), target->GetAreaId()))) { if (verbose) botAI->TellError("I cannot attack other players in PvP prohibited areas."); diff --git a/src/strategy/actions/PetsAction.cpp b/src/strategy/actions/PetsAction.cpp index 945a883672..9e9e3172c4 100644 --- a/src/strategy/actions/PetsAction.cpp +++ b/src/strategy/actions/PetsAction.cpp @@ -25,7 +25,9 @@ bool PetsAction::Execute(Event event) if (param.empty()) { // If no parameter is provided, show usage instructions and return. - botAI->TellError("Usage: pet "); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_usage_error", "Usage: pet ", {}); + botAI->TellError(text); return false; } @@ -52,7 +54,9 @@ bool PetsAction::Execute(Event event) // If no pets or guardians are found, notify and return. if (targets.empty()) { - botAI->TellError("You have no pet or guardian pet."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_no_pet_error", "You have no pet or guardian pet.", {}); + botAI->TellError(text); return false; } @@ -63,42 +67,54 @@ bool PetsAction::Execute(Event event) if (param == "aggressive") { react = REACT_AGGRESSIVE; - stanceText = "aggressive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_aggressive", "aggressive", {}); } else if (param == "defensive") { react = REACT_DEFENSIVE; - stanceText = "defensive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_defensive", "defensive", {}); } else if (param == "passive") { react = REACT_PASSIVE; - stanceText = "passive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_passive", "passive", {}); } // The "stance" command simply reports the current stance of each pet/guardian. else if (param == "stance") { for (Creature* target : targets) { - std::string type = target->IsPet() ? "pet" : "guardian"; + std::string type = target->IsPet() ? + sPlayerbotTextMgr->GetBotTextOrDefault("pet_type_pet", "pet", {}) : + sPlayerbotTextMgr->GetBotTextOrDefault("pet_type_guardian", "guardian", {}); std::string name = target->GetName(); std::string stance; switch (target->GetReactState()) { case REACT_AGGRESSIVE: - stance = "aggressive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_aggressive", "aggressive", {}); break; case REACT_DEFENSIVE: - stance = "defensive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_defensive", "defensive", {}); break; case REACT_PASSIVE: - stance = "passive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_passive", "passive", {}); break; default: - stance = "unknown"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_unknown", "unknown", {}); break; } - botAI->TellMaster("Current stance of " + type + " \"" + name + "\": " + stance + "."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_report", "Current stance of %type \"%name\": %stance.", + {{"type", type}, {"name", name}, {"stance", stance}}); + botAI->TellMaster(text); } return true; } @@ -121,17 +137,31 @@ bool PetsAction::Execute(Event event) // If no valid target is selected, show an error and return. if (!targetUnit) { - botAI->TellError("No valid target selected by master."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_no_target_error", "No valid target selected by master.", {}); + botAI->TellError(text); return false; } if (!targetUnit->IsAlive()) { - botAI->TellError("Target is not alive."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_target_dead_error", "Target is not alive.", {}); + botAI->TellError(text); return false; } if (!bot->IsValidAttackTarget(targetUnit)) { - botAI->TellError("Target is not a valid attack target for the bot."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_invalid_target_error", "Target is not a valid attack target for the bot.", {}); + botAI->TellError(text); + return false; + } + if (sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) + && (targetUnit->IsPlayer() || targetUnit->IsPet())) + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_pvp_prohibited_error", "I cannot command my pet to attack players in PvP prohibited areas.", {}); + botAI->TellError(text); return false; } @@ -182,9 +212,17 @@ bool PetsAction::Execute(Event event) } // Inform the master if the command succeeded or failed. if (didAttack && sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to attack your target."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_attack_success", "Pet commanded to attack your target.", {}); + botAI->TellMaster(text); + } else if (!didAttack) - botAI->TellError("Pet did not attack. (Already attacking or unable to attack target)"); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_attack_failed", "Pet did not attack. (Already attacking or unable to attack target)", {}); + botAI->TellError(text); + } return didAttack; } // The "follow" command makes all pets/guardians follow the bot. @@ -192,7 +230,11 @@ bool PetsAction::Execute(Event event) { botAI->PetFollow(); if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to follow."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_follow_success", "Pet commanded to follow.", {}); + botAI->TellMaster(text); + } return true; } // The "stay" command causes all pets/guardians to stop and stay in place. @@ -229,14 +271,20 @@ bool PetsAction::Execute(Event event) } } if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to stay."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stay_success", "Pet commanded to stay.", {}); + botAI->TellMaster(text); + } return true; } // Unknown command: show usage instructions and return. else { - botAI->TellError("Unknown pet command: " + param + - ". Use: pet "); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_unknown_command_error", "Unknown pet command: %param. Use: pet ", + {{"param", param}}); + botAI->TellError(text); return false; } @@ -251,7 +299,12 @@ bool PetsAction::Execute(Event event) // Inform the master of the new stance if debug is enabled. if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet stance set to " + stanceText + "."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_set_success", "Pet stance set to %stance.", + {{"stance", stanceText}}); + botAI->TellMaster(text); + } return true; -} +} \ No newline at end of file diff --git a/src/strategy/values/AttackersValue.cpp b/src/strategy/values/AttackersValue.cpp index 745369a3b5..d89f1f8da5 100644 --- a/src/strategy/values/AttackersValue.cpp +++ b/src/strategy/values/AttackersValue.cpp @@ -107,7 +107,6 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set& t { ThreatMgr* threatMgr = ref->GetSource(); Unit* attacker = threatMgr->GetOwner(); - Unit* victim = attacker->GetVictim(); if (player->IsValidAttackTarget(attacker) && player->GetDistance2d(attacker) < sPlayerbotAIConfig->sightDistance) @@ -142,57 +141,107 @@ bool AttackersValue::hasRealThreat(Unit* attacker) (attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast(attacker)); } -bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float range) +bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) { - Creature* c = attacker->ToCreature(); - bool rti = false; - if (attacker && bot->GetGroup()) - rti = bot->GetGroup()->GetTargetIcon(7) == attacker->GetGUID(); - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + if (!botAI) + return false; + + // Basic check + if (!attacker) + return false; + + // bool inCannon = botAI->IsInVehicle(false, true); + // bool enemy = botAI->GetAiObjectContext()->GetValue("enemy player target")->Get(); + + // Validity checks + if (!attacker->IsVisible() || !attacker->IsInWorld() || attacker->GetMapId() != bot->GetMapId()) + return false; - bool leaderHasThreat = false; - if (attacker && bot->GetGroup() && botAI->GetMaster()) - leaderHasThreat = attacker->GetThreatMgr().GetThreat(botAI->GetMaster()); + if (attacker->isDead() || attacker->HasSpiritOfRedemptionAura()) + return false; - bool isMemberBotGroup = false; - if (bot->GetGroup() && botAI->GetMaster()) + // Flag checks + if (attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NON_ATTACKABLE_2)) + return false; + + if (attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) || attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return false; + + // Relationship checks + if (attacker->IsFriendlyTo(bot)) + return false; + + // Critter exception + if (attacker->GetCreatureType() == CREATURE_TYPE_CRITTER && !attacker->IsInCombat()) + return false; + + // Visibility check + if (!bot->CanSeeOrDetect(attacker)) + return false; + + // PvP prohibition checks + if ((attacker->GetGUID().IsPlayer() || attacker->GetGUID().IsPet()) && + (sPlayerbotAIConfig->IsPvpProhibited(attacker->GetZoneId(), attacker->GetAreaId()) || + sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()))) { - PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(botAI->GetMaster()); - if (masterBotAI && !masterBotAI->IsRealPlayer()) - isMemberBotGroup = true; + // This will stop aggresive pets from starting an attack. + // This will stop currently attacking pets from continuing their attack. + // This will first require the bot to change from a combat strat. It will + // not be reached if the bot only switches targets, including NPC targets. + for (Unit::ControlSet::const_iterator itr = bot->m_Controlled.begin(); + itr != bot->m_Controlled.end(); ++itr) + { + Creature* creature = dynamic_cast(*itr); + if (creature && creature->GetVictim() == attacker) + { + creature->AttackStop(); + if (CharmInfo* charmInfo = creature->GetCharmInfo()) + charmInfo->SetIsCommandAttack(false); + } + } + + return false; } - // bool inCannon = botAI->IsInVehicle(false, true); - // bool enemy = botAI->GetAiObjectContext()->GetValue("enemy player target")->Get(); + // Unflagged player check + if (attacker->IsPlayer() && !attacker->IsPvP() && !attacker->IsFFAPvP() && + (!bot->duel || bot->duel->Opponent != attacker)) + return false; + + // Creature-specific checks + Creature* c = attacker->ToCreature(); + if (c) + { + if (c->IsInEvadeMode()) + return false; + + bool leaderHasThreat = false; + if (bot->GetGroup() && botAI->GetMaster()) + leaderHasThreat = attacker->GetThreatMgr().GetThreat(botAI->GetMaster()); + + bool isMemberBotGroup = false; + if (bot->GetGroup() && botAI->GetMaster()) + { + PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(botAI->GetMaster()); + if (masterBotAI && !masterBotAI->IsRealPlayer()) + isMemberBotGroup = true; + } + + bool canAttack = (!isMemberBotGroup && botAI->HasStrategy("attack tagged", BOT_STATE_NON_COMBAT)) || + leaderHasThreat || + (!c->hasLootRecipient() && + (!c->GetVictim() || + (c->GetVictim() && + ((!c->GetVictim()->IsPlayer() || bot->IsInSameGroupWith(c->GetVictim()->ToPlayer())) || + (botAI->GetMaster() && c->GetVictim() == botAI->GetMaster()))))) || + c->isTappedBy(bot); + + if (!canAttack) + return false; + } - return attacker && attacker->IsVisible() && attacker->IsInWorld() && attacker->GetMapId() == bot->GetMapId() && - !attacker->isDead() && - !attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NON_ATTACKABLE_2) && - // (inCannon || !attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) && - // attacker->CanSeeOrDetect(bot) && - // !(attacker->HasUnitState(UNIT_STATE_STUNNED) && botAI->HasAura("shackle undead", attacker)) && - // !((attacker->IsPolymorphed() || botAI->HasAura("sap", attacker) || /*attacker->IsCharmed() ||*/ - // attacker->isFeared()) && !rti) && - /*!sServerFacade->IsInRoots(attacker) &&*/ - !attacker->IsFriendlyTo(bot) && !attacker->HasSpiritOfRedemptionAura() && - // !(attacker->GetGUID().IsPet() && enemy) && - !(attacker->GetCreatureType() == CREATURE_TYPE_CRITTER && !attacker->IsInCombat()) && - !attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) && !attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) && - bot->CanSeeOrDetect(attacker) && - !(sPlayerbotAIConfig->IsPvpProhibited(attacker->GetZoneId(), attacker->GetAreaId()) && - (attacker->GetGUID().IsPlayer() || attacker->GetGUID().IsPet())) && - !(attacker->IsPlayer() && !attacker->IsPvP() && !attacker->IsFFAPvP() && - (!bot->duel || bot->duel->Opponent != attacker)) && - (!c || - (!c->IsInEvadeMode() && - ((!isMemberBotGroup && botAI->HasStrategy("attack tagged", BOT_STATE_NON_COMBAT)) || leaderHasThreat || - (!c->hasLootRecipient() && - (!c->GetVictim() || - (c->GetVictim() && - ((!c->GetVictim()->IsPlayer() || bot->IsInSameGroupWith(c->GetVictim()->ToPlayer())) || - (botAI->GetMaster() && c->GetVictim() == botAI->GetMaster()))))) || - c->isTappedBy(bot)))); + return true; } bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) From bb569b4d3987b7f15e702620eedb22d2ed5cc269 Mon Sep 17 00:00:00 2001 From: Tecc Date: Mon, 8 Dec 2025 12:35:06 +0100 Subject: [PATCH 14/20] Fix: Arena - PersonalRating and MMR issue for bot teams (#1789) # Fix: Arena PersonalRating and MMR issue for bot teams ## Problem Bot arena teams are created with artificial random ratings (1000-2000 range), but when bots join these teams, their personal ratings and matchmaker ratings (MMR) use default config values instead of being adjusted to match the team's artificial rating. This causes matchmaking issues since the system uses personal ratings for queue calculations. ## Root Cause The issue occurred because `SetRatingForAll()` was called during team creation but only affected the captain. When additional bots were added later via `AddMember()`, they received default values from `CONFIG_ARENA_START_PERSONAL_RATING` and `CONFIG_ARENA_START_MATCHMAKER_RATING` instead of values appropriate for the team's artificial rating. ## Solution After bots are added to arena teams, the fix: 1. Uses `SetRatingForAll()` to align all personal ratings with team rating 2. Adjusts matchmaker ratings based on team context vs default configuration 3. Saves changes to both database tables with proper data types ## Impact - Personal ratings now match team ratings for artificial bot teams - MMR values are adjusted for artificial bot team ratings instead of using default config values - Arena matchmaking functions correctly for bot teams with random ratings - Only affects new arena team assignments after deployment - Existing player teams and normal config behavior are unaffected ## Manual Database Update For existing installations, the provided SQL script could be used to fix bot teams created before this patch. ### Update personal rating ```sql UPDATE arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id SET atm.personalRating = at.rating WHERE a.username LIKE 'rndbot%' AND atm.personalRating != at.rating; ``` ### Update MMR for existing entries ```sql UPDATE character_arena_stats cas JOIN characters c ON cas.guid = c.guid JOIN auth.account a ON c.account = a.id JOIN arena_team_member atm ON cas.guid = atm.guid JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId SET cas.matchMakerRating = GREATEST(at.rating, 1500), -- Use team rating or 1500 minimum cas.maxMMR = GREATEST(cas.maxMMR, cas.matchMakerRating) -- Update maxMMR if needed WHERE a.username LIKE '%rndbot%' AND ( -- Update if MMR doesn't match team context (at.rating > 1500 AND cas.matchMakerRating < at.rating) OR (at.rating <= 1500 AND cas.matchMakerRating != 1500) OR cas.matchMakerRating IS NULL ) AND ( -- Map arena team type to character_arena_stats slot (at.type = 2 AND cas.slot = 0) OR -- 2v2 teams use slot 0 (at.type = 3 AND cas.slot = 1) OR -- 3v3 teams use slot 1 (at.type = 5 AND cas.slot = 2) -- 5v5 teams use slot 2 ); ``` ### Insert missing MMR records for bots without character_arena_stats entries ```sql INSERT INTO character_arena_stats (guid, slot, matchMakerRating, maxMMR) SELECT atm.guid, CASE WHEN at.type = 2 THEN 0 -- 2v2 -> slot 0 WHEN at.type = 3 THEN 1 -- 3v3 -> slot 1 WHEN at.type = 5 THEN 2 -- 5v5 -> slot 2 ELSE 0 END as slot, GREATEST(at.rating, 1500) as matchMakerRating, GREATEST(at.rating, 1500) as maxMMR FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id WHERE a.username LIKE '%rndbot%' AND NOT EXISTS ( SELECT 1 FROM character_arena_stats cas2 WHERE cas2.guid = atm.guid AND cas2.slot = CASE WHEN at.type = 2 THEN 0 WHEN at.type = 3 THEN 1 WHEN at.type = 5 THEN 2 ELSE 0 END ) AND at.rating > 0; ``` ## Related issues Fixes: #1787 Fixes: #1800 ## Verification Queries ### Query 1: Check personal rating alignment ```sql SELECT 'Personal Rating Check' as check_type, COUNT(*) as total_bot_members, SUM(CASE WHEN atm.personalRating = at.rating THEN 1 ELSE 0 END) as correct_ratings, SUM(CASE WHEN atm.personalRating != at.rating THEN 1 ELSE 0 END) as incorrect_ratings, ROUND(AVG(at.rating), 2) as avg_team_rating, ROUND(AVG(atm.personalRating), 2) as avg_personal_rating FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id WHERE a.username LIKE '%rndbot%'; ``` ### Query 2: Check MMR alignment ```sql SELECT 'MMR Alignment Check' as check_type, COUNT(*) as total_mmr_records, SUM(CASE WHEN at.rating > 1500 AND cas.matchMakerRating >= at.rating THEN 1 WHEN at.rating <= 1500 AND cas.matchMakerRating = 1500 THEN 1 ELSE 0 END) as correct_mmr, SUM(CASE WHEN at.rating > 1500 AND cas.matchMakerRating < at.rating THEN 1 WHEN at.rating <= 1500 AND cas.matchMakerRating != 1500 THEN 1 ELSE 0 END) as incorrect_mmr, ROUND(AVG(at.rating), 2) as avg_team_rating, ROUND(AVG(cas.matchMakerRating), 2) as avg_mmr, ROUND(AVG(cas.maxMMR), 2) as avg_max_mmr FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id JOIN character_arena_stats cas ON atm.guid = cas.guid WHERE a.username LIKE '%rndbot%' AND ( (at.type = 2 AND cas.slot = 0) OR (at.type = 3 AND cas.slot = 1) OR (at.type = 5 AND cas.slot = 2) ); ``` ### Query 3: Detailed team-by-team analysis ```sql SELECT at.arenaTeamId, at.name as team_name, at.type as team_type, at.rating as team_rating, COUNT(atm.guid) as member_count, GROUP_CONCAT(DISTINCT atm.personalRating) as personal_ratings, GROUP_CONCAT(DISTINCT cas.matchMakerRating) as mmr_values, CASE WHEN COUNT(DISTINCT atm.personalRating) = 1 AND MIN(atm.personalRating) = at.rating THEN 'OK' ELSE 'MISMATCH' END as personal_rating_status, CASE WHEN COUNT(DISTINCT cas.matchMakerRating) = 1 AND ( (at.rating > 1500 AND MIN(cas.matchMakerRating) >= at.rating) OR (at.rating <= 1500 AND MIN(cas.matchMakerRating) = 1500) ) THEN 'OK' ELSE 'MISMATCH' END as mmr_status FROM arena_team at JOIN arena_team_member atm ON at.arenaTeamId = atm.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id LEFT JOIN character_arena_stats cas ON atm.guid = cas.guid AND cas.slot = CASE WHEN at.type = 2 THEN 0 WHEN at.type = 3 THEN 1 WHEN at.type = 5 THEN 2 ELSE 0 END WHERE a.username LIKE '%rndbot%' GROUP BY at.arenaTeamId, at.name, at.type, at.rating ORDER BY at.rating DESC; ``` --- src/factory/PlayerbotFactory.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/factory/PlayerbotFactory.cpp b/src/factory/PlayerbotFactory.cpp index 65ffc444ee..50a216c970 100644 --- a/src/factory/PlayerbotFactory.cpp +++ b/src/factory/PlayerbotFactory.cpp @@ -4099,6 +4099,7 @@ void PlayerbotFactory::InitImmersive() void PlayerbotFactory::InitArenaTeam() { + if (!sPlayerbotAIConfig->IsInRandomAccountList(bot->GetSession()->GetAccountId())) return; @@ -4185,10 +4186,34 @@ void PlayerbotFactory::InitArenaTeam() if (botcaptain && botcaptain->GetTeamId() == bot->GetTeamId()) // need? { + // Add bot to arena team arenateam->AddMember(bot->GetGUID()); - arenateam->SaveToDB(); + + // Only synchronize ratings once the team is full (avoid redundant work) + // The captain was added with incorrect ratings when the team was created, + // so we fix everyone's ratings once the roster is complete + if (arenateam->GetMembersSize() >= (uint32)arenateam->GetType()) + { + uint32 teamRating = arenateam->GetRating(); + + // Use SetRatingForAll to align all members with team rating + arenateam->SetRatingForAll(teamRating); + + // For bot-only teams, keep MMR synchronized with team rating + // This ensures matchmaking reflects the artificial team strength (1000-2000 range) + // instead of being influenced by the global CONFIG_ARENA_START_MATCHMAKER_RATING + for (auto& member : arenateam->GetMembers()) + { + // Set MMR to match personal rating (which already matches team rating) + member.MatchMakerRating = member.PersonalRating; + member.MaxMMR = std::max(member.MaxMMR, member.PersonalRating); + } + // Force save all member data to database + arenateam->SaveToDB(true); + } } } + arenateams.erase(arenateams.begin() + index); } From 910b8a9c53bed15e4504bd7de965db68e6f011ef Mon Sep 17 00:00:00 2001 From: Nicolas Lebacq Date: Tue, 9 Dec 2025 18:29:57 +0000 Subject: [PATCH 15/20] fix: Made bots roll in a more reasonable time on group loots. (#1857) # Description This PR changes the way loot rolls are being evaluated. It puts a maximum priority on the loot action so it does not hang for so long. --- src/PlayerbotAI.cpp | 3 --- src/strategy/generic/WorldPacketHandlerStrategy.cpp | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 2ef6860a2a..c1878632c6 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -1384,9 +1384,6 @@ void PlayerbotAI::DoNextAction(bool min) else if (bot->isAFK()) bot->ToggleAFK(); - Group* group = bot->GetGroup(); - PlayerbotAI* masterBotAI = nullptr; - if (master && master->IsInWorld()) { float distance = sServerFacade->GetDistance2d(bot, master); diff --git a/src/strategy/generic/WorldPacketHandlerStrategy.cpp b/src/strategy/generic/WorldPacketHandlerStrategy.cpp index 69b7685e2f..97b1ba1ba8 100644 --- a/src/strategy/generic/WorldPacketHandlerStrategy.cpp +++ b/src/strategy/generic/WorldPacketHandlerStrategy.cpp @@ -5,8 +5,6 @@ #include "WorldPacketHandlerStrategy.h" -#include "Playerbots.h" - void WorldPacketHandlerStrategy::InitTriggers(std::vector& triggers) { PassTroughStrategy::InitTriggers(triggers); @@ -69,7 +67,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back(new TriggerNode("questgiver quest details", NextAction::array(0, new NextAction("turn in query quest", relevance), nullptr))); // loot roll - triggers.push_back(new TriggerNode("very often", NextAction::array(0, new NextAction("loot roll", 10.0f), nullptr))); + triggers.push_back(new TriggerNode("very often", NextAction::array(0, new NextAction("loot roll", relevance), nullptr))); } WorldPacketHandlerStrategy::WorldPacketHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) From f4b4d8967fd5136a199663b717414274161e8dc4 Mon Sep 17 00:00:00 2001 From: Crow Date: Wed, 10 Dec 2025 14:08:25 -0600 Subject: [PATCH 16/20] Karazhan Refactor + Nightbane Strategy (#1847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR completely refactors the Karazhan raid strategies to clean up the code and polish up/introduce new strategies, including for Nightbane. General changes with respect to the code: - Moved gating checks for action methods to triggers instead of relying on isUseful functions - Got rid of the nonsensical helper class I made and switched to a helper namespace - Broke up some longer action classes into separate methods and/or private members - Deleted/consolidated some excess code - Made greater use of early returns - Renamed methods, multipliers, and triggers to make them easier to understand - Generally made edits to conform with AC code standards Below are the implemented strategies. I’m including all of them, not just new/modified ones, because I don’t think the first set of strategies was ever tested. So each boss could probably use complete testing. ### **Trash** I added strategies for a trash mob that I find particularly annoying: - Mana Warp: These are, IMO, the most annoying trash mobs in Karazhan. They blow up when low on health, and having two blow up in quick succession is enough to take out a decent chunk of your raid. The strategy directs bots to use pretty much every stun in the book on Mana Warps when they’re low on HP, which is the only way to prevent the explosion. ### **Attumen** - During the first phase, bots will focus on Midnight (taking either Midnight or Attumen to 25% starts the next phase). When Attumen spawns, the assist tank will pick him up and move him away so that bots don’t get cleaved. - When Attumen mounts Midnight, starting phase 2, threat is wiped, and bots will pause DPS for a few seconds to allow the main tank to get aggro. All bots, other than the main tank and any bot that pulls aggro, will stack behind Attumen (~6 yards for ranged so Hunters can still attack). ### **Moroes** - As before, bots will mark and prioritize adds and the boss in the recommended kill order: Dorothea, Catriona, Keira, Rafe, Robin, Crispin, and Moroes. In practice, the enemies will probably be stacked up, and the bots will AoE them down in accordance with their typical AoE strategies, but classes without AoE capabilities should still prioritize the skull. - Based on testing feedback, added a method for the main tank to prioritize Moroes ### **Maiden of Virtue** I’ve made only minor changes to Revision’s original strategy here. - The tank with aggro will position Maiden in the middle of the room and move her to a healer when Repentance is cast so that the Holy Ground will break the healer’s stun. - Ranged bots have assigned positions between the columns around the middle of the room (to prevent chain damage from Holy Wrath). ### **The Big Bad Wolf** - The tank with aggro brings the boss to the front left of the stage. - If a bot gets turned into Little Red Riding Hood, it will run around the stage in a counter-clockwise rectangle until the transformation fades. I tweaked this strategy a bit; it's still not perfect, but it works better than before. ### **Romulo and Julianne** There are no substantive changes to this strategy. As before, in phase 3, when both bosses are active, the bots switch back-and-forth between the bosses by alternating the skull icon based on which boss has lower HP (10% differential). ### **The Wizard of Oz** There are no substantive changes to this strategy. As before, bots mark the bosses with a skull icon in their recommended kill order: Dorothee, Tito (assuming he spawns before you kill Dorothee), Roar, Strawman, Tinhead, and the Crone. Additionally, Mages will spam Scorch on Strawman to daze him. ### **The Curator** - The tank will drag the boss to a fixed spot down the hallway. Ranged bots will spread out to avoid chain damage from Arcing Sear, and bots will mark Astral Flares with the skull icon to prioritize them down. - Those strategies already existed, but now I made the assist tank also focus on the boss to try to stay second in aggro and therefore absorb Hateful Bolts. - Added a multiplier to save Bloodlust/Heroism until Evocation. ### **Terestian Illhoof** There are no substantive changes to this strategy. The bots will mark targets with the skull icon in the following order: Demonic Chains, Kil'rek, and Illhoof. ### **Shade of Aran** I redid the strategies a bit, and I think (hope) they work better. - Flame Wreath: Bots will stop moving until the aura fades. There is a bug in which Flame Wreath will sometimes persist long beyond its supposed 20-second duration. If Aran casts Arcane Explosion during this time, you will almost certainly wipe, so that’s frustrating. I made it so that bots will stay in place still and eat the Arcane Explosion because it’s the lesser of two evils, and if you are overgeared, you may be able to survive. In the previous strategy, bots would stop actions entirely, but now they should keep attacking/casting without moving. - Arcane Explosion: No substantive changes here--bots will run out immediately and stay out of range until the cast finishes. - Conjured Elementals: No substantive changes here--they will be marked one-by-one by the skull icon, except that the marking will skip any elemental that is banished. - Ranged Positioning: I redid this strategy. Ranged bots will now maintain a distance between 11 and 15 yards from the boss. This keeps them out of the 10-yard radius in which Aran silences while also keeping them from getting too far away and getting stuck in the alcoves. ### **Netherspite** I significantly refactored the action methods here, but substantively the original strategy remains mostly intact. - Red (Tank) Beam: One tank will be assigned to block the beam for each Portal Phase. The assigned tank will dance in and out of the beam (5 seconds in, 5 seconds out). Tanks intentionally do not avoid Void Zones (it was the lesser of two evils for them to take that damage vs. trying to dynamically avoid them, moving the boss, and possibly getting everybody out of position. - Blue (DPS) Beam: DPS other than Rogues and Warriors are eligible to be assigned (one-by-one) to block this beam. When the assigned blocker reaches 25 stacks of the debuff, they will leave the beam, and the next assigned blocker will take their place. If a Void Zone drops under the assigned blocker, the bot will move along the beam to get out of the Void Zone so that they do not stop blocking. - Green (Healer) Beam: This works the same way as the Blue Beam, except that eligible blockers are healers, Rogues, and DPS Warriors. Healers that are assigned to block will swap in the same way as Blue Beam blockers. If a Rogue or DPS Warrior is the assigned blocker, however, they will stand in the beam for the entire Portal Phase since they do not suffer any adverse effects from the beam. In this PR, I made the strategy prioritize Rogues and DPS Warriors over healers to try to avoid the need for bots to swap (and to avoid the irritating scenario in which a healer would block the beam for the first half of a phase and then tag in a Rogue or DPS Warrior, which would be wasted by blocking only half of a phase). - Non-Blockers: They will stay at least 5 yards away from each beam until called to be an assigned blocker. They will also avoid Void Zones. - Banish Phase: The only strategy I implemented was for bots to avoid residual Void Zones from the Portal Phase. - Phase Transitions: Bots should pause DPS at the beginning of the encounter and whenever Netherspite transitions back from the Banish Phase to the Portal Phase (which is an aggro reset). Note that this doesn't wipe DOTs, and there's not much I can do about that. ### **Prince Malchezaar** The action methods are significantly refactored, but the strategy substantively is not changed very much. - Bots will maintain distance from Infernals. The tank has a larger avoidance radius to give DPS a little bit of margin to work with. Depending on Infernal placement, it is possible for bots to get stuck in some bad positions. In that case, the best solution is to put them on “flee” and lead them to a better position. - Bots that get Enfeebled will run out of Shadow Nova range. They should pick a path that does not cross within any Infernal's Hellfire radius. - Added a multiplier to save Bloodlust/Heroism until Phase 3. ### **Nightbane** **Disclaimer**: Bots are terrible at this encounter, in large part because the map is awful (even before the recent Core changes). So the strategies are not ideal because they need to operate within the bots’ limitations. I STRONGLY suggest you clear the entire Livery Stables (including the upper level) because the mobs in them have a high risk of pulling through the floor of the Master’s Terrace. Ideally, you should clear out the Scullery too. The strategy uses waypoints toward the Northeastern door to the Master’s Terrace. I tried several different locations, and that worked best for me based on where Nightbane lands (note that he has a different landing spot for the encounter start vs. the start subsequent ground phases). - Ground Phase, main tank: The main tank uses two waypoints after it picks up the boss—the movement pattern should be kind of like a reverse checkmark, where the tank moves back along the inner edge of the terrace, then pivots and moves at a bit of an angle to the outer edge. I did this as a way to get the tank to face Nightbane sideways across the terrace, which is how he’s supposed to be tanked. The main tank will not get out of Charred Earth. This is intended. The tank cannot move dynamically enough to avoid it while also not turning the boss and wiping the raid. - Ground phase, ranged: Ranged bots rotate between three waypoints. They start stacked at the same position. If Charred Earth is dropped on that position, the bots will rotate to the second position. If Charred Earth is dropped on that position, they will rotate to the third position. The maximum number of Charred Earths that can be active is two, so if one is dropped on the third position, the ranged bots should rotate back to the first position. - Ground Phase, other melee: Melee bots have no coded Charred Earth avoidance strategy. They do decently enough with the general “avoid aoe” strategy. - Flight Phase: Bots move to Nightbane’s flight position—Nightbane is bugged and cannot be attacked in the air in AC, but all bots still need to stay near him or he will wipe the raid with Fireball Barrage. Bots stack on the same position; when Rain of Bones is cast (one time, snapshotted on the target’s location), all bots will move away to a second nearby position. They will then kill the Restless Skeletons. The Flight Phase lasts for 45 seconds, but Nightbane emotes after 35 seconds and goes to land—during the final 10-second period, bots are freed from all strategies and will follow the master. You need to lead them back to the Northeastern part of the terrace before Nightbane lands so that bots can get immediately positioned when he lands. - Phase Changes: Whenever Nightbane lands, bots should pause DPS for the main tank to get aggro. - Managing bots/pets: During the Flight Phase, bots and pets have a tendency to chase Nightbane outside of the boundaries of the map and pull mobs from other parts of the instance as well as cause Restless Skeletons to spawn out of bounds (and then further cause bots/pets to go out of bounds to attack the skeletons). The strategy should solve for this, but if there are still issues, it should be noted. My resolution was to implement the following: (1) when Nightbane takes flight, bots stop attacking and mark him with the moon icon, and Hunters and Warlocks put pets on passive and recall them (they will put pets on defensive again when Nightbane lands), and (2) all temporary pets are disabled for the duration of the fight (Water Elementals, Shaman wolves, Druid treants, etc.). **Known Issues:** - The approach to getting Nightbane to turn sideways is not always spot on since it’s a workaround to account for what should be dynamic movement. He can end up at a bit of an angle. I’ve tweaked the positions such that if he is at an angle, it should not be a situation in which any ranged position is exposed to Smoldering Breath or Cleave. That does increase the risk that poor positioning may expose a ranged position to Tail Sweep. This is obviously suboptimal, but that attack is much more survivable. - Ranged bots move between the three waypoints by reading the presence of the Charred Earth aura on themselves. Sometimes, a bot may reach the next waypoint with the Charred Earth aura from the previous waypoint still on them (depending on timing of ticks), in which case the bot will keep moving to the next waypoint. This can cause an issue in which the ranged group gets split. So you could have Charred Earth dropped on position 1, and some ranged bots will go to position 2 but others will go to position 3, and then if the second Charred Earth is dropped on position 3, the bots at position 3 will move back to position 1 (which is still in Charred Earth) before then moving to position 2. Balancing spacing between waypoints, positioning with respect to the boss, line of sight, and maximum distance is very difficult to do. I messed with the positioning a lot, and I am not sure if this possibility cannot be entirely avoided without creating other issues. I have at least reached a result in which I have not seen any bots cycle indefinitely (though if testing observes this, it is obviously a problem). Ultimately, I wouldn’t say I’m totally satisfied with the strategy. Wipes are still possible on account of bad RNG. But I think it does make the fight a lot more manageable, as it is a fight that is very difficult with IP nerfs in the absence of any strategy. Previously, I needed to bring 3 healers, and they would still all be out of mana by the first Flight Phase and reliant on MP5 for the remainder of the fight, and I would wipe multiple times before a kill. --- .../karazhan/RaidKarazhanActionContext.h | 277 ++- .../raids/karazhan/RaidKarazhanActions.cpp | 1767 +++++++++-------- .../raids/karazhan/RaidKarazhanActions.h | 259 ++- .../raids/karazhan/RaidKarazhanHelpers.cpp | 649 +++--- .../raids/karazhan/RaidKarazhanHelpers.h | 189 +- .../karazhan/RaidKarazhanMultipliers.cpp | 446 +++-- .../raids/karazhan/RaidKarazhanMultipliers.h | 110 +- .../raids/karazhan/RaidKarazhanStrategy.cpp | 203 +- .../raids/karazhan/RaidKarazhanStrategy.h | 2 +- .../karazhan/RaidKarazhanTriggerContext.h | 270 ++- .../raids/karazhan/RaidKarazhanTriggers.cpp | 365 +++- .../raids/karazhan/RaidKarazhanTriggers.h | 262 ++- 12 files changed, 3207 insertions(+), 1592 deletions(-) diff --git a/src/strategy/raids/karazhan/RaidKarazhanActionContext.h b/src/strategy/raids/karazhan/RaidKarazhanActionContext.h index 2c1f71686a..34f276a94b 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActionContext.h +++ b/src/strategy/raids/karazhan/RaidKarazhanActionContext.h @@ -1,5 +1,5 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANACTIONS_CONTEXT_H -#define _PLAYERBOT_RAIDKARAZHANACTIONS_CONTEXT_H +#ifndef _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H +#define _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H #include "RaidKarazhanActions.h" #include "NamedObjectContext.h" @@ -9,77 +9,254 @@ class RaidKarazhanActionContext : public NamedObjectContext public: RaidKarazhanActionContext() { - creators["karazhan attumen the huntsman stack behind"] = &RaidKarazhanActionContext::karazhan_attumen_the_huntsman_stack_behind; + // Trash + creators["mana warp stun creature before warp breach"] = + &RaidKarazhanActionContext::mana_warp_stun_creature_before_warp_breach; - creators["karazhan moroes mark target"] = &RaidKarazhanActionContext::karazhan_moroes_mark_target; + // Attumen the Huntsman + creators["attumen the huntsman mark target"] = + &RaidKarazhanActionContext::attumen_the_huntsman_mark_target; - creators["karazhan maiden of virtue position boss"] = &RaidKarazhanActionContext::karazhan_maiden_of_virtue_position_boss; - creators["karazhan maiden of virtue position ranged"] = &RaidKarazhanActionContext::karazhan_maiden_of_virtue_position_ranged; + creators["attumen the huntsman split bosses"] = + &RaidKarazhanActionContext::attumen_the_huntsman_split_bosses; - creators["karazhan big bad wolf position boss"] = &RaidKarazhanActionContext::karazhan_big_bad_wolf_position_boss; - creators["karazhan big bad wolf run away"] = &RaidKarazhanActionContext::karazhan_big_bad_wolf_run_away; + creators["attumen the huntsman stack behind"] = + &RaidKarazhanActionContext::attumen_the_huntsman_stack_behind; - creators["karazhan romulo and julianne mark target"] = &RaidKarazhanActionContext::karazhan_romulo_and_julianne_mark_target; + creators["attumen the huntsman manage dps timer"] = + &RaidKarazhanActionContext::attumen_the_huntsman_manage_dps_timer; - creators["karazhan wizard of oz mark target"] = &RaidKarazhanActionContext::karazhan_wizard_of_oz_mark_target; - creators["karazhan wizard of oz scorch strawman"] = &RaidKarazhanActionContext::karazhan_wizard_of_oz_scorch_strawman; + // Moroes + creators["moroes main tank attack boss"] = + &RaidKarazhanActionContext::moroes_main_tank_attack_boss; - creators["karazhan the curator mark target"] = &RaidKarazhanActionContext::karazhan_the_curator_mark_target; - creators["karazhan the curator position boss"] = &RaidKarazhanActionContext::karazhan_the_curator_position_boss; - creators["karazhan the curator spread ranged"] = &RaidKarazhanActionContext::karazhan_the_curator_spread_ranged; + creators["moroes mark target"] = + &RaidKarazhanActionContext::moroes_mark_target; - creators["karazhan terestian illhoof mark target"] = &RaidKarazhanActionContext::karazhan_terestian_illhoof_mark_target; + // Maiden of Virtue + creators["maiden of virtue move boss to healer"] = + &RaidKarazhanActionContext::maiden_of_virtue_move_boss_to_healer; - creators["karazhan shade of aran arcane explosion run away"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_arcane_explosion_run_away; - creators["karazhan shade of aran flame wreath stop movement"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_flame_wreath_stop_movement; - creators["karazhan shade of aran mark conjured elemental"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_mark_conjured_elemental; - creators["karazhan shade of aran spread ranged"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_spread_ranged; + creators["maiden of virtue position ranged"] = + &RaidKarazhanActionContext::maiden_of_virtue_position_ranged; - creators["karazhan netherspite block red beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_red_beam; - creators["karazhan netherspite block blue beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_blue_beam; - creators["karazhan netherspite block green beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_green_beam; - creators["karazhan netherspite avoid beam and void zone"] = &RaidKarazhanActionContext::karazhan_netherspite_avoid_beam_and_void_zone; - creators["karazhan netherspite banish phase avoid void zone"] = &RaidKarazhanActionContext::karazhan_netherspite_banish_phase_avoid_void_zone; + // The Big Bad Wolf + creators["big bad wolf position boss"] = + &RaidKarazhanActionContext::big_bad_wolf_position_boss; - creators["karazhan prince malchezaar non tank avoid hazard"] = &RaidKarazhanActionContext::karazhan_prince_malchezaar_non_tank_avoid_hazard; - creators["karazhan prince malchezaar tank avoid hazard"] = &RaidKarazhanActionContext::karazhan_prince_malchezaar_tank_avoid_hazard; + creators["big bad wolf run away from boss"] = + &RaidKarazhanActionContext::big_bad_wolf_run_away_from_boss; + + // Romulo and Julianne + creators["romulo and julianne mark target"] = + &RaidKarazhanActionContext::romulo_and_julianne_mark_target; + + // The Wizard of Oz + creators["wizard of oz mark target"] = + &RaidKarazhanActionContext::wizard_of_oz_mark_target; + creators["wizard of oz scorch strawman"] = + &RaidKarazhanActionContext::wizard_of_oz_scorch_strawman; + + // The Curator + creators["the curator mark astral flare"] = + &RaidKarazhanActionContext::the_curator_mark_astral_flare; + + creators["the curator position boss"] = + &RaidKarazhanActionContext::the_curator_position_boss; + + creators["the curator spread ranged"] = + &RaidKarazhanActionContext::the_curator_spread_ranged; + + // Terestian Illhoof + creators["terestian illhoof mark target"] = + &RaidKarazhanActionContext::terestian_illhoof_mark_target; + + // Shade of Aran + creators["shade of aran run away from arcane explosion"] = + &RaidKarazhanActionContext::shade_of_aran_run_away_from_arcane_explosion; + + creators["shade of aran stop moving during flame wreath"] = + &RaidKarazhanActionContext::shade_of_aran_stop_moving_during_flame_wreath; + + creators["shade of aran mark conjured elemental"] = + &RaidKarazhanActionContext::shade_of_aran_mark_conjured_elemental; + + creators["shade of aran ranged maintain distance"] = + &RaidKarazhanActionContext::shade_of_aran_ranged_maintain_distance; + + // Netherspite + creators["netherspite block red beam"] = + &RaidKarazhanActionContext::netherspite_block_red_beam; + + creators["netherspite block blue beam"] = + &RaidKarazhanActionContext::netherspite_block_blue_beam; + + creators["netherspite block green beam"] = + &RaidKarazhanActionContext::netherspite_block_green_beam; + + creators["netherspite avoid beam and void zone"] = + &RaidKarazhanActionContext::netherspite_avoid_beam_and_void_zone; + + creators["netherspite banish phase avoid void zone"] = + &RaidKarazhanActionContext::netherspite_banish_phase_avoid_void_zone; + + creators["netherspite manage timers and trackers"] = + &RaidKarazhanActionContext::netherspite_manage_timers_and_trackers; + + // Prince Malchezaar + creators["prince malchezaar enfeebled avoid hazard"] = + &RaidKarazhanActionContext::prince_malchezaar_enfeebled_avoid_hazard; + + creators["prince malchezaar non tank avoid infernal"] = + &RaidKarazhanActionContext::prince_malchezaar_non_tank_avoid_infernal; + + creators["prince malchezaar main tank movement"] = + &RaidKarazhanActionContext::prince_malchezaar_main_tank_movement; + + // Nightbane + creators["nightbane ground phase position boss"] = + &RaidKarazhanActionContext::nightbane_ground_phase_position_boss; + + creators["nightbane ground phase rotate ranged positions"] = + &RaidKarazhanActionContext::nightbane_ground_phase_rotate_ranged_positions; + + creators["nightbane cast fear ward on main tank"] = + &RaidKarazhanActionContext::nightbane_cast_fear_ward_on_main_tank; + + creators["nightbane control pet aggression"] = + &RaidKarazhanActionContext::nightbane_control_pet_aggression; + + creators["nightbane flight phase movement"] = + &RaidKarazhanActionContext::nightbane_flight_phase_movement; + + creators["nightbane manage timers and trackers"] = + &RaidKarazhanActionContext::nightbane_manage_timers_and_trackers; } private: - static Action* karazhan_attumen_the_huntsman_stack_behind(PlayerbotAI* botAI) { return new KarazhanAttumenTheHuntsmanStackBehindAction(botAI); } + // Trash + static Action* mana_warp_stun_creature_before_warp_breach( + PlayerbotAI* botAI) { return new ManaWarpStunCreatureBeforeWarpBreachAction(botAI); } + + // Attumen the Huntsman + static Action* attumen_the_huntsman_mark_target( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanMarkTargetAction(botAI); } + + static Action* attumen_the_huntsman_split_bosses( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanSplitBossesAction(botAI); } + + static Action* attumen_the_huntsman_stack_behind( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanStackBehindAction(botAI); } + + static Action* attumen_the_huntsman_manage_dps_timer( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanManageDpsTimerAction(botAI); } + + // Moroes + static Action* moroes_main_tank_attack_boss( + PlayerbotAI* botAI) { return new MoroesMainTankAttackBossAction(botAI); } + + static Action* moroes_mark_target( + PlayerbotAI* botAI) { return new MoroesMarkTargetAction(botAI); } + + // Maiden of Virtue + static Action* maiden_of_virtue_move_boss_to_healer( + PlayerbotAI* botAI) { return new MaidenOfVirtueMoveBossToHealerAction(botAI); } + + static Action* maiden_of_virtue_position_ranged( + PlayerbotAI* botAI) { return new MaidenOfVirtuePositionRangedAction(botAI); } + + // The Big Bad Wolf + static Action* big_bad_wolf_position_boss( + PlayerbotAI* botAI) { return new BigBadWolfPositionBossAction(botAI); } + + static Action* big_bad_wolf_run_away_from_boss( + PlayerbotAI* botAI) { return new BigBadWolfRunAwayFromBossAction(botAI); } + + // Romulo and Julianne + static Action* romulo_and_julianne_mark_target( + PlayerbotAI* botAI) { return new RomuloAndJulianneMarkTargetAction(botAI); } + + // The Wizard of Oz + static Action* wizard_of_oz_mark_target( + PlayerbotAI* botAI) { return new WizardOfOzMarkTargetAction(botAI); } + + static Action* wizard_of_oz_scorch_strawman( + PlayerbotAI* botAI) { return new WizardOfOzScorchStrawmanAction(botAI); } + + // The Curator + static Action* the_curator_mark_astral_flare( + PlayerbotAI* botAI) { return new TheCuratorMarkAstralFlareAction(botAI); } + + static Action* the_curator_position_boss( + PlayerbotAI* botAI) { return new TheCuratorPositionBossAction(botAI); } + + static Action* the_curator_spread_ranged( + PlayerbotAI* botAI) { return new TheCuratorSpreadRangedAction(botAI); } + + // Terestian Illhoof + static Action* terestian_illhoof_mark_target( + PlayerbotAI* botAI) { return new TerestianIllhoofMarkTargetAction(botAI); } + + // Shade of Aran + static Action* shade_of_aran_run_away_from_arcane_explosion( + PlayerbotAI* botAI) { return new ShadeOfAranRunAwayFromArcaneExplosionAction(botAI); } + + static Action* shade_of_aran_stop_moving_during_flame_wreath( + PlayerbotAI* botAI) { return new ShadeOfAranStopMovingDuringFlameWreathAction(botAI); } + + static Action* shade_of_aran_mark_conjured_elemental( + PlayerbotAI* botAI) { return new ShadeOfAranMarkConjuredElementalAction(botAI); } + + static Action* shade_of_aran_ranged_maintain_distance( + PlayerbotAI* botAI) { return new ShadeOfAranRangedMaintainDistanceAction(botAI); } + + // Netherspite + static Action* netherspite_block_red_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockRedBeamAction(botAI); } + + static Action* netherspite_block_blue_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockBlueBeamAction(botAI); } + + static Action* netherspite_block_green_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockGreenBeamAction(botAI); } + + static Action* netherspite_avoid_beam_and_void_zone( + PlayerbotAI* botAI) { return new NetherspiteAvoidBeamAndVoidZoneAction(botAI); } + + static Action* netherspite_banish_phase_avoid_void_zone( + PlayerbotAI* botAI) { return new NetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } - static Action* karazhan_moroes_mark_target(PlayerbotAI* botAI) { return new KarazhanMoroesMarkTargetAction(botAI); } + static Action* netherspite_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NetherspiteManageTimersAndTrackersAction(botAI); } - static Action* karazhan_maiden_of_virtue_position_boss(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtuePositionBossAction(botAI); } - static Action* karazhan_maiden_of_virtue_position_ranged(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtuePositionRangedAction(botAI); } + // Prince Malchezaar + static Action* prince_malchezaar_enfeebled_avoid_hazard( + PlayerbotAI* botAI) { return new PrinceMalchezaarEnfeebledAvoidHazardAction(botAI); } - static Action* karazhan_big_bad_wolf_position_boss(PlayerbotAI* botAI) { return new KarazhanBigBadWolfPositionBossAction(botAI); } - static Action* karazhan_big_bad_wolf_run_away(PlayerbotAI* botAI) { return new KarazhanBigBadWolfRunAwayAction(botAI); } + static Action* prince_malchezaar_non_tank_avoid_infernal( + PlayerbotAI* botAI) { return new PrinceMalchezaarNonTankAvoidInfernalAction(botAI); } - static Action* karazhan_romulo_and_julianne_mark_target(PlayerbotAI* botAI) { return new KarazhanRomuloAndJulianneMarkTargetAction(botAI); } + static Action* prince_malchezaar_main_tank_movement( + PlayerbotAI* botAI) { return new PrinceMalchezaarMainTankMovementAction(botAI); } - static Action* karazhan_wizard_of_oz_mark_target(PlayerbotAI* botAI) { return new KarazhanWizardOfOzMarkTargetAction(botAI); } - static Action* karazhan_wizard_of_oz_scorch_strawman(PlayerbotAI* botAI) { return new KarazhanWizardOfOzScorchStrawmanAction(botAI); } + // Nightbane + static Action* nightbane_ground_phase_position_boss( + PlayerbotAI* botAI) { return new NightbaneGroundPhasePositionBossAction(botAI); } - static Action* karazhan_the_curator_mark_target(PlayerbotAI* botAI) { return new KarazhanTheCuratorMarkTargetAction(botAI); } - static Action* karazhan_the_curator_position_boss(PlayerbotAI* botAI) { return new KarazhanTheCuratorPositionBossAction(botAI); } - static Action* karazhan_the_curator_spread_ranged(PlayerbotAI* botAI) { return new KarazhanTheCuratorSpreadRangedAction(botAI); } + static Action* nightbane_ground_phase_rotate_ranged_positions( + PlayerbotAI* botAI) { return new NightbaneGroundPhaseRotateRangedPositionsAction(botAI); } - static Action* karazhan_terestian_illhoof_mark_target(PlayerbotAI* botAI) { return new KarazhanTerestianIllhoofMarkTargetAction(botAI); } + static Action* nightbane_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new NightbaneCastFearWardOnMainTankAction(botAI); } - static Action* karazhan_shade_of_aran_arcane_explosion_run_away(PlayerbotAI* botAI) { return new KarazhanShadeOfAranArcaneExplosionRunAwayAction(botAI); } - static Action* karazhan_shade_of_aran_flame_wreath_stop_movement(PlayerbotAI* botAI) { return new KarazhanShadeOfAranFlameWreathStopMovementAction(botAI); } - static Action* karazhan_shade_of_aran_mark_conjured_elemental(PlayerbotAI* botAI) { return new KarazhanShadeOfAranMarkConjuredElementalAction(botAI); } - static Action* karazhan_shade_of_aran_spread_ranged(PlayerbotAI* botAI) { return new KarazhanShadeOfAranSpreadRangedAction(botAI); } + static Action* nightbane_control_pet_aggression( + PlayerbotAI* botAI) { return new NightbaneControlPetAggressionAction(botAI); } - static Action* karazhan_netherspite_block_red_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockRedBeamAction(botAI); } - static Action* karazhan_netherspite_block_blue_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockBlueBeamAction(botAI); } - static Action* karazhan_netherspite_block_green_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockGreenBeamAction(botAI); } - static Action* karazhan_netherspite_avoid_beam_and_void_zone(PlayerbotAI* botAI) { return new KarazhanNetherspiteAvoidBeamAndVoidZoneAction(botAI); } - static Action* karazhan_netherspite_banish_phase_avoid_void_zone(PlayerbotAI* botAI) { return new KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } + static Action* nightbane_flight_phase_movement( + PlayerbotAI* botAI) { return new NightbaneFlightPhaseMovementAction(botAI); } - static Action* karazhan_prince_malchezaar_non_tank_avoid_hazard(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarNonTankAvoidHazardAction(botAI); } - static Action* karazhan_prince_malchezaar_tank_avoid_hazard(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarTankAvoidHazardAction(botAI); } + static Action* nightbane_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NightbaneManageTimersAndTrackersAction(botAI); } }; #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanActions.cpp b/src/strategy/raids/karazhan/RaidKarazhanActions.cpp index e45d63f7b2..e452cf4583 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActions.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanActions.cpp @@ -1,81 +1,215 @@ #include "RaidKarazhanActions.h" #include "RaidKarazhanHelpers.h" -#include "AiObjectContext.h" -#include "PlayerbotAI.h" -#include "PlayerbotMgr.h" -#include "PlayerbotTextMgr.h" #include "Playerbots.h" -#include "Position.h" +#include "PlayerbotTextMgr.h" + +using namespace KarazhanHelpers; + +// Trash + +// Mana Warps blow up when they die for massive raid damage +// But they cannot cast the ability if they are stunned +bool ManaWarpStunCreatureBeforeWarpBreachAction::Execute(Event event) +{ + Unit* manaWarp = GetFirstAliveUnitByEntry(botAI, NPC_MANA_WARP); + if (!manaWarp) + return false; + + static const std::array spells = + { + "bash", + "concussion blow", + "hammer of justice", + "kidney shot", + "maim", + "revenge stun", + "shadowfury", + "shockwave" + }; + + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, manaWarp)) + return botAI->CastSpell(spell, manaWarp); + } + + return false; +} + +// Attumen the Huntsman + +// Prioritize Midnight until Attumen is mounted +bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) +{ + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (attumenMounted) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithStar(bot, attumenMounted); + + SetRtiTarget(botAI, "star", attumenMounted); + + if (bot->GetTarget() != attumenMounted->GetGUID()) + { + bot->SetTarget(attumenMounted->GetGUID()); + return Attack(attumenMounted); + } + } + else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithStar(bot, midnight); + + if (!botAI->IsAssistTankOfIndex(bot, 0)) + { + SetRtiTarget(botAI, "star", midnight); + + if (bot->GetTarget() != midnight->GetGUID()) + { + bot->SetTarget(midnight->GetGUID()); + return Attack(midnight); + } + } + } + + return false; +} -namespace +// Off tank should move Attumen out of the way so he doesn't cleave bots +bool AttumenTheHuntsmanSplitBossesAction::Execute(Event event) { - // Big Bad Wolf - static int currentIndex = 0; - // Netherspite - static std::map beamMoveTimes; - static std::map lastBeamMoveSideways; + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return false; + + Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN); + if (!attumen) + return false; + + MarkTargetWithSquare(bot, attumen); + SetRtiTarget(botAI, "square", attumen); + + if (bot->GetVictim() != attumen) + return Attack(attumen); + + if (attumen->GetVictim() == bot && midnight->GetVictim() != bot) + { + const float safeDistance = 6.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer && attumen->GetExactDist2d(nearestPlayer) < safeDistance) + return MoveFromGroup(safeDistance + 2.0f); + } + + return false; } -bool KarazhanAttumenTheHuntsmanStackBehindAction::Execute(Event event) +// Stack behind mounted Attumen (inside minimum range of Berserker Charge) +bool AttumenTheHuntsmanStackBehindAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); - - float distance = 5.0f; - float orientation = boss->GetOrientation() + M_PI; - float x = boss->GetPositionX(); - float y = boss->GetPositionY(); - float z = boss->GetPositionZ(); - float rx = x + cos(orientation) * distance; - float ry = y + sin(orientation) * distance; - - if (bot->GetExactDist2d(rx, ry) > 1.0f) + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return false; + + const float distanceBehind = botAI->IsRanged(bot) ? 6.0f : 2.0f; + float orientation = attumenMounted->GetOrientation() + M_PI; + float rearX = attumenMounted->GetPositionX() + std::cos(orientation) * distanceBehind; + float rearY = attumenMounted->GetPositionY() + std::sin(orientation) * distanceBehind; + + if (bot->GetExactDist2d(rearX, rearY) > 1.0f) { - return MoveTo(bot->GetMapId(), rx, ry, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, attumenMounted->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } return false; } -bool KarazhanAttumenTheHuntsmanStackBehindAction::isUseful() +// Reset timer for bots to pause DPS when Attumen mounts Midnight +bool AttumenTheHuntsmanManageDpsTimerAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return false; - return boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot); + if (midnight && midnight->GetHealth() == midnight->GetMaxHealth()) + attumenDpsWaitTimer.erase(KARAZHAN_MAP_ID); + + // Midnight is still present as a separate (invisible) unit after Attumen mounts + // So this block can be reached + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return false; + + const time_t now = std::time(nullptr); + + if (attumenMounted) + attumenDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + + return false; } -bool KarazhanMoroesMarkTargetAction::Execute(Event event) +// Moroes + +bool MoroesMainTankAttackBossAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); + Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes"); + if (!moroes) + return false; + MarkTargetWithCircle(bot, moroes); + SetRtiTarget(botAI, "circle", moroes); + + if (bot->GetVictim() != moroes) + return Attack(moroes); + + return false; +} + +// Mark targets with skull in the recommended kill order +bool MoroesMarkTargetAction::Execute(Event event) +{ Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); Unit* rafe = AI_VALUE2(Unit*, "find target", "baron rafe dreuger"); Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - Unit* target = karazhanHelper.GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); + Unit* target = GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); + + if (target) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithSkull(bot, target); - karazhanHelper.MarkTargetWithSkull(target); + SetRtiTarget(botAI, "skull", target); + } return false; } -bool KarazhanMaidenOfVirtuePositionBossAction::Execute(Event event) +// Maiden of Virtue + +// Tank the boss in the center of the room +// Move to healers after Repentenace to break the stun +bool MaidenOfVirtueMoveBossToHealerAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - Unit* healer = nullptr; + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + if (!maiden) + return false; + if (bot->GetVictim() != maiden) + return Attack(maiden); + + Unit* healer = nullptr; if (Group* group = bot->GetGroup()) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsHeal(member) || !member->HasAura(SPELL_REPENTANCE)) - { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsHeal(member) || + !member->HasAura(SPELL_REPENTANCE)) continue; - } + healer = member; break; } @@ -84,580 +218,534 @@ bool KarazhanMaidenOfVirtuePositionBossAction::Execute(Event event) if (healer) { float angle = healer->GetOrientation(); - float targetX = healer->GetPositionX() + cos(angle) * 6.0f; - float targetY = healer->GetPositionY() + sin(angle) * 6.0f; - float targetZ = healer->GetPositionZ(); + float targetX = healer->GetPositionX() + std::cos(angle) * 6.0f; + float targetY = healer->GetPositionY() + std::sin(angle) * 6.0f; { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + return MoveTo(KARAZHAN_MAP_ID, targetX, targetY, healer->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION); - - if (distanceToBossPosition > maxDistance) + const Position& position = MAIDEN_OF_VIRTUE_BOSS_POSITION; + const float maxDistance = 2.0f; + float distanceToPosition = maiden->GetExactDist2d(position); + if (distanceToPosition > maxDistance) { - float dX = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - float mX = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + float dX = position.GetPositionX() - maiden->GetPositionX(); + float dY = position.GetPositionY() - maiden->GetPositionY(); + float mX = position.GetPositionX() + (dX / distanceToPosition) * maxDistance; + float mY = position.GetPositionY() + (dY / distanceToPosition) * maxDistance; { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), mX, mY, - bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } return false; } -bool KarazhanMaidenOfVirtuePositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; -} - -bool KarazhanMaidenOfVirtuePositionRangedAction::Execute(Event event) +// Spread out ranged DPS between the pillars +bool MaidenOfVirtuePositionRangedAction::Execute(Event event) { - int maxIndex = 7; - int index = 0; - - const GuidVector members = AI_VALUE(GuidVector, "group members"); + const uint8 maxIndex = 7; + uint8 index = 0; - for (auto const& memberGuid : members) + if (Group* group = bot->GetGroup()) { - Unit* member = botAI->GetUnit(memberGuid); - - if (!member || !botAI->IsRanged(member->ToPlayer())) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; - } + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; - if (member == bot) - break; + if (member == bot) + break; - if (index >= maxIndex) - { - index = 0; - continue; + if (index >= maxIndex) + { + index = 0; + continue; + } + index++; } - index++; } - float distance = bot->GetExactDist2d(KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index]); - const float maxDistance = 2.0f; - if (distance > maxDistance) + const Position& position = MAIDEN_OF_VIRTUE_RANGED_POSITION[index]; + if (bot->GetExactDist2d(position) > 2.0f) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index].GetPositionX(), - KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index].GetPositionY(), bot->GetPositionZ(), false, - false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanMaidenOfVirtuePositionRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - - return boss && botAI->IsRanged(bot); -} +// The Big Bad Wolf -bool KarazhanBigBadWolfPositionBossAction::Execute(Event event) +// Tank the boss at the front left corner of the stage +bool BigBadWolfPositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + if (!wolf) + return false; - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION); + if (bot->GetVictim() != wolf) + return Attack(wolf); - if (distanceToBossPosition > maxDistance) + if (wolf->GetVictim() == bot) { - float dX = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); + const Position& position = BIG_BAD_WOLF_BOSS_POSITION; + const float maxStep = 2.0f; + float dist = wolf->GetExactDist2d(position); - float mX = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; - - float moveDistance = bot->GetExactDist2d(mX, mY); - if (moveDistance < 0.5f) + if (dist > 0.0f && dist > maxStep) { - return false; + float dX = position.GetPositionX() - wolf->GetPositionX(); + float dY = position.GetPositionY() - wolf->GetPositionY(); + float moveDist = std::min(maxStep, dist); + float moveX = wolf->GetPositionX() + (dX / dist) * moveDist; + float moveY = wolf->GetPositionY() + (dY / dist) * moveDist; + + return MoveTo(KARAZHAN_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } - - return MoveTo(bot->GetMapId(), mX, mY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanBigBadWolfPositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot && - !bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD); -} - -bool KarazhanBigBadWolfRunAwayAction::Execute(Event event) +// Run away, little girl, run away +bool BigBadWolfRunAwayFromBossAction::Execute(Event event) { - constexpr float threshold = 1.0f; - Position target = KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[currentIndex]; + const ObjectGuid botGuid = bot->GetGUID(); + uint8 index = bigBadWolfRunIndex.count(botGuid) ? bigBadWolfRunIndex[botGuid] : 0; - while (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) < threshold) + while (bot->GetExactDist2d(BIG_BAD_WOLF_RUN_POSITION[index].GetPositionX(), + BIG_BAD_WOLF_RUN_POSITION[index].GetPositionY()) < 1.0f) { - currentIndex = (currentIndex + 1) % 4; - target = KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[currentIndex]; + index = (index + 1) % 4; } + bigBadWolfRunIndex[botGuid] = index; bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + const Position& position = BIG_BAD_WOLF_RUN_POSITION[index]; + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } -bool KarazhanBigBadWolfRunAwayAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - return boss && bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD); -} +// Romulo and Julianne -bool KarazhanRomuloAndJulianneMarkTargetAction::Execute(Event event) +// Keep the couple within 10% HP of each other +bool RomuloAndJulianneMarkTargetAction::Execute(Event event) { - Unit* target = nullptr; Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); + if (!romulo) + return false; + Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!julianne) + return false; + + Unit* target = nullptr; + const float maxPctDifference = 10.0f; - const int maxPctDifference = 10; if (julianne->GetHealthPct() + maxPctDifference < romulo->GetHealthPct() || julianne->GetHealthPct() < 1.0f) - { target = romulo; - } else if (romulo->GetHealthPct() + maxPctDifference < julianne->GetHealthPct() || romulo->GetHealthPct() < 1.0f) - { target = julianne; - } - if (!target) - { - return false; - } + else + target = (romulo->GetHealthPct() >= julianne->GetHealthPct()) ? romulo : julianne; - RaidKarazhanHelpers karazhanHelper(botAI); - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanWizardOfOzMarkTargetAction::Execute(Event event) +// The Wizard of Oz + +// Mark targets with skull in the recommended kill order +bool WizardOfOzMarkTargetAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - Unit* target = karazhanHelper.GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); + Unit* target = GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanWizardOfOzScorchStrawmanAction::Execute(Event event) +// Mages spam Scorch on Strawman to disorient him +bool WizardOfOzScorchStrawmanAction::Execute(Event event) { Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); - if (!strawman || !strawman->IsAlive() || bot->getClass() != CLASS_MAGE) - { - return false; - } - - if (botAI->CanCastSpell("scorch", strawman)) - { - botAI->CastSpell("scorch", strawman); - } + if (strawman && botAI->CanCastSpell("scorch", strawman)) + return botAI->CastSpell("scorch", strawman); return false; } -bool KarazhanTheCuratorMarkTargetAction::Execute(Event event) +// The Curator + +// Prioritize destroying Astral Flares +bool TheCuratorMarkAstralFlareAction::Execute(Event event) { - Unit* target = AI_VALUE2(Unit*, "find target", "astral flare"); - if (!target || !target->IsAlive()) - { + Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); + if (!flare) return false; - } - RaidKarazhanHelpers karazhanHelper(botAI); - karazhanHelper.MarkTargetWithSkull(target); + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithSkull(bot, flare); + + SetRtiTarget(botAI, "skull", flare); return false; } -bool KarazhanTheCuratorPositionBossAction::Execute(Event event) +// Tank the boss in the center of the hallway near the Guardian's Library +// Main tank and off tank will attack the boss; others will focus on Astral Flares +bool TheCuratorPositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_THE_CURATOR_BOSS_POSITION); + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return false; + + MarkTargetWithCircle(bot, curator); + SetRtiTarget(botAI, "circle", curator); - if (distanceToBossPosition > maxDistance) + if (bot->GetVictim() != curator) + return Attack(curator); + + if (curator->GetVictim() == bot) { - float dX = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - float mX = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + const Position& position = THE_CURATOR_BOSS_POSITION; + const float maxDistance = 3.0f; + float distanceToBossPosition = curator->GetExactDist2d(position); + + if (distanceToBossPosition > maxDistance) { - return MoveTo(bot->GetMapId(), mX, mY, - bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, - false); + float dX = position.GetPositionX() - curator->GetPositionX(); + float dY = position.GetPositionY() - curator->GetPositionY(); + float mX = position.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; + float mY = position.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + + return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } return false; } -bool KarazhanTheCuratorPositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; -} - -bool KarazhanTheCuratorSpreadRangedAction::Execute(Event event) +// Spread out ranged DPS to avoid Arcing Sear damage +bool TheCuratorSpreadRangedAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); const float minDistance = 5.0f; - Unit* nearestPlayer = karazhanHelper.GetNearestPlayerInRadius(minDistance); + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance); if (nearestPlayer) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - + bot->InterruptNonMeleeSpells(true); return FleePosition(nearestPlayer->GetPosition(), minDistance); } return false; } -bool KarazhanTheCuratorSpreadRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - - return boss && botAI->IsRanged(bot); -} +// Terestian Illhoof -bool KarazhanTerestianIllhoofMarkTargetAction::Execute(Event event) +// Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof +bool TerestianIllhoofMarkTargetAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "terestian illhoof"); - if (!boss) - { - return false; - } + Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains"); + Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek"); + Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_DEMON_CHAINS); - if (!target || !target->IsAlive()) - { - target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_KILREK); - if (!target || !target->IsAlive()) - { - target = boss; - } - } - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanShadeOfAranArcaneExplosionRunAwayAction::Execute(Event event) +// Shade of Aran + +// Run to the edge of the room to avoid Arcane Explosion +bool ShadeOfAranRunAwayFromArcaneExplosionAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - const float safeDistance = 20.0f; - const float distance = bot->GetDistance2d(boss); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return false; + const float safeDistance = 20.0f; + float distance = bot->GetDistance2d(aran); if (distance < safeDistance) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveAway(boss, safeDistance - distance); + bot->InterruptNonMeleeSpells(true); + return MoveAway(aran, safeDistance - distance); } return false; } -bool KarazhanShadeOfAranArcaneExplosionRunAwayAction::isUseful() +// I will not move when Flame Wreath is cast or the raid blows up +bool ShadeOfAranStopMovingDuringFlameWreathAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - - return boss && boss->IsAlive() && boss->HasUnitState(UNIT_STATE_CASTING) && - boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION); -} + AI_VALUE(LastMovement&, "last movement").Set(nullptr); -bool KarazhanShadeOfAranFlameWreathStopMovementAction::Execute(Event event) -{ - RaidKarazhanHelpers karazhanHelper(botAI); - if (karazhanHelper.IsFlameWreathActive()) + if (bot->isMoving()) { - AI_VALUE(LastMovement&, "last movement").Set(nullptr); bot->GetMotionMaster()->Clear(); - if (bot->isMoving()) - { - bot->StopMoving(); - } + bot->StopMoving(); return true; } return false; } -bool KarazhanShadeOfAranMarkConjuredElementalAction::Execute(Event event) +// Mark Conjured Elementals with skull so DPS can burn them down +bool ShadeOfAranMarkConjuredElementalAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - Unit* target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_CONJURED_ELEMENTAL); - - if (!boss || !boss->IsAlive() || - !target || !target->IsAlive() || target->HasAura(SPELL_WARLOCK_BANISH)) - { - return false; - } + Unit* elemental = GetFirstAliveUnitByEntry(botAI, NPC_CONJURED_ELEMENTAL); - karazhanHelper.MarkTargetWithSkull(target); + if (elemental) + MarkTargetWithSkull(bot, elemental); return false; } -bool KarazhanShadeOfAranSpreadRangedAction::Execute(Event event) +// Don't get closer than 11 yards to Aran to avoid counterspell +// Don't get farther than 15 yards from Aran to avoid getting stuck in alcoves +bool ShadeOfAranRangedMaintainDistanceAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + const float minDist = 11.0f; + const float maxDist = 15.0f; + const float ringIncrement = M_PI / 8; + const float distIncrement = 0.5f; + + float bestX = 0, bestY = 0, bestMoveDist = std::numeric_limits::max(); + bool found = false; - const float maxBossDistance = 12.0f; - float bossDistance = bot->GetExactDist2d(boss); - if (bossDistance > maxBossDistance) + for (float dist = minDist; dist <= maxDist; dist += distIncrement) { - float dX = bot->GetPositionX() - boss->GetPositionX(); - float dY = bot->GetPositionY() - boss->GetPositionY(); - float length = std::sqrt(dX * dX + dY * dY); - dX /= length; - dY /= length; - float tX = boss->GetPositionX() + dX * maxBossDistance; - float tY = boss->GetPositionY() + dY * maxBossDistance; + for (float angle = 0; angle < 2 * M_PI; angle += ringIncrement) { - return MoveTo(bot->GetMapId(), tX, tY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + float x = aran->GetPositionX() + std::cos(angle) * dist; + float y = aran->GetPositionY() + std::sin(angle) * dist; + + bool tooClose = false; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + if (member->GetExactDist2d(x, y) < 3.0f) + { + tooClose = true; + break; + } + } + if (tooClose) + continue; + + float moveDist = bot->GetExactDist2d(x, y); + if (moveDist < bestMoveDist) + { + bestMoveDist = moveDist; + bestX = x; + bestY = y; + found = true; + } } } - const float minDistance = 5.0f; - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* nearestPlayer = karazhanHelper.GetNearestPlayerInRadius(minDistance); - if (nearestPlayer) + if (found && bestMoveDist > 0.5f) { - return FleePosition(nearestPlayer->GetPosition(), minDistance); + return MoveTo(KARAZHAN_MAP_ID, bestX, bestY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanShadeOfAranSpreadRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - RaidKarazhanHelpers karazhanHelper(botAI); - - return boss && boss->IsAlive() && botAI->IsRanged(bot) && !karazhanHelper.IsFlameWreathActive() && - !(boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)); -} +// Netherspite // One tank bot per phase will dance in and out of the red beam (5 seconds in, 5 seconds out) -// Tank bots will ignore void zones--their positioning is too important -bool KarazhanNetherspiteBlockRedBeamAction::Execute(Event event) +// Tank bots will ignore void zones--their positioning is too important to risk losing beam control +bool NetherspiteBlockRedBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + if (!redPortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingRedBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == redBlocker); - bool wasBlocking = wasBlockingRedBeam[botGuid]; - Position beamPos = karazhanHelper.GetPositionOnBeam(boss, redPortal, 18.0f); + auto it = _wasBlockingRedBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingRedBeam.end()) ? it->second : false; + + Position beamPos = GetPositionOnBeam(netherspite, redPortal, 18.0f); if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_red", "%player is moving to block the red beam!", ph); + "netherspite_beam_blocking_red", "%player is moving to block the red beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingRedBeam[botGuid] = true; + _wasBlockingRedBeam[botGuid] = true; - uint32 intervalSecs = 5; - - if (beamMoveTimes[botGuid] == 0) - { - beamMoveTimes[botGuid] = time(nullptr); - lastBeamMoveSideways[botGuid] = false; - } - if (time(nullptr) - beamMoveTimes[botGuid] >= intervalSecs) + const uint8 intervalSecs = 5; + if (std::time(nullptr) - redBeamMoveTimer[botGuid] >= intervalSecs) { lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid]; - beamMoveTimes[botGuid] = time(nullptr); + redBeamMoveTimer[botGuid] = std::time(nullptr); } if (!lastBeamMoveSideways[botGuid]) - { - return MoveTo(bot->GetMapId(), beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } + return MoveTo(KARAZHAN_MAP_ID, beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); else { - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float px = redPortal->GetPositionX(); - float py = redPortal->GetPositionY(); - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(redPortal); if (length == 0.0f) - { return false; - } - dx /= length; - dy /= length; + float dx = (redPortal->GetPositionX() - netherspite->GetPositionX()) / length; + float dy = (redPortal->GetPositionY() - netherspite->GetPositionY()) / length; float perpDx = -dy; float perpDy = dx; float sideX = beamPos.GetPositionX() + perpDx * 3.0f; float sideY = beamPos.GetPositionY() + perpDy * 3.0f; float sideZ = beamPos.GetPositionZ(); - return MoveTo(bot->GetMapId(), sideX, sideY, sideZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); + return MoveTo(KARAZHAN_MAP_ID, sideX, sideY, sideZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } - wasBlockingRedBeam[botGuid] = false; + _wasBlockingRedBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockRedBeamAction::isUseful() +Position NetherspiteBlockRedBeamAction::GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); - - ObjectGuid botGuid = bot->GetGUID(); - static std::map lastBossBanishState; - bool bossIsBanished = boss && boss->HasAura(SPELL_NETHERSPITE_BANISHED); - - if (lastBossBanishState[botGuid] != bossIsBanished) - { - if (!bossIsBanished) - { - beamMoveTimes[botGuid] = 0; - lastBeamMoveSideways[botGuid] = false; - } - lastBossBanishState[botGuid] = bossIsBanished; - } - - return boss && redPortal && !bossIsBanished; + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); + float px = portal->GetPositionX(); + float py = portal->GetPositionY(); + + float dx = px - bx; + float dy = py - by; + float length = netherspite->GetExactDist2d(px, py); + if (length == 0.0f) + return Position(bx, by, bz); + + dx /= length; + dy /= length; + float targetX = bx + dx * distanceFromBoss; + float targetY = by + dy * distanceFromBoss; + float targetZ = bz; + + return Position(targetX, targetY, targetZ); } -// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 26 debuff stacks) +// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 25 debuff stacks) // When avoiding void zones, blocking bots will move along the beam to continue blocking -bool KarazhanNetherspiteBlockBlueBeamAction::Execute(Event event) +bool NetherspiteBlockBlueBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + if (!bluePortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingBlueBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == blueBlocker); - bool wasBlocking = wasBlockingBlueBeam[botGuid]; + + auto it = _wasBlockingBlueBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingBlueBeam.end()) ? it->second : false; if (wasBlocking && !isBlockingNow) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", ph); + "netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", placeholders); bot->Yell(text, LANG_UNIVERSAL); - wasBlockingBlueBeam[botGuid] = false; - - return false; + _wasBlockingBlueBeam[botGuid] = false; } if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", ph); + "netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingBlueBeam[botGuid] = true; + _wasBlockingBlueBeam[botGuid] = true; + + float idealDistance = botAI->IsRanged(bot) ? 25.0f : 18.0f; + std::vector voidZones = GetAllVoidZones(botAI, bot); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); float px = bluePortal->GetPositionX(); float py = bluePortal->GetPositionY(); + float dx = px - bx; float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(bluePortal); if (length == 0.0f) - { return false; - } dx /= length; dy /= length; float bestDist = 150.0f; Position bestPos; bool found = false; + for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f) { float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - bool outsideAllVoidZones = true; - for (Unit* voidZone : voidZones) - { - float voidZoneDist = sqrt(pow(candidateX - voidZone->GetPositionX(), 2) + - pow(candidateY - voidZone->GetPositionY(), 2)); - if (voidZoneDist < 4.0f) - { - outsideAllVoidZones = false; - break; - } - } - if (!outsideAllVoidZones) - { + if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) continue; - } - float distToIdeal = fabs(dist - 18.0f); + + float distToIdeal = fabs(dist - idealDistance); if (!found || distToIdeal < bestDist) { bestDist = distToIdeal; @@ -665,490 +753,454 @@ bool KarazhanNetherspiteBlockBlueBeamAction::Execute(Event event) found = true; } } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; } - wasBlockingBlueBeam[botGuid] = false; + _wasBlockingBlueBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockBlueBeamAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); - - return boss && bluePortal && !boss->HasAura(SPELL_NETHERSPITE_BANISHED); -} - -// Two healer bots will block the green beam for each phase (swap at 26 debuff stacks) +// Two healer bots will block the green beam for each phase (swap at 25 debuff stacks) // OR one rogue or DPS warrior bot will block the green beam for an entire phase (if they begin the phase as the blocker) // When avoiding void zones, blocking bots will move along the beam to continue blocking -bool KarazhanNetherspiteBlockGreenBeamAction::Execute(Event event) +bool NetherspiteBlockGreenBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + if (!greenPortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingGreenBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == greenBlocker); - bool wasBlocking = wasBlockingGreenBeam[botGuid]; + + auto it = _wasBlockingGreenBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingGreenBeam.end()) ? it->second : false; if (wasBlocking && !isBlockingNow) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", ph); + "netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", placeholders); bot->Yell(text, LANG_UNIVERSAL); - wasBlockingGreenBeam[botGuid] = false; - - return false; + _wasBlockingGreenBeam[botGuid] = false; } if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_green", "%player is moving to block the green beam!", ph); + "netherspite_beam_blocking_green", "%player is moving to block the green beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingGreenBeam[botGuid] = true; + _wasBlockingGreenBeam[botGuid] = true; - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); + std::vector voidZones = GetAllVoidZones(botAI, bot); + + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); float px = greenPortal->GetPositionX(); float py = greenPortal->GetPositionY(); + float dx = px - bx; float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(greenPortal); if (length == 0.0f) - { return false; - } dx /= length; dy /= length; float bestDist = 150.0f; Position bestPos; bool found = false; + for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f) { float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - bool outsideAllVoidZones = true; - for (Unit* voidZone : voidZones) - { - float voidZoneDist = sqrt(pow(candidateX - voidZone->GetPositionX(), 2) + - pow(candidateY - voidZone->GetPositionY(), 2)); - if (voidZoneDist < 4.0f) - { - outsideAllVoidZones = false; - break; - } - } - if (!outsideAllVoidZones) - { - continue; - } - float distToIdeal = fabs(dist - 18.0f); - if (!found || distToIdeal < bestDist) + if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) + continue; + + float distToIdeal = fabs(dist - 18.0f); + if (!found || distToIdeal < bestDist) { bestDist = distToIdeal; bestPos = Position(candidateX, candidateY, candidateZ); found = true; } } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; } - wasBlockingGreenBeam[botGuid] = false; + _wasBlockingGreenBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockGreenBeamAction::isUseful() +// All bots not currently blocking a beam will avoid beams and void zones +bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; - return boss && greenPortal && !boss->HasAura(SPELL_NETHERSPITE_BANISHED); -} + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + std::vector voidZones = GetAllVoidZones(botAI, bot); + + bool nearVoidZone = !IsSafePosition(bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), voidZones, 4.0f); -// All bots not currently blocking a beam will avoid beams and void zones -bool KarazhanNetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - bool nearVoidZone = false; - for (Unit* vz : voidZones) - { - if (bot->GetExactDist2d(vz) < 4.0f) - { - nearVoidZone = true; - break; - } - } - struct BeamAvoid { Unit* portal; float minDist, maxDist; }; std::vector beams; Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + if (redPortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = redPortal->GetPositionX(), py = redPortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(redPortal); beams.push_back({redPortal, 0.0f, length}); } + if (bluePortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = bluePortal->GetPositionX(), py = bluePortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(bluePortal); beams.push_back({bluePortal, 0.0f, length}); } + if (greenPortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = greenPortal->GetPositionX(), py = greenPortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(greenPortal); beams.push_back({greenPortal, 0.0f, length}); } - bool nearBeam = false; - for (auto const& beam : beams) - { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = bot->GetPositionX() - bx, botdy = bot->GetPositionY() - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(bot->GetPositionX() - beamX, 2) + pow(bot->GetPositionY() - beamY, 2)); - if (distToBeam < 5.0f && t > beam.minDist && t < beam.maxDist) - { - nearBeam = true; - break; - } - } + + bool nearBeam = !IsAwayFromBeams(bot->GetPositionX(), bot->GetPositionY(), beams, netherspite); + if (!nearVoidZone && !nearBeam) - { return false; - } - const float minMoveDist = 2.0f, maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f; - float bossZ = boss->GetPositionZ(); + const float minMoveDist = 2.0f; + const float minMoveDistSq = minMoveDist * minMoveDist; + const float maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f; + float netherspiteZ = netherspite->GetPositionZ(); Position bestCandidate; - float bestDist = 0.0f; + float bestDistSq = std::numeric_limits::max(); bool found = false; + + const float botX = bot->GetPositionX(); + const float botY = bot->GetPositionY(); + for (float angle = 0; angle < 2 * M_PI; angle += stepAngle) { for (float dist = 2.0f; dist <= maxSearchDist; dist += stepDist) { - float cx = bot->GetPositionX() + cos(angle) * dist; - float cy = bot->GetPositionY() + sin(angle) * dist; - float cz = bossZ; - if (std::any_of(voidZones.begin(), voidZones.end(), [&](Unit* vz){ return Position(cx, cy, cz).GetExactDist2d(vz) < 4.0f; })) - { - continue; - } - bool tooCloseToBeam = false; - for (auto const& beam : beams) - { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = cx - bx, botdy = cy - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(cx - beamX, 2) + pow(cy - beamY, 2)); - if (distToBeam < 5.0f && t > beam.minDist && t < beam.maxDist) - { - tooCloseToBeam = true; - break; - } - } - if (tooCloseToBeam) - { + float cx = botX + std::cos(angle) * dist; + float cy = botY + std::sin(angle) * dist; + float cz = netherspiteZ; + + if (!IsSafePosition(cx, cy, cz, voidZones, 4.0f) || + !IsAwayFromBeams(cx, cy, beams, netherspite)) continue; - } - float moveDist = sqrt(pow(cx - bot->GetPositionX(), 2) + pow(cy - bot->GetPositionY(), 2)); - if (moveDist < minMoveDist) - { + + float dx = cx - botX; + float dy = cy - botY; + float moveDistSq = dx*dx + dy*dy; + if (moveDistSq < minMoveDistSq) continue; - } - if (!found || moveDist < bestDist) + + if (!found || moveDistSq < bestDistSq) { bestCandidate = Position(cx, cy, cz); - bestDist = moveDist; + bestDistSq = moveDistSq; found = true; } } } - if (found && karazhanHelper.IsSafePosition(bestCandidate.GetPositionX(), - bestCandidate.GetPositionY(), bestCandidate.GetPositionZ(), - voidZones, 4.0f)) + + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestCandidate.GetPositionX(), bestCandidate.GetPositionY(), - bestCandidate.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestCandidate.GetPositionX(), bestCandidate.GetPositionY(), + bestCandidate.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanNetherspiteAvoidBeamAndVoidZoneAction::isUseful() +bool NetherspiteAvoidBeamAndVoidZoneAction::IsAwayFromBeams( + float x, float y, const std::vector& beams, Unit* netherspite) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || boss->HasAura(SPELL_NETHERSPITE_BANISHED)) + for (auto const& beam : beams) { - return false; - } + float bx = netherspite->GetPositionX(), by = netherspite->GetPositionY(); + float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); + float dx = px - bx, dy = py - by; + float length = netherspite->GetExactDist2d(beam.portal); - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - if (bot == redBlocker || bot == blueBlocker || bot == greenBlocker) - { - return false; + if (length == 0.0f) + continue; + + dx /= length; dy /= length; + float botdx = x - bx, botdy = y - by; + float distanceAlongBeam = (botdx * dx + botdy * dy); + float beamX = bx + dx * distanceAlongBeam, beamY = by + dy * distanceAlongBeam; + float distToBeam = sqrt((x - beamX) * (x - beamX) + (y - beamY) * (y - beamY)); + + if (distToBeam < 5.0f && distanceAlongBeam > beam.minDist && distanceAlongBeam < beam.maxDist) + return false; } return true; } -bool KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event) +bool NetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); + std::vector voidZones = GetAllVoidZones(botAI, bot); for (Unit* vz : voidZones) { if (vz->GetEntry() == NPC_VOID_ZONE && bot->GetExactDist2d(vz) < 4.0f) - { return FleePosition(vz->GetPosition(), 4.0f); - } } return false; } -bool KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction::isUseful() +bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->HasAura(SPELL_NETHERSPITE_BANISHED)) - { + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) return false; + + const ObjectGuid botGuid = bot->GetGUID(); + const time_t now = std::time(nullptr); + + // DpsWaitTimer is for pausing DPS during phase transitions + // redBeamMoveTimer and lastBeamMoveSideways are for tank dancing in/out of the red beam + if (netherspite->GetHealth() == netherspite->GetMaxHealth() && + !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) + { + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.insert_or_assign(KARAZHAN_MAP_ID, now); + + if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) + { + redBeamMoveTimer.erase(botGuid); + lastBeamMoveSideways.erase(botGuid); + } } + else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + { + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.erase(KARAZHAN_MAP_ID); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - for (Unit* vz : voidZones) + if (botAI->IsTank(bot)) + { + redBeamMoveTimer.erase(botGuid); + lastBeamMoveSideways.erase(botGuid); + } + } + else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (bot->GetExactDist2d(vz) < 4.0f) + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + + if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) { - return true; + redBeamMoveTimer.try_emplace(botGuid, now); + lastBeamMoveSideways.try_emplace(botGuid, false); } } return false; } -bool KarazhanPrinceMalchezaarNonTankAvoidHazardAction::Execute(Event event) +// Move away from the boss to avoid Shadow Nova when Enfeebled +// Do not cross within Infernal Hellfire radius while doing so +bool PrinceMalchezaarEnfeebledAvoidHazardAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector infernals = karazhanHelper.GetSpawnedInfernals(); + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; + + std::vector infernals = GetSpawnedInfernals(botAI); const float minSafeBossDistance = 34.0f; + const float minSafeBossDistanceSq = minSafeBossDistance * minSafeBossDistance; const float maxSafeBossDistance = 60.0f; const float safeInfernalDistance = 23.0f; - const float stepSize = 0.5f; - const int numAngles = 64; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float distIncrement = 0.5f; + const uint8 numAngles = 64; + float bx = bot->GetPositionX(); float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); - float bossX = boss->GetPositionX(); - float bossY = boss->GetPositionY(); - float bossZ = boss->GetPositionZ(); - float bestMoveDist = std::numeric_limits::max(); + float malchezaarX = malchezaar->GetPositionX(); + float malchezaarY = malchezaar->GetPositionY(); + float malchezaarZ = malchezaar->GetPositionZ(); + float bestMoveDistSq = std::numeric_limits::max(); float bestDestX = 0.0f, bestDestY = 0.0f, bestDestZ = bz; bool found = false; - if (bot->HasAura(SPELL_ENFEEBLE)) + for (int i = 0; i < numAngles; ++i) { - for (int i = 0; i < numAngles; ++i) + float angle = (2 * M_PI * i) / numAngles; + float dx = std::cos(angle); + float dy = std::sin(angle); + + for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += distIncrement) { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += stepSize) + float x = malchezaarX + dx * dist; + float y = malchezaarY + dy * dist; + float destZ = malchezaarZ; + float destX = x, destY = y; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) + continue; + + float ddx = destX - malchezaarX; + float ddy = destY - malchezaarY; + float distFromBossSq = ddx*ddx + ddy*ddy; + if (distFromBossSq < minSafeBossDistanceSq) + continue; + + bool pathSafe = IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ), + infernals, safeInfernalDistance, distIncrement); + float mdx = destX - bx; + float mdy = destY - by; + float moveDistSq = mdx*mdx + mdy*mdy; + + if (pathSafe && moveDistSq < bestMoveDistSq) { - float x = bossX + dx * dist; - float y = bossY + dy * dist; - float destZ = bossZ; - float destX = x, destY = y, destZ2 = destZ; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ2, true)) - { - continue; - } - float distFromBoss = sqrt(pow(destX - bossX, 2) + pow(destY - bossY, 2)); - if (distFromBoss < minSafeBossDistance) - { - continue; - } - bool pathSafe = karazhanHelper.IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ2), - infernals, safeInfernalDistance, stepSize); - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (pathSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ2; - found = true; - } + bestMoveDistSq = moveDistSq; + bestDestX = destX; + bestDestY = destY; + bestDestZ = destZ; + found = true; } } - if (found) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + } - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - } + if (found) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} +// Move away from infernals while staying within range of the boss +// Prioritize finding a safe path to the new location, but will fallback to just finding a safe location if needed +bool PrinceMalchezaarNonTankAvoidInfernalAction::Execute(Event event) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) return false; - } - if (!bot->HasAura(SPELL_ENFEEBLE)) + std::vector infernals = GetSpawnedInfernals(botAI); + + const float safeInfernalDistance = 23.0f; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float maxSafeBossDistance = 35.0f; + + float bx = bot->GetPositionX(); + float by = bot->GetPositionY(); + float bz = bot->GetPositionZ(); + float malchezaarX = malchezaar->GetPositionX(); + float malchezaarY = malchezaar->GetPositionY(); + float malchezaarZ = malchezaar->GetPositionZ(); + + bool nearInfernal = false; + for (Unit* infernal : infernals) { - bool nearInfernal = false; - for (Unit* infernal : infernals) + float dx = bx - infernal->GetPositionX(); + float dy = by - infernal->GetPositionY(); + float infernalDistSq = dx*dx + dy*dy; + if (infernalDistSq < safeInfernalDistanceSq) { - float infernalDist = sqrt(pow(bx - infernal->GetPositionX(), 2) + pow(by - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - nearInfernal = true; - break; - } + nearInfernal = true; + break; } - if (nearInfernal) + } + + float bestDestX = bx, bestDestY = by, bestDestZ = bz; + bool found = false; + + if (nearInfernal) + { + const float distIncrement = 0.5f; + const uint8 numAngles = 64; + + // 1. Try to find a safe position with a safe path + found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance, + true, bestDestX, bestDestY, bestDestZ); + + // 2. Fallback: try to find a safe position (ignore path safety) + if (!found) { - float bestMoveDist = std::numeric_limits::max(); - float bestDestX = bx, bestDestY = by, bestDestZ = bz; - bool found = false; - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSafeBossDistance; dist += stepSize) - { - float x = bossX + dx * dist; - float y = bossY + dy * dist; - float destZ = bossZ; - float destX = x, destY = y, destZ2 = destZ; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bossX, bossY, bossZ, destX, destY, destZ2, true)) - { - continue; - } - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - if (!destSafe) - continue; - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ2; - found = true; - } - } - } - if (found) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance, + false, bestDestX, bestDestY, bestDestZ); + } - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } + if (found) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } return false; } -bool KarazhanPrinceMalchezaarNonTankAvoidHazardAction::isUseful() +// This is similar to the non-tank avoid infernal action, but the movement is based on the bot's location +// And the safe distance from infernals is larger to give melee more room to maneuver +bool PrinceMalchezaarMainTankMovementAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; - return boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot); -} + if (bot->GetVictim() != malchezaar) + return Attack(malchezaar); + + std::vector infernals = GetSpawnedInfernals(botAI); + + const float safeInfernalDistance = 30.0f; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float maxSampleDist = 75.0f; -bool KarazhanPrinceMalchezaarTankAvoidHazardAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector infernals = karazhanHelper.GetSpawnedInfernals(); - - const float safeInfernalDistance = 28.0f; - const float stepSize = 0.5f; - const int numAngles = 64; - const float maxSampleDist = 60.0f; float bx = bot->GetPositionX(); float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); @@ -1156,116 +1208,283 @@ bool KarazhanPrinceMalchezaarTankAvoidHazardAction::Execute(Event event) bool nearInfernal = false; for (Unit* infernal : infernals) { - float infernalDist = sqrt(pow(bx - infernal->GetPositionX(), 2) + pow(by - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) + float dx = bx - infernal->GetPositionX(); + float dy = by - infernal->GetPositionY(); + float infernalDistSq = dx*dx + dy*dy; + if (infernalDistSq < safeInfernalDistanceSq) { nearInfernal = true; break; } } - float bestMoveDist = std::numeric_limits::max(); float bestDestX = bx, bestDestY = by, bestDestZ = bz; bool found = false; if (nearInfernal) { - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) - { - float x = bx + dx * dist; - float y = by + dy * dist; - float z = bz; - - float destX = x, destY = y, destZ = z; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) - continue; + const float distIncrement = 0.5f; + const uint8 numAngles = 64; - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - if (!destSafe) - continue; + // 1. Try to find a safe position with a safe path + found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist, + true, bestDestX, bestDestY, bestDestZ); - bool pathSafe = karazhanHelper.IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ), - infernals, safeInfernalDistance, stepSize); - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (pathSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ; - found = true; - } - } - } + // 2. Fallback: try to find a safe position (ignore path safety) if (!found) { - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) - { - float x = bx + dx * dist; - float y = by + dy * dist; - float z = bz; - - float destX = x, destY = y, destZ = z; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) - continue; - - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (destSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ; - found = true; - } - } - } + found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist, + false, bestDestX, bestDestY, bestDestZ); } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +// The tank position is near the Southeastern area of the Master's Terrace +// The tank moves Nightbane into position in two steps to try to get Nightbane to face sideways to the raid +bool NightbaneGroundPhasePositionBossAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + MarkTargetWithSkull(bot, nightbane); + + if (bot->GetVictim() != nightbane) + return Attack(nightbane); + + const ObjectGuid botGuid = bot->GetGUID(); + uint8 step = nightbaneTankStep.count(botGuid) ? nightbaneTankStep[botGuid] : 0; + + if (nightbane->GetVictim() == bot) + { + const Position tankPositions[2] = + { + NIGHTBANE_TRANSITION_BOSS_POSITION, + NIGHTBANE_FINAL_BOSS_POSITION + }; + const Position& position = tankPositions[step]; + const float maxDistance = 0.5f; + float distanceToTarget = bot->GetExactDist2d(position); - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + if ((distanceToTarget > maxDistance) && bot->IsWithinMeleeRange(nightbane)) + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); + + if (step == 0 && distanceToTarget <= maxDistance) + nightbaneTankStep[botGuid] = 1; + + if (step == 1 && distanceToTarget <= maxDistance) + { + float orientation = atan2(nightbane->GetPositionY() - bot->GetPositionY(), + nightbane->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); } } return false; } -bool KarazhanPrinceMalchezaarTankAvoidHazardAction::isUseful() +// Ranged bots rotate between 3 positions to avoid standing in Charred Earth, which lasts for +// 30s and has a minimum cooldown of 18s (so there can be 2 active at once) +// Ranged positions are near the Northeastern door to the tower +bool NightbaneGroundPhaseRotateRangedPositionsAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + const ObjectGuid botGuid = bot->GetGUID(); + uint8 index = nightbaneRangedStep.count(botGuid) ? nightbaneRangedStep[botGuid] : 0; + + const Position rangedPositions[3] = + { + NIGHTBANE_RANGED_POSITION1, + NIGHTBANE_RANGED_POSITION2, + NIGHTBANE_RANGED_POSITION3 + }; + const Position& position = rangedPositions[index]; + const float maxDistance = 2.0f; + float distanceToTarget = bot->GetExactDist2d(position); + + if (distanceToTarget <= maxDistance && + bot->HasAura(SPELL_CHARRED_EARTH) && !bot->HasAura(SPELL_BELLOWING_ROAR)) + { + index = (index + 1) % 3; + nightbaneRangedStep[botGuid] = index; + const Position& newPosition = rangedPositions[index]; + float newDistanceToTarget = bot->GetExactDist2d(newPosition); + if (newDistanceToTarget > maxDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, newPosition.GetPositionX(), newPosition.GetPositionY(), newPosition.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + return false; + } - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; + if (distanceToTarget > maxDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// For countering Bellowing Roars during the ground phase +bool NightbaneCastFearWardOnMainTankAction::Execute(Event event) +{ + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Put pets on passive during the flight phase so they don't try to chase Nightbane off the map +bool NightbaneControlPetAggressionAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Pet* pet = bot->GetPet(); + if (!pet) + return false; + + if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z && pet->GetReactState() == REACT_PASSIVE) + pet->SetReactState(REACT_DEFENSIVE); + + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z && pet->GetReactState() != REACT_PASSIVE) + { + pet->AttackStop(); + pet->SetReactState(REACT_PASSIVE); + } + + return false; +} + +// 1. Stack at the "Flight Stack Position" near Nightbane so he doesn't use Fireball Barrage +// 2. Once Rain of Bones hits, the whole party moves to a new stack position +// This action lasts for the first 35 seconds of the flight phase, after which Nightbane gets +// ready to land, and the player will need to lead the bots over near the ground phase position +bool NightbaneFlightPhaseMovementAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + return false; + + MarkTargetWithMoon(bot, nightbane); + + Unit* botTarget = botAI->GetUnit(bot->GetTarget()); + if (botTarget && botTarget == nightbane) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + } + + const ObjectGuid botGuid = bot->GetGUID(); + bool hasRainOfBones = bot->HasAura(SPELL_RAIN_OF_BONES); + + if (hasRainOfBones) + nightbaneRainOfBonesHit[botGuid] = true; + + float destX, destY, destZ; + if (nightbaneRainOfBonesHit[botGuid]) + { + destX = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionX(); + destY = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionY(); + destZ = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionZ(); + } + else + { + destX = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionX(); + destY = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionY(); + destZ = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionZ(); + } + + if (bot->GetExactDist2d(destX, destY) > 2.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, destX, destY, destZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool NightbaneManageTimersAndTrackersAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + const ObjectGuid botGuid = bot->GetGUID(); + const time_t now = std::time(nullptr); + + // Erase DPS wait timer and tank and ranged position tracking on encounter reset + if (nightbane->GetHealth() == nightbane->GetMaxHealth()) + { + if (botAI->IsMainTank(bot)) + nightbaneTankStep.erase(botGuid); + + if (botAI->IsRanged(bot)) + nightbaneRangedStep.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + nightbaneDpsWaitTimer.erase(KARAZHAN_MAP_ID); + } + // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer + else if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + { + nightbaneRainOfBonesHit.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + { + nightbaneFlightPhaseStartTimer.erase(KARAZHAN_MAP_ID); + nightbaneDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + } + } + // Erase DPS wait timer and tank and ranged position tracking and start flight phase timer + // at beginning of flight phase + else if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + { + if (botAI->IsMainTank(bot)) + nightbaneTankStep.erase(botGuid); + + if (botAI->IsRanged(bot)) + nightbaneRangedStep.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + { + nightbaneDpsWaitTimer.erase(KARAZHAN_MAP_ID); + nightbaneFlightPhaseStartTimer.try_emplace(KARAZHAN_MAP_ID, now); + } + } + + return false; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanActions.h b/src/strategy/raids/karazhan/RaidKarazhanActions.h index 4ab24ed94f..2d50944cd3 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActions.h +++ b/src/strategy/raids/karazhan/RaidKarazhanActions.h @@ -2,217 +2,322 @@ #define _PLAYERBOT_RAIDKARAZHANACTIONS_H #include "Action.h" +#include "AttackAction.h" #include "MovementActions.h" -class KarazhanAttumenTheHuntsmanStackBehindAction : public MovementAction +class ManaWarpStunCreatureBeforeWarpBreachAction : public AttackAction { public: - KarazhanAttumenTheHuntsmanStackBehindAction(PlayerbotAI* botAI, std::string const name = "karazhan attumen the huntsman stack behind") : MovementAction(botAI, name) {} + ManaWarpStunCreatureBeforeWarpBreachAction( + PlayerbotAI* botAI, std::string const name = "mana warp stun creature before warp breach") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanMoroesMarkTargetAction : public Action +class AttumenTheHuntsmanMarkTargetAction : public AttackAction { public: - KarazhanMoroesMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan moroes mark target") : Action(botAI, name) {} - + AttumenTheHuntsmanMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman mark target") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanMaidenOfVirtuePositionBossAction : public MovementAction +class AttumenTheHuntsmanSplitBossesAction : public AttackAction { public: - KarazhanMaidenOfVirtuePositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan maiden of virtue position boss") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanSplitBossesAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman split bosses") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanMaidenOfVirtuePositionRangedAction : public MovementAction +class AttumenTheHuntsmanStackBehindAction : public MovementAction { public: - KarazhanMaidenOfVirtuePositionRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan maiden of virtue position ranged") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanStackBehindAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman stack behind") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanBigBadWolfPositionBossAction : public MovementAction +class AttumenTheHuntsmanManageDpsTimerAction : public Action { public: - KarazhanBigBadWolfPositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan big bad wolf position boss") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanManageDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman manage dps timer") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanBigBadWolfRunAwayAction : public MovementAction +class MoroesMainTankAttackBossAction : public AttackAction { public: - KarazhanBigBadWolfRunAwayAction(PlayerbotAI* botAI, std::string const name = "karazhan big bad wolf run away") : MovementAction(botAI, name) {} - + MoroesMainTankAttackBossAction( + PlayerbotAI* botAI, std::string const name = "moroes main tank attack boss") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; - -private: - size_t currentIndex = 0; }; -class KarazhanRomuloAndJulianneMarkTargetAction : public Action +class MoroesMarkTargetAction : public Action { public: - KarazhanRomuloAndJulianneMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan romulo and julianne mark target") : Action(botAI, name) {} - + MoroesMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "moroes mark target") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanWizardOfOzMarkTargetAction : public Action +class MaidenOfVirtueMoveBossToHealerAction : public AttackAction { public: - KarazhanWizardOfOzMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan wizard of oz mark target") : Action(botAI, name) {} - + MaidenOfVirtueMoveBossToHealerAction( + PlayerbotAI* botAI, std::string const name = "maiden of virtue move boss to healer") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanWizardOfOzScorchStrawmanAction : public Action +class MaidenOfVirtuePositionRangedAction : public MovementAction { public: - KarazhanWizardOfOzScorchStrawmanAction(PlayerbotAI* botAI, std::string const name = "karazhan wizard of oz scorch strawman") : Action(botAI, name) {} - + MaidenOfVirtuePositionRangedAction( + PlayerbotAI* botAI, std::string const name = "maiden of virtue position ranged") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanTheCuratorMarkTargetAction : public Action +class BigBadWolfPositionBossAction : public AttackAction { public: - KarazhanTheCuratorMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator mark target") : Action(botAI, name) {} - + BigBadWolfPositionBossAction( + PlayerbotAI* botAI, std::string const name = "big bad wolf position boss") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanTheCuratorPositionBossAction : public MovementAction +class BigBadWolfRunAwayFromBossAction : public MovementAction { public: - KarazhanTheCuratorPositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator position boss") : MovementAction(botAI, name) {} - + BigBadWolfRunAwayFromBossAction( + PlayerbotAI* botAI, std::string const name = "big bad wolf run away from boss") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanTheCuratorSpreadRangedAction : public MovementAction +class RomuloAndJulianneMarkTargetAction : public Action { public: - KarazhanTheCuratorSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator spread ranged") : MovementAction(botAI, name) {} + RomuloAndJulianneMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "romulo and julianne mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class WizardOfOzMarkTargetAction : public Action +{ +public: + WizardOfOzMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "wizard of oz mark target") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanTerestianIllhoofMarkTargetAction : public Action +class WizardOfOzScorchStrawmanAction : public Action { public: - KarazhanTerestianIllhoofMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan terestian illhoof mark target") : Action(botAI, name) {} + WizardOfOzScorchStrawmanAction( + PlayerbotAI* botAI, std::string const name = "wizard of oz scorch strawman") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class TheCuratorMarkAstralFlareAction : public Action +{ +public: + TheCuratorMarkAstralFlareAction( + PlayerbotAI* botAI, std::string const name = "the curator mark astral flare") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranArcaneExplosionRunAwayAction : public MovementAction +class TheCuratorPositionBossAction : public AttackAction { public: - KarazhanShadeOfAranArcaneExplosionRunAwayAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran arcane explosion run away") : MovementAction(botAI, name) {} + TheCuratorPositionBossAction( + PlayerbotAI* botAI, std::string const name = "the curator position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; +class TheCuratorSpreadRangedAction : public MovementAction +{ +public: + TheCuratorSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "the curator spread ranged") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanShadeOfAranFlameWreathStopMovementAction : public MovementAction +class TerestianIllhoofMarkTargetAction : public Action { public: - KarazhanShadeOfAranFlameWreathStopMovementAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran flame wreath stop bot") : MovementAction(botAI, name) {} + TerestianIllhoofMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "terestian illhoof mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class ShadeOfAranRunAwayFromArcaneExplosionAction : public MovementAction +{ +public: + ShadeOfAranRunAwayFromArcaneExplosionAction( + PlayerbotAI* botAI, std::string const name = "shade of aran run away from arcane explosion") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranMarkConjuredElementalAction : public Action +class ShadeOfAranStopMovingDuringFlameWreathAction : public MovementAction { public: - KarazhanShadeOfAranMarkConjuredElementalAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran mark conjured elemental") : Action(botAI, name) {} + ShadeOfAranStopMovingDuringFlameWreathAction( + PlayerbotAI* botAI, std::string const name = "shade of aran stop moving during flame wreath") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class ShadeOfAranMarkConjuredElementalAction : public Action +{ +public: + ShadeOfAranMarkConjuredElementalAction( + PlayerbotAI* botAI, std::string const name = "shade of aran mark conjured elemental") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranSpreadRangedAction : public MovementAction +class ShadeOfAranRangedMaintainDistanceAction : public MovementAction { public: - KarazhanShadeOfAranSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran spread ranged") : MovementAction(botAI, name) {} + ShadeOfAranRangedMaintainDistanceAction( + PlayerbotAI* botAI, std::string const name = "shade of aran ranged maintain distance") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class NetherspiteBlockRedBeamAction : public MovementAction +{ +public: + NetherspiteBlockRedBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block red beam") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; + +private: + Position GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss); + std::unordered_map _wasBlockingRedBeam; }; -class KarazhanNetherspiteBlockRedBeamAction : public MovementAction +class NetherspiteBlockBlueBeamAction : public MovementAction { public: - KarazhanNetherspiteBlockRedBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block red beam") : MovementAction(botAI, name) {} + NetherspiteBlockBlueBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block blue beam") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +private: + std::unordered_map _wasBlockingBlueBeam; +}; + +class NetherspiteBlockGreenBeamAction : public MovementAction +{ +public: + NetherspiteBlockGreenBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block green beam") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; + +private: + std::unordered_map _wasBlockingGreenBeam; }; -class KarazhanNetherspiteBlockBlueBeamAction : public MovementAction +class NetherspiteAvoidBeamAndVoidZoneAction : public MovementAction { public: - KarazhanNetherspiteBlockBlueBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block blue beam") : MovementAction(botAI, name) {} + NetherspiteAvoidBeamAndVoidZoneAction( + PlayerbotAI* botAI, std::string const name = "netherspite avoid beam and void zone") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + struct BeamAvoid + { + Unit* portal; + float minDist, maxDist; + }; + bool IsAwayFromBeams(float x, float y, const std::vector& beams, Unit* netherspite); +}; +class NetherspiteBanishPhaseAvoidVoidZoneAction : public MovementAction +{ +public: + NetherspiteBanishPhaseAvoidVoidZoneAction( + PlayerbotAI* botAI, std::string const name = "netherspite banish phase avoid void zone") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBlockGreenBeamAction : public MovementAction +class NetherspiteManageTimersAndTrackersAction : public Action { public: - KarazhanNetherspiteBlockGreenBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block green beam") : MovementAction(botAI, name) {} + NetherspiteManageTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "netherspite manage timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class PrinceMalchezaarEnfeebledAvoidHazardAction : public MovementAction +{ +public: + PrinceMalchezaarEnfeebledAvoidHazardAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar enfeebled avoid hazard") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteAvoidBeamAndVoidZoneAction : public MovementAction +class PrinceMalchezaarNonTankAvoidInfernalAction : public MovementAction { public: - KarazhanNetherspiteAvoidBeamAndVoidZoneAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite avoid beam and void zone") : MovementAction(botAI, name) {} + PrinceMalchezaarNonTankAvoidInfernalAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar non tank avoid infernal") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class PrinceMalchezaarMainTankMovementAction : public AttackAction +{ +public: + PrinceMalchezaarMainTankMovementAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar main tank movement") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction : public MovementAction +class NightbaneGroundPhasePositionBossAction : public AttackAction { public: - KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite banish phase avoid void zone") : MovementAction(botAI, name) {} + NightbaneGroundPhasePositionBossAction( + PlayerbotAI* botAI, std::string const name = "nightbane ground phase position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; +class NightbaneGroundPhaseRotateRangedPositionsAction : public MovementAction +{ +public: + NightbaneGroundPhaseRotateRangedPositionsAction( + PlayerbotAI* botAI, std::string const name = "nightbane ground phase rotate ranged positions") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanPrinceMalchezaarNonTankAvoidHazardAction : public MovementAction +class NightbaneCastFearWardOnMainTankAction : public Action { public: - KarazhanPrinceMalchezaarNonTankAvoidHazardAction(PlayerbotAI* botAI, std::string const name = "karazhan prince malchezaar non-tank avoid hazard") : MovementAction(botAI, name) {} + NightbaneCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "nightbane cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class NightbaneControlPetAggressionAction : public Action +{ +public: + NightbaneControlPetAggressionAction( + PlayerbotAI* botAI, std::string const name = "nightbane control pet aggression") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanPrinceMalchezaarTankAvoidHazardAction : public MovementAction +class NightbaneFlightPhaseMovementAction : public MovementAction { public: - KarazhanPrinceMalchezaarTankAvoidHazardAction(PlayerbotAI* botAI, std::string const name = "karazhan prince malchezaar tank avoid hazard") : MovementAction(botAI, name) {} + NightbaneFlightPhaseMovementAction( + PlayerbotAI* botAI, std::string const name = "nightbane flight phase movement") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class NightbaneManageTimersAndTrackersAction : public Action +{ +public: + NightbaneManageTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "nightbane manage timers and trackers") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp b/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp index 296341746f..9ca9f7b300 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp @@ -1,316 +1,356 @@ -#include -#include - #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" -#include "AiObjectContext.h" -#include "PlayerbotMgr.h" -#include "Position.h" -#include "Spell.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" -const Position KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION = Position(-10945.881f, -2103.782f, 92.712f); -const Position KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[8] = -{ - { -10931.178f, -2116.580f, 92.179f }, - { -10925.828f, -2102.425f, 92.180f }, - { -10933.089f, -2088.5017f, 92.180f }, - { -10947.59f, -2082.8147f, 92.180f }, - { -10960.912f, -2090.4368f, 92.179f }, - { -10966.017f, -2105.288f, 92.175f }, - { -10959.242f, -2119.6172f, 92.180f }, - { -10944.495f, -2123.857f, 92.180f }, -}; - -const Position KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION = Position(-10913.391f, -1773.508f, 90.477f); -const Position KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[4] = +namespace KarazhanHelpers { - { -10875.456f, -1779.036f, 90.477f }, - { -10872.281f, -1751.638f, 90.477f }, - { -10910.492f, -1747.401f, 90.477f }, - { -10913.391f, -1773.508f, 90.477f }, -}; + // Attumen the Huntsman + std::unordered_map attumenDpsWaitTimer; + // Big Bad Wolf + std::unordered_map bigBadWolfRunIndex; + // Netherspite + std::unordered_map netherspiteDpsWaitTimer; + std::unordered_map redBeamMoveTimer; + std::unordered_map lastBeamMoveSideways; + // Nightbane + std::unordered_map nightbaneDpsWaitTimer; + std::unordered_map nightbaneTankStep; + std::unordered_map nightbaneRangedStep; + std::unordered_map nightbaneFlightPhaseStartTimer; + std::unordered_map nightbaneRainOfBonesHit; + + const Position MAIDEN_OF_VIRTUE_BOSS_POSITION = { -10945.881f, -2103.782f, 92.712f }; + const Position MAIDEN_OF_VIRTUE_RANGED_POSITION[8] = + { + { -10931.178f, -2116.580f, 92.179f }, + { -10925.828f, -2102.425f, 92.180f }, + { -10933.089f, -2088.502f, 92.180f }, + { -10947.590f, -2082.815f, 92.180f }, + { -10960.912f, -2090.437f, 92.179f }, + { -10966.017f, -2105.288f, 92.175f }, + { -10959.242f, -2119.617f, 92.180f }, + { -10944.495f, -2123.857f, 92.180f }, + }; + + const Position BIG_BAD_WOLF_BOSS_POSITION = { -10913.391f, -1773.508f, 90.477f }; + const Position BIG_BAD_WOLF_RUN_POSITION[4] = + { + { -10875.456f, -1779.036f, 90.477f }, + { -10872.281f, -1751.638f, 90.477f }, + { -10910.492f, -1747.401f, 90.477f }, + { -10913.391f, -1773.508f, 90.477f }, + }; + + const Position THE_CURATOR_BOSS_POSITION = { -11139.463f, -1884.645f, 165.765f }; + + const Position NIGHTBANE_TRANSITION_BOSS_POSITION = { -11160.646f, -1932.773f, 91.473f }; // near some ribs + const Position NIGHTBANE_FINAL_BOSS_POSITION = { -11173.530f, -1940.707f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION1 = { -11145.949f, -1970.927f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION2 = { -11143.594f, -1954.981f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION3 = { -11159.778f, -1961.031f, 91.473f }; + const Position NIGHTBANE_FLIGHT_STACK_POSITION = { -11159.555f, -1893.526f, 91.473f }; // Broken Barrel + const Position NIGHTBANE_RAIN_OF_BONES_POSITION = { -11165.233f, -1911.123f, 91.473f }; + + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) + { + if (!target) + return; -const Position KARAZHAN_THE_CURATOR_BOSS_POSITION = Position(-11139.463f, -1884.645f, 165.765f); + if (Group* group = bot->GetGroup()) + { + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } + } -void RaidKarazhanHelpers::MarkTargetWithSkull(Unit* target) -{ - if (!target) + void MarkTargetWithSkull(Player* bot, Unit* target) { - return; + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); } - if (Group* group = bot->GetGroup()) + void MarkTargetWithSquare(Player* bot, Unit* target) { - constexpr uint8_t skullIconId = 7; - ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); - - if (skullGuid != target->GetGUID()) - { - group->SetTargetIcon(skullIconId, bot->GetGUID(), target->GetGUID()); - } + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); } -} -Unit* RaidKarazhanHelpers::GetFirstAliveUnit(const std::vector& units) -{ - for (Unit* unit : units) + void MarkTargetWithStar(Player* bot, Unit* target) { - if (unit && unit->IsAlive()) - { - return unit; - } + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); } - return nullptr; -} + void MarkTargetWithCircle(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); + } -Unit* RaidKarazhanHelpers::GetFirstAliveUnitByEntry(uint32 entry) -{ - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + void MarkTargetWithMoon(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); + } - for (auto const& npcGuid : npcs) + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) { - Unit* unit = botAI->GetUnit(npcGuid); + if (!target) + return; - if (unit && unit->IsAlive() && unit->GetEntry() == entry) + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) { - return unit; + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); } } - return nullptr; -} - -Unit* RaidKarazhanHelpers::GetNearestPlayerInRadius(float radius) -{ - if (Group* group = bot->GetGroup()) + // Only one bot is needed to set/reset mapwide timers + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - - if (!member || !member->IsAlive() || member == bot) - { - continue; - } - - if (bot->GetExactDist2d(member) < radius) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - return member; + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) + return member == bot; } } + + return false; } - return nullptr; -} + Unit* GetFirstAliveUnit(const std::vector& units) + { + for (Unit* unit : units) + { + if (unit && unit->IsAlive()) + return unit; + } -bool RaidKarazhanHelpers::IsFlameWreathActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - Spell* currentSpell = boss ? boss->GetCurrentSpell(CURRENT_GENERIC_SPELL) : nullptr; - if (currentSpell && currentSpell->m_spellInfo && currentSpell->m_spellInfo->Id == SPELL_FLAME_WREATH) + return nullptr; + } + + Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) { - return true; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + return unit; + } + + return nullptr; } - if (Group* group = bot->GetGroup()) + Unit* GetNearestPlayerInRadius(Player* bot, float radius) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) - { - continue; - } - if (member->HasAura(SPELL_AURA_FLAME_WREATH)) + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) { - return true; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } } } - } - return false; -} + return nearestPlayer; + } -// Red beam blockers: tank bots, no Nether Exhaustion Red -std::vector RaidKarazhanHelpers::GetRedBlockers() -{ - std::vector redBlockers; - if (Group* group = bot->GetGroup()) + bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + Unit* aran = botAI->GetAiObjectContext()->GetValue("find target", "shade of aran")->Get(); + Spell* currentSpell = aran ? aran->GetCurrentSpell(CURRENT_GENERIC_SPELL) : nullptr; + + if (currentSpell && currentSpell->m_spellInfo && + currentSpell->m_spellInfo->Id == SPELL_FLAME_WREATH_CAST) + return true; + + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsTank(member) || !GET_PLAYERBOT_AI(member) || - member->HasAura(SPELL_NETHER_EXHAUSTION_RED)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_FLAME_WREATH_AURA)) + return true; } - redBlockers.push_back(member); } - } - return redBlockers; -} + return false; + } -// Blue beam blockers: non-Rogue/Warrior DPS bots, no Nether Exhaustion Blue and ≤25 stacks of Blue Beam debuff -std::vector RaidKarazhanHelpers::GetBlueBlockers() -{ - std::vector blueBlockers; - if (Group* group = bot->GetGroup()) + // Red beam blockers: tank bots, no Nether Exhaustion Red + std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + std::vector redBlockers; + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsTank(member) || !GET_PLAYERBOT_AI(member) || + member->HasAura(SPELL_NETHER_EXHAUSTION_RED)) + continue; + + redBlockers.push_back(member); } - bool isDps = botAI->IsDps(member); - bool isWarrior = member->getClass() == CLASS_WARRIOR; - bool isRogue = member->getClass() == CLASS_ROGUE; - bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_BLUE); - Aura* blueBuff = member->GetAura(SPELL_BLUE_BEAM_DEBUFF); - bool overStack = blueBuff && blueBuff->GetStackAmount() >= 26; - if (isDps && !isWarrior && !isRogue && !hasExhaustion && !overStack) + } + + return redBlockers; + } + + // Blue beam blockers: non-Rogue/Warrior DPS bots, no Nether Exhaustion Blue and <24 stacks of Blue Beam debuff + std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot) + { + std::vector blueBlockers; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - blueBlockers.push_back(member); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_BLUE); + Aura* blueBuff = member->GetAura(SPELL_BLUE_BEAM_DEBUFF); + bool overStack = blueBuff && blueBuff->GetStackAmount() >= 24; + + bool isDps = botAI->IsDps(member); + bool isWarrior = member->getClass() == CLASS_WARRIOR; + bool isRogue = member->getClass() == CLASS_ROGUE; + + if (isDps && !isWarrior && !isRogue && !hasExhaustion && !overStack) + blueBlockers.push_back(member); } } - } - return blueBlockers; -} + return blueBlockers; + } -// Green beam blockers: -// (1) Rogue and non-tank Warrior bots, no Nether Exhaustion Green -// (2) Healer bots, no Nether Exhaustion Green and ≤25 stacks of Green Beam debuff -std::vector RaidKarazhanHelpers::GetGreenBlockers() -{ - std::vector greenBlockers; - if (Group* group = bot->GetGroup()) + // Green beam blockers: + // (1) Prioritize Rogues and non-tank Warrior bots, no Nether Exhaustion Green + // (2) Then assign Healer bots, no Nether Exhaustion Green and <24 stacks of Green Beam debuff + std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + std::vector greenBlockers; + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); + bool isRogue = member->getClass() == CLASS_ROGUE; + bool isDpsWarrior = member->getClass() == CLASS_WARRIOR && botAI->IsDps(member); + bool eligibleRogueWarrior = (isRogue || isDpsWarrior) && !hasExhaustion; + + if (eligibleRogueWarrior) + greenBlockers.push_back(member); } - bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); - Aura* greenBuff = member->GetAura(SPELL_GREEN_BEAM_DEBUFF); - bool overStack = greenBuff && greenBuff->GetStackAmount() >= 26; - bool isRogue = member->getClass() == CLASS_ROGUE; - bool isDpsWarrior = member->getClass() == CLASS_WARRIOR && botAI->IsDps(member); - bool eligibleRogueWarrior = (isRogue || isDpsWarrior) && !hasExhaustion; - bool isHealer = botAI->IsHeal(member); - bool eligibleHealer = isHealer && !hasExhaustion && !overStack; - if (eligibleRogueWarrior || eligibleHealer) + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - greenBlockers.push_back(member); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); + Aura* greenBuff = member->GetAura(SPELL_GREEN_BEAM_DEBUFF); + bool overStack = greenBuff && greenBuff->GetStackAmount() >= 24; + bool isHealer = botAI->IsHeal(member); + bool eligibleHealer = isHealer && !hasExhaustion && !overStack; + + if (eligibleHealer) + greenBlockers.push_back(member); } } - } - - return greenBlockers; -} -Position RaidKarazhanHelpers::GetPositionOnBeam(Unit* boss, Unit* portal, float distanceFromBoss) -{ - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); - float px = portal->GetPositionX(); - float py = portal->GetPositionY(); - - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - return Position(bx, by, bz); + return greenBlockers; } - dx /= length; - dy /= length; - float targetX = bx + dx * distanceFromBoss; - float targetY = by + dy * distanceFromBoss; - float targetZ = bz; + std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot) + { + static ObjectGuid currentRedBlocker; + static ObjectGuid currentGreenBlocker; + static ObjectGuid currentBlueBlocker; - return Position(targetX, targetY, targetZ); -} + Player* redBlocker = nullptr; + Player* greenBlocker = nullptr; + Player* blueBlocker = nullptr; -std::tuple RaidKarazhanHelpers::GetCurrentBeamBlockers() -{ - static ObjectGuid currentRedBlocker; - static ObjectGuid currentGreenBlocker; - static ObjectGuid currentBlueBlocker; + std::vector redBlockers = GetRedBlockers(botAI, bot); + if (!redBlockers.empty()) + { + auto it = std::find_if(redBlockers.begin(), redBlockers.end(), [](Player* player) + { + return player && player->GetGUID() == currentRedBlocker; + }); - Player* redBlocker = nullptr; - Player* greenBlocker = nullptr; - Player* blueBlocker = nullptr; + if (it != redBlockers.end()) + redBlocker = *it; + else + redBlocker = redBlockers.front(); - std::vector redBlockers = GetRedBlockers(); - if (!redBlockers.empty()) - { - auto it = std::find_if(redBlockers.begin(), redBlockers.end(), [](Player* p) - { - return p && p->GetGUID() == currentRedBlocker; - }); - if (it != redBlockers.end()) - { - redBlocker = *it; + currentRedBlocker = redBlocker ? redBlocker->GetGUID() : ObjectGuid::Empty; } else { - redBlocker = redBlockers.front(); + currentRedBlocker = ObjectGuid::Empty; + redBlocker = nullptr; } - currentRedBlocker = redBlocker ? redBlocker->GetGUID() : ObjectGuid::Empty; - } - else - { - currentRedBlocker = ObjectGuid::Empty; - redBlocker = nullptr; - } - std::vector greenBlockers = GetGreenBlockers(); - if (!greenBlockers.empty()) - { - auto it = std::find_if(greenBlockers.begin(), greenBlockers.end(), [](Player* p) - { - return p && p->GetGUID() == currentGreenBlocker; - }); - if (it != greenBlockers.end()) + std::vector greenBlockers = GetGreenBlockers(botAI, bot); + if (!greenBlockers.empty()) { - greenBlocker = *it; + auto it = std::find_if(greenBlockers.begin(), greenBlockers.end(), [](Player* player) + { + return player && player->GetGUID() == currentGreenBlocker; + }); + + if (it != greenBlockers.end()) + greenBlocker = *it; + else + greenBlocker = greenBlockers.front(); + + currentGreenBlocker = greenBlocker ? greenBlocker->GetGUID() : ObjectGuid::Empty; } else { - greenBlocker = greenBlockers.front(); + currentGreenBlocker = ObjectGuid::Empty; + greenBlocker = nullptr; } - currentGreenBlocker = greenBlocker ? greenBlocker->GetGUID() : ObjectGuid::Empty; - } - else - { - currentGreenBlocker = ObjectGuid::Empty; - greenBlocker = nullptr; - } - std::vector blueBlockers = GetBlueBlockers(); + std::vector blueBlockers = GetBlueBlockers(botAI, bot); if (!blueBlockers.empty()) { - auto it = std::find_if(blueBlockers.begin(), blueBlockers.end(), [](Player* p) + auto it = std::find_if(blueBlockers.begin(), blueBlockers.end(), [](Player* player) { - return p && p->GetGUID() == currentBlueBlocker; + return player && player->GetGUID() == currentBlueBlocker; }); + if (it != blueBlockers.end()) - { blueBlocker = *it; - } else - { blueBlocker = blueBlockers.front(); - } + currentBlueBlocker = blueBlocker ? blueBlocker->GetGUID() : ObjectGuid::Empty; } else @@ -319,91 +359,132 @@ std::tuple RaidKarazhanHelpers::GetCurrentBeamBlocker blueBlocker = nullptr; } - return std::make_tuple(redBlocker, greenBlocker, blueBlocker); -} + return std::make_tuple(redBlocker, greenBlocker, blueBlocker); + } -std::vector RaidKarazhanHelpers::GetAllVoidZones() -{ - std::vector voidZones; - const float radius = 30.0f; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + std::vector GetAllVoidZones(PlayerbotAI* botAI, Player* bot) { - Unit* unit = botAI->GetUnit(npcGuid); - if (!unit || unit->GetEntry() != NPC_VOID_ZONE) - { - continue; - } - float dist = bot->GetExactDist2d(unit); - if (dist < radius) + std::vector voidZones; + const float radius = 30.0f; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) { - voidZones.push_back(unit); + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || unit->GetEntry() != NPC_VOID_ZONE) + continue; + + float dist = bot->GetExactDist2d(unit); + if (dist < radius) + voidZones.push_back(unit); } - } - return voidZones; -} + return voidZones; + } -bool RaidKarazhanHelpers::IsSafePosition(float x, float y, float z, - const std::vector& hazards, float hazardRadius) -{ - for (Unit* hazard : hazards) + bool IsSafePosition(float x, float y, float z, const std::vector& hazards, float hazardRadius) { - float dist = std::sqrt(std::pow(x - hazard->GetPositionX(), 2) + std::pow(y - hazard->GetPositionY(), 2)); - if (dist < hazardRadius) + for (Unit* hazard : hazards) { - return false; + float dist = hazard->GetExactDist2d(x, y); + if (dist < hazardRadius) + return false; } - } - return true; -} + return true; + } -std::vector RaidKarazhanHelpers::GetSpawnedInfernals() const -{ - std::vector infernals; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + std::vector GetSpawnedInfernals(PlayerbotAI* botAI) { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == NPC_NETHERSPITE_INFERNAL) + std::vector infernals; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) { - infernals.push_back(unit); + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_NETHERSPITE_INFERNAL) + infernals.push_back(unit); } - } - return infernals; -} + return infernals; + } -bool RaidKarazhanHelpers::IsStraightPathSafe(const Position& start, const Position& target, const std::vector& hazards, float hazardRadius, float stepSize) -{ - float sx = start.GetPositionX(); - float sy = start.GetPositionY(); - float sz = start.GetPositionZ(); - float tx = target.GetPositionX(); - float ty = target.GetPositionY(); - float tz = target.GetPositionZ(); - float totalDist = std::sqrt(std::pow(tx - sx, 2) + std::pow(ty - sy, 2)); - if (totalDist == 0.0f) + bool IsStraightPathSafe(const Position& start, const Position& target, const std::vector& hazards, + float hazardRadius, float stepSize) { + float sx = start.GetPositionX(); + float sy = start.GetPositionY(); + float sz = start.GetPositionZ(); + float tx = target.GetPositionX(); + float ty = target.GetPositionY(); + float tz = target.GetPositionZ(); + + const float totalDist = start.GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + if (totalDist == 0.0f) + return true; + + for (float checkDist = 0.0f; checkDist <= totalDist; checkDist += stepSize) + { + float t = checkDist / totalDist; + float checkX = sx + (tx - sx) * t; + float checkY = sy + (ty - sy) * t; + float checkZ = sz + (tz - sz) * t; + for (Unit* hazard : hazards) + { + const float hx = checkX - hazard->GetPositionX(); + const float hy = checkY - hazard->GetPositionY(); + if ((hx*hx + hy*hy) < hazardRadius * hazardRadius) + return false; + } + } + return true; } - for (float checkDist = 0.0f; checkDist <= totalDist; checkDist += stepSize) + bool TryFindSafePositionWithSafePath( + Player* bot, float originX, float originY, float originZ, float centerX, float centerY, float centerZ, + const std::vector& hazards, float safeDistance, float stepSize, uint8 numAngles, + float maxSampleDist, bool requireSafePath, float& bestDestX, float& bestDestY, float& bestDestZ) { - float t = checkDist / totalDist; - float checkX = sx + (tx - sx) * t; - float checkY = sy + (ty - sy) * t; - float checkZ = sz + (tz - sz) * t; - for (Unit* hazard : hazards) + float bestMoveDist = std::numeric_limits::max(); + bool found = false; + + for (int i = 0; i < numAngles; ++i) { - float hazardDist = std::sqrt(std::pow(checkX - hazard->GetPositionX(), 2) + std::pow(checkY - hazard->GetPositionY(), 2)); - if (hazardDist < hazardRadius) + float angle = (2.0f * M_PI * i) / numAngles; + float dx = cos(angle); + float dy = sin(angle); + + for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) { - return false; + float x = centerX + dx * dist; + float y = centerY + dy * dist; + float z = centerZ; + float destX = x, destY = y, destZ = z; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, centerX, centerY, centerZ, + destX, destY, destZ, true)) + continue; + + if (!IsSafePosition(destX, destY, destZ, hazards, safeDistance)) + continue; + + if (requireSafePath) + { + if (!IsStraightPathSafe(Position(originX, originY, originZ), Position(destX, destY, destZ), + hazards, safeDistance, stepSize)) + continue; + } + + const float moveDist = Position(originX, originY, originZ).GetExactDist2d(destX, destY); + if (moveDist < bestMoveDist) + { + bestMoveDist = moveDist; + bestDestX = destX; + bestDestY = destY; + bestDestZ = destZ; + found = true; + } } } - } - return true; + return found; + } } diff --git a/src/strategy/raids/karazhan/RaidKarazhanHelpers.h b/src/strategy/raids/karazhan/RaidKarazhanHelpers.h index d26d8501d1..fb08333a25 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanHelpers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanHelpers.h @@ -1,85 +1,136 @@ #ifndef _PLAYERBOT_RAIDKARAZHANHELPERS_H_ #define _PLAYERBOT_RAIDKARAZHANHELPERS_H_ +#include +#include + #include "AiObject.h" -#include "Playerbots.h" #include "Position.h" +#include "Unit.h" -enum KarazhanSpells +namespace KarazhanHelpers { - // Maiden of Virtue - SPELL_REPENTANCE = 29511, + enum KarazhanSpells + { + // Maiden of Virtue + SPELL_REPENTANCE = 29511, - // Opera Event - SPELL_LITTLE_RED_RIDING_HOOD = 30756, + // Opera Event + SPELL_LITTLE_RED_RIDING_HOOD = 30756, - // Shade of Aran - SPELL_FLAME_WREATH = 30004, - SPELL_AURA_FLAME_WREATH = 29946, - SPELL_ARCANE_EXPLOSION = 29973, - SPELL_WARLOCK_BANISH = 18647, // Rank 2 + // The Curator + SPELL_CURATOR_EVOCATION = 30254, - // Netherspite - SPELL_GREEN_BEAM_DEBUFF = 30422, - SPELL_BLUE_BEAM_DEBUFF = 30423, - SPELL_NETHER_EXHAUSTION_RED = 38637, - SPELL_NETHER_EXHAUSTION_GREEN = 38638, - SPELL_NETHER_EXHAUSTION_BLUE = 38639, - SPELL_NETHERSPITE_BANISHED = 39833, - - // Prince Malchezaar - SPELL_ENFEEBLE = 30843, -}; - -enum KarazhanNpcs -{ - // Attumen the Huntsman - NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Shade of Aran + SPELL_FLAME_WREATH_CAST = 30004, + SPELL_FLAME_WREATH_AURA = 29946, + SPELL_ARCANE_EXPLOSION = 29973, + + // Netherspite + SPELL_RED_BEAM_DEBUFF = 30421, // "Nether Portal - Perseverance" (player aura) + SPELL_GREEN_BEAM_DEBUFF = 30422, // "Nether Portal - Serenity" (player aura) + SPELL_BLUE_BEAM_DEBUFF = 30423, // "Nether Portal - Dominance" (player aura) + SPELL_GREEN_BEAM_HEAL = 30467, // "Nether Portal - Serenity" (Netherspite aura) + SPELL_NETHER_EXHAUSTION_RED = 38637, + SPELL_NETHER_EXHAUSTION_GREEN = 38638, + SPELL_NETHER_EXHAUSTION_BLUE = 38639, + SPELL_NETHERSPITE_BANISHED = 39833, // "Vortex Shade Black" + + // Prince Malchezaar + SPELL_ENFEEBLE = 30843, + + // Nightbane + SPELL_CHARRED_EARTH = 30129, + SPELL_BELLOWING_ROAR = 36922, + SPELL_RAIN_OF_BONES = 37091, + + // Warlock + SPELL_WARLOCK_BANISH = 18647, + + // Priest + SPELL_FEAR_WARD = 6346, + }; - // Terestian Illhoof - NPC_KILREK = 17229, - NPC_DEMON_CHAINS = 17248, + enum KarazhanNPCs + { + // Trash + NPC_SPECTRAL_RETAINER = 16410, + NPC_MANA_WARP = 16530, - // Shade of Aran - NPC_CONJURED_ELEMENTAL = 17167, + // Attumen the Huntsman + NPC_ATTUMEN_THE_HUNTSMAN = 15550, + NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Shade of Aran + NPC_CONJURED_ELEMENTAL = 17167, + + // Netherspite + NPC_VOID_ZONE = 16697, + NPC_GREEN_PORTAL = 17367, // "Nether Portal - Serenity " + NPC_BLUE_PORTAL = 17368, // "Nether Portal - Dominance " + NPC_RED_PORTAL = 17369, // "Nether Portal - Perseverance " + + // Prince Malchezaar + NPC_NETHERSPITE_INFERNAL = 17646, + }; + + const uint32 KARAZHAN_MAP_ID = 532; + const float NIGHTBANE_FLIGHT_Z = 95.0f; + + // Attumen the Huntsman + extern std::unordered_map attumenDpsWaitTimer; + // Big Bad Wolf + extern std::unordered_map bigBadWolfRunIndex; // Netherspite - NPC_VOID_ZONE = 16697, - NPC_RED_PORTAL = 17369, - NPC_BLUE_PORTAL = 17368, - NPC_GREEN_PORTAL = 17367, - - // Prince Malchezaar - NPC_NETHERSPITE_INFERNAL = 17646, -}; - -extern const Position KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION; -extern const Position KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[8]; -extern const Position KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION; -extern const Position KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[4]; -extern const Position KARAZHAN_THE_CURATOR_BOSS_POSITION; - -class RaidKarazhanHelpers : public AiObject -{ -public: - explicit RaidKarazhanHelpers(PlayerbotAI* botAI) : AiObject(botAI) {} - - void MarkTargetWithSkull(Unit* /*target*/); - Unit* GetFirstAliveUnit(const std::vector& /*units*/); - Unit* GetFirstAliveUnitByEntry(uint32 /*entry*/); - Unit* GetNearestPlayerInRadius(float /*radius*/ = 5.0f); - bool IsFlameWreathActive(); - Position GetPositionOnBeam(Unit* boss, Unit* portal, float distanceFromBoss); - std::vector GetRedBlockers(); - std::vector GetBlueBlockers(); - std::vector GetGreenBlockers(); - std::tuple GetCurrentBeamBlockers(); - std::vector GetAllVoidZones(); - bool IsSafePosition (float x, float y, float z, - const std::vector& hazards, float hazardRadius); - std::vector GetSpawnedInfernals() const; - bool IsStraightPathSafe(const Position& start, const Position& target, - const std::vector& hazards, float hazardRadius, float stepSize); -}; + extern std::unordered_map netherspiteDpsWaitTimer; + extern std::unordered_map redBeamMoveTimer; + extern std::unordered_map lastBeamMoveSideways; + // Nightbane + extern std::unordered_map nightbaneDpsWaitTimer; + extern std::unordered_map nightbaneTankStep; + extern std::unordered_map nightbaneRangedStep; + extern std::unordered_map nightbaneFlightPhaseStartTimer; + extern std::unordered_map nightbaneRainOfBonesHit; + + extern const Position MAIDEN_OF_VIRTUE_BOSS_POSITION; + extern const Position MAIDEN_OF_VIRTUE_RANGED_POSITION[8]; + extern const Position BIG_BAD_WOLF_BOSS_POSITION; + extern const Position BIG_BAD_WOLF_RUN_POSITION[4]; + extern const Position THE_CURATOR_BOSS_POSITION; + extern const Position NIGHTBANE_TRANSITION_BOSS_POSITION; + extern const Position NIGHTBANE_FINAL_BOSS_POSITION; + extern const Position NIGHTBANE_RANGED_POSITION1; + extern const Position NIGHTBANE_RANGED_POSITION2; + extern const Position NIGHTBANE_RANGED_POSITION3; + extern const Position NIGHTBANE_FLIGHT_STACK_POSITION; + extern const Position NIGHTBANE_RAIN_OF_BONES_POSITION; + + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); + void MarkTargetWithSkull(Player* bot, Unit* target); + void MarkTargetWithSquare(Player* bot, Unit* target); + void MarkTargetWithStar(Player* bot, Unit* target); + void MarkTargetWithCircle(Player* bot, Unit* target); + void MarkTargetWithMoon(Player* bot, Unit* target); + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot); + Unit* GetFirstAliveUnit(const std::vector& units); + Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); + Unit* GetNearestPlayerInRadius(Player* bot, float radius); + bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot); + std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot); + std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetAllVoidZones(PlayerbotAI *botAI, Player* bot); + bool IsSafePosition (float x, float y, float z, const std::vector& hazards, float hazardRadius); + std::vector GetSpawnedInfernals(PlayerbotAI* botAI); + bool IsStraightPathSafe( + const Position& start, const Position& target, + const std::vector& hazards, float hazardRadius, float stepSize); + bool TryFindSafePositionWithSafePath( + Player* bot, float originX, float originY, float originZ, float centerX, float centerY, float centerZ, + const std::vector& hazards, float safeDistance, float stepSize, uint8 numAngles, + float maxSampleDist, bool requireSafePath, float& bestDestX, float& bestDestY, float& bestDestZ); +} #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp index f68cbe8b57..cd64ea985b 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp @@ -1,265 +1,359 @@ #include "RaidKarazhanMultipliers.h" #include "RaidKarazhanActions.h" #include "RaidKarazhanHelpers.h" -#include "AiObjectContext.h" #include "AttackAction.h" -#include "DruidBearActions.h" -#include "DruidCatActions.h" +#include "ChooseTargetActions.h" +#include "DruidActions.h" +#include "FollowActions.h" +#include "GenericActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "Playerbots.h" +#include "PriestActions.h" +#include "ReachTargetActions.h" #include "RogueActions.h" -#include "WarriorActions.h" +#include "ShamanActions.h" -static bool IsChargeAction(Action* action) +using namespace KarazhanHelpers; + +// Keep tanks from jumping back and forth between Attumen and Midnight +float AttumenTheHuntsmanDisableTankAssistMultiplier::GetValue(Action* action) { - return dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return 1.0f; + + Unit* attumen = AI_VALUE2(Unit*, "find target", "attumen the huntsman"); + if (!attumen) + return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + return 1.0f; } -float KarazhanAttumenTheHuntsmanMultiplier::GetValue(Action* action) +// Try to get rid of jittering when bots are stacked behind Attumen +float AttumenTheHuntsmanStayStackedMultiplier::GetValue(Action* action) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); - if (boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot) && - (dynamic_cast(action) && - !dynamic_cast(action))) + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return 1.0f; + + if (!botAI->IsMainTank(bot) && attumenMounted->GetVictim() != bot) { - return 0.0f; + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } return 1.0f; } -float KarazhanBigBadWolfMultiplier::GetValue(Action* action) +// Give the main tank 8 seconds to grab aggro when Attumen mounts Midnight +// In reality it's shorter because it takes Attumen a few seconds to aggro after mounting +float AttumenTheHuntsmanWaitForDpsMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - if (!boss) - { + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) return 1.0f; - } - if (bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 8; + + auto it = attumenDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == attumenDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) { - if ((dynamic_cast(action) && !dynamic_cast(action)) || - (dynamic_cast(action))) + if (!botAI->IsMainTank(bot)) { - return 0.0f; + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } return 1.0f; } -float KarazhanShadeOfAranMultiplier::GetValue(Action* action) +// The assist tank should stay on the boss to be 2nd on aggro and tank Hateful Bolts +float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - if (!boss) - { + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Save Bloodlust/Heroism for Evocation (100% increased damage) +float TheCuratorDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return 1.0f; + + if (!curator->HasAura(SPELL_CURATOR_EVOCATION)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } - if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) + return 1.0f; +} + +// Don't charge back in when running from Arcane Explosion +float ShadeOfAranArcaneExplosionDisableChargeMultiplier::GetValue(Action* action) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return 1.0f; + + if (aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) { - if (IsChargeAction(action)) - { + if (dynamic_cast(action)) return 0.0f; - } - if (dynamic_cast(action)) + if (bot->GetDistance2d(aran) >= 20.0f) { - const float safeDistance = 20.0f; - if (bot->GetDistance2d(boss) >= safeDistance) - { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } } } - bool flameWreathActive = boss->HasAura(SPELL_FLAME_WREATH); - if (!flameWreathActive && bot->GetGroup()) - { - for (GroupReference* itr = bot->GetGroup()->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->HasAura(SPELL_AURA_FLAME_WREATH)) - { - flameWreathActive = true; - break; - } - } - } - if (flameWreathActive) + return 1.0f; +} + +// I will not move when Flame Wreath is cast or the raid blows up +float ShadeOfAranFlameWreathDisableMovementMultiplier::GetValue(Action* action) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return 1.0f; + + if (IsFlameWreathActive(botAI, bot)) { - if (dynamic_cast(action) || IsChargeAction(action)) - { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } } return 1.0f; } -float KarazhanNetherspiteBlueAndGreenBeamMultiplier::GetValue(Action* action) +// Try to rid of the jittering when blocking beams +float NetherspiteKeepBlockingBeamMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->IsAlive()) - { + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) return 1.0f; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + + if (bot == redBlocker) + { + if (dynamic_cast(action)) + return 0.0f; } - if (dynamic_cast(action) || dynamic_cast(action)) + if (bot == blueBlocker) { - return 0.0f; + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker /*unused*/, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - bool isBlocker = (bot == greenBlocker || bot == blueBlocker); - if (isBlocker) + if (bot == greenBlocker) { - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); - bool inBeam = false; - for (Unit* portal : {bluePortal, greenPortal}) - { - if (!portal) - { - continue; - } - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = portal->GetPositionX(), py = portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = bot->GetPositionX() - bx, botdy = bot->GetPositionY() - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(bot->GetPositionX() - beamX, 2) + pow(bot->GetPositionY() - beamY, 2)); - if (distToBeam < 0.3f && t > 0.0f && t < length) - { - inBeam = true; - break; - } - } - if (inBeam) + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Give tanks 5 seconds to get aggro during phase transitions +float NetherspiteWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 5; + + auto it = netherspiteDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == netherspiteDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + if (!botAI->IsTank(bot)) { - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - bool inVoidZone = false; - for (Unit* vz : voidZones) - { - if (bot->GetExactDist2d(vz) < 4.0f) - { - inVoidZone = true; - break; - } - } - if (!inVoidZone) - { - if (dynamic_cast(action) || IsChargeAction(action)) - { - return 0.0f; - } - } + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } + return 1.0f; +} + +// Disable standard "avoid aoe" strategy, which may interfere with scripted avoidance +float PrinceMalchezaarDisableAvoidAoeMultiplier::GetValue(Action* action) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + return 1.0f; } -float KarazhanNetherspiteRedBeamMultiplier::GetValue(Action* action) +// Don't run back into Shadow Nova when Enfeebled +float PrinceMalchezaarEnfeebleKeepDistanceMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->IsAlive()) - { + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) return 1.0f; + + if (bot->HasAura(SPELL_ENFEEBLE)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; } - if (dynamic_cast(action)) + return 1.0f; +} + +// Wait until Phase 3 to use Bloodlust/Heroism +float PrinceMalchezaarDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return 1.0f; + + if (malchezaar->GetHealthPct() > 30.0f) { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Pets tend to run out of bounds and cause skeletons to spawn off the map +// Pets also tend to pull adds from inside of the tower through the floor +// This multiplier DOES NOT impact Hunter and Warlock pets +// Hunter and Warlock pets are addressed in ControlPetAggressionAction +float NightbaneDisablePetsMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; + + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + { + if (dynamic_cast(action)) + return 0.0f; } - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker /*unused*/, blueBlocker /*unused*/] = karazhanHelper.GetCurrentBeamBlockers(); - static std::map beamMoveTimes; - static std::map lastBeamMoveSideways; - ObjectGuid botGuid = bot->GetGUID(); - Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); - if (bot == redBlocker && boss && redPortal) + return 1.0f; +} + +// Give the main tank 8 seconds to get aggro during phase transitions +float NightbaneWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 8; + + auto it = nightbaneDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == nightbaneDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) { - Position blockingPos = karazhanHelper.GetPositionOnBeam(boss, redPortal, 18.0f); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float px = redPortal->GetPositionX(); - float py = redPortal->GetPositionY(); - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length != 0.0f) + if (!botAI->IsMainTank(bot)) { - dx /= length; - dy /= length; - float perpDx = -dy; - float perpDy = dx; - Position sidewaysPos(blockingPos.GetPositionX() + perpDx * 3.0f, - blockingPos.GetPositionY() + perpDy * 3.0f, - blockingPos.GetPositionZ()); - - uint32 intervalSecs = 5; - if (beamMoveTimes[botGuid] == 0) - { - beamMoveTimes[botGuid] = time(nullptr); - lastBeamMoveSideways[botGuid] = false; - } - if (time(nullptr) - beamMoveTimes[botGuid] >= intervalSecs) - { - lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid]; - beamMoveTimes[botGuid] = time(nullptr); - } - Position targetPos = lastBeamMoveSideways[botGuid] ? sidewaysPos : blockingPos; - float distToTarget = bot->GetExactDist2d(targetPos.GetPositionX(), targetPos.GetPositionY()); - const float positionTolerance = 1.5f; - if (distToTarget < positionTolerance) - { - if (dynamic_cast(action) || IsChargeAction(action)) - { - return 0.0f; - } - } + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } return 1.0f; } -float KarazhanPrinceMalchezaarMultiplier::GetValue(Action* action) +// The "avoid aoe" strategy must be disabled for the main tank +// Otherwise, the main tank will spin Nightbane to avoid Charred Earth and wipe the raid +// It is also disabled for all bots during the flight phase +float NightbaneDisableAvoidAoeMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - if (!boss || !boss->IsAlive()) - { + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) return 1.0f; - } - if (dynamic_cast(action)) + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z || botAI->IsMainTank(bot)) { - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; } - if (botAI->IsMelee(bot) && bot->HasAura(SPELL_ENFEEBLE) && - !dynamic_cast(action)) - { + return 1.0f; +} + +// Disable some movement actions that conflict with the strategies +float NightbaneDisableMovementMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } - if (botAI->IsRanged(bot) && bot->HasAura(SPELL_ENFEEBLE) && - (dynamic_cast(action) && - !dynamic_cast(action))) + // Disable CombatFormationMoveAction for all bots except: + // (1) main tank and (2) only during the ground phase, other melee + if (botAI->IsRanged(bot) || + (botAI->IsMelee(bot) && !botAI->IsMainTank(bot) && + nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z)) { - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; } return 1.0f; diff --git a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h index c8a4ba3722..8ad5a1d4ec 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h @@ -3,45 +3,131 @@ #include "Multiplier.h" -class KarazhanAttumenTheHuntsmanMultiplier : public Multiplier +class AttumenTheHuntsmanDisableTankAssistMultiplier : public Multiplier { public: - KarazhanAttumenTheHuntsmanMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan attumen the huntsman multiplier") {} + AttumenTheHuntsmanDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman disable tank assist multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanBigBadWolfMultiplier : public Multiplier +class AttumenTheHuntsmanStayStackedMultiplier : public Multiplier { public: - KarazhanBigBadWolfMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan big bad wolf multiplier") {} + AttumenTheHuntsmanStayStackedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman stay stacked multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanShadeOfAranMultiplier : public Multiplier +class AttumenTheHuntsmanWaitForDpsMultiplier : public Multiplier { public: - KarazhanShadeOfAranMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan shade of aran multiplier") {} + AttumenTheHuntsmanWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman wait for dps multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanNetherspiteBlueAndGreenBeamMultiplier : public Multiplier +class TheCuratorDisableTankAssistMultiplier : public Multiplier { public: - KarazhanNetherspiteBlueAndGreenBeamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan netherspite blue and green beam multiplier") {} + TheCuratorDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator disable tank assist multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanNetherspiteRedBeamMultiplier : public Multiplier +class TheCuratorDelayBloodlustAndHeroismMultiplier : public Multiplier { public: - KarazhanNetherspiteRedBeamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan netherspite red beam multiplier") {} + TheCuratorDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator delay bloodlust and heroism multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanPrinceMalchezaarMultiplier : public Multiplier +class ShadeOfAranArcaneExplosionDisableChargeMultiplier : public Multiplier { public: - KarazhanPrinceMalchezaarMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan prince malchezaar multiplier") {} + ShadeOfAranArcaneExplosionDisableChargeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "shade of aran arcane explosion disable charge multiplier") {} + virtual float GetValue(Action* action); +}; + +class ShadeOfAranFlameWreathDisableMovementMultiplier : public Multiplier +{ +public: + ShadeOfAranFlameWreathDisableMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "shade of aran flame wreath disable movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class NetherspiteKeepBlockingBeamMultiplier : public Multiplier +{ +public: + NetherspiteKeepBlockingBeamMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "netherspite keep blocking beam multiplier") {} + virtual float GetValue(Action* action); +}; + +class NetherspiteWaitForDpsMultiplier : public Multiplier +{ +public: + NetherspiteWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "netherspite wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarDisableAvoidAoeMultiplier : public Multiplier +{ +public: + PrinceMalchezaarDisableAvoidAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar disable avoid aoe multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarEnfeebleKeepDistanceMultiplier : public Multiplier +{ +public: + PrinceMalchezaarEnfeebleKeepDistanceMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar enfeeble keep distance multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + PrinceMalchezaarDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar delay bloodlust and heroism multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisablePetsMultiplier : public Multiplier +{ +public: + NightbaneDisablePetsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable pets multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneWaitForDpsMultiplier : public Multiplier +{ +public: + NightbaneWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisableAvoidAoeMultiplier : public Multiplier +{ +public: + NightbaneDisableAvoidAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable avoid aoe multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisableMovementMultiplier : public Multiplier +{ +public: + NightbaneDisableMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable movement multiplier") {} virtual float GetValue(Action* action); }; diff --git a/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp b/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp index f93c923c70..041651afd4 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp @@ -3,79 +3,160 @@ void RaidKarazhanStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "karazhan attumen the huntsman", NextAction::array(0, - new NextAction("karazhan attumen the huntsman stack behind", ACTION_RAID + 1), - nullptr))); + // Trash + triggers.push_back(new TriggerNode("mana warp is about to explode", + NextAction::array(0, new NextAction("mana warp stun creature before warp breach", ACTION_EMERGENCY + 6), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan moroes", NextAction::array(0, - new NextAction("karazhan moroes mark target", ACTION_RAID + 1), - nullptr))); + // Attumen the Huntsman + triggers.push_back(new TriggerNode("attumen the huntsman need target priority", + NextAction::array(0, new NextAction("attumen the huntsman mark target", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman attumen spawned", + NextAction::array(0, new NextAction("attumen the huntsman split bosses", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman attumen is mounted", + NextAction::array(0, new NextAction("attumen the huntsman stack behind", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman boss wipes aggro when mounting", + NextAction::array(0, new NextAction("attumen the huntsman manage dps timer", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan maiden of virtue", NextAction::array(0, - new NextAction("karazhan maiden of virtue position ranged", ACTION_RAID + 1), - new NextAction("karazhan maiden of virtue position boss", ACTION_RAID + 1), - nullptr))); + // Moroes + triggers.push_back(new TriggerNode("moroes boss engaged by main tank", + NextAction::array(0, new NextAction("moroes main tank attack boss", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("moroes need target priority", + NextAction::array(0, new NextAction("moroes mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan big bad wolf", NextAction::array(0, - new NextAction("karazhan big bad wolf run away", ACTION_EMERGENCY + 6), - new NextAction("karazhan big bad wolf position boss", ACTION_RAID + 1), - nullptr))); + // Maiden of Virtue + triggers.push_back(new TriggerNode("maiden of virtue healers are stunned by repentance", + NextAction::array(0, new NextAction("maiden of virtue move boss to healer", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("maiden of virtue holy wrath deals chain damage", + NextAction::array(0, new NextAction("maiden of virtue position ranged", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan romulo and julianne", NextAction::array(0, - new NextAction("karazhan romulo and julianne mark target", ACTION_RAID + 1), - nullptr))); + // The Big Bad Wolf + triggers.push_back(new TriggerNode("big bad wolf boss is chasing little red riding hood", + NextAction::array(0, new NextAction("big bad wolf run away from boss", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("big bad wolf boss engaged by tank", + NextAction::array(0, new NextAction("big bad wolf position boss", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan wizard of oz", NextAction::array(0, - new NextAction("karazhan wizard of oz scorch strawman", ACTION_RAID + 2), - new NextAction("karazhan wizard of oz mark target", ACTION_RAID + 1), - nullptr))); + // Romulo and Julianne + triggers.push_back(new TriggerNode("romulo and julianne both bosses revived", + NextAction::array(0, new NextAction("romulo and julianne mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan the curator", NextAction::array(0, - new NextAction("karazhan the curator spread ranged", ACTION_RAID + 2), - new NextAction("karazhan the curator position boss", ACTION_RAID + 2), - new NextAction("karazhan the curator mark target", ACTION_RAID + 1), - nullptr))); + // The Wizard of Oz + triggers.push_back(new TriggerNode("wizard of oz need target priority", + NextAction::array(0, new NextAction("wizard of oz mark target", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("wizard of oz strawman is vulnerable to fire", + NextAction::array(0, new NextAction("wizard of oz scorch strawman", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan terestian illhoof", NextAction::array(0, - new NextAction("karazhan terestian illhoof mark target", ACTION_RAID + 1), - nullptr))); + // The Curator + triggers.push_back(new TriggerNode("the curator astral flare spawned", + NextAction::array(0, new NextAction("the curator mark astral flare", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("the curator boss engaged by tanks", + NextAction::array(0, new NextAction("the curator position boss", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("the curator astral flares cast arcing sear", + NextAction::array(0, new NextAction("the curator spread ranged", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan shade of aran", NextAction::array(0, - new NextAction("karazhan shade of aran flame wreath stop movement", ACTION_EMERGENCY + 7), - new NextAction("karazhan shade of aran arcane explosion run away", ACTION_EMERGENCY + 6), - new NextAction("karazhan shade of aran spread ranged", ACTION_RAID + 2), - new NextAction("karazhan shade of aran mark conjured elemental", ACTION_RAID + 1), - nullptr))); + // Terestian Illhoof + triggers.push_back(new TriggerNode("terestian illhoof need target priority", + NextAction::array(0, new NextAction("terestian illhoof mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan netherspite", NextAction::array(0, - new NextAction("karazhan netherspite block red beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite block blue beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite block green beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite avoid beam and void zone", ACTION_EMERGENCY + 7), - new NextAction("karazhan netherspite banish phase avoid void zone", ACTION_RAID + 1), - nullptr))); + // Shade of Aran + triggers.push_back(new TriggerNode("shade of aran arcane explosion is casting", + NextAction::array(0, new NextAction("shade of aran run away from arcane explosion", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran flame wreath is active", + NextAction::array(0, new NextAction("shade of aran stop moving during flame wreath", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran conjured elementals summoned", + NextAction::array(0, new NextAction("shade of aran mark conjured elemental", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran boss uses counterspell and blizzard", + NextAction::array(0, new NextAction("shade of aran ranged maintain distance", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan prince malchezaar", NextAction::array(0, - new NextAction("karazhan prince malchezaar non tank avoid hazard", ACTION_EMERGENCY + 6), - new NextAction("karazhan prince malchezaar tank avoid hazard", ACTION_EMERGENCY + 6), - nullptr))); + // Netherspite + triggers.push_back(new TriggerNode("netherspite red beam is active", + NextAction::array(0, new NextAction("netherspite block red beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite blue beam is active", + NextAction::array(0, new NextAction("netherspite block blue beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite green beam is active", + NextAction::array(0, new NextAction("netherspite block green beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite bot is not beam blocker", + NextAction::array(0, new NextAction("netherspite avoid beam and void zone", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite boss is banished", + NextAction::array(0, new NextAction("netherspite banish phase avoid void zone", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite need to manage timers and trackers", + NextAction::array(0, new NextAction("netherspite manage timers and trackers", ACTION_EMERGENCY + 10), nullptr) + )); + + // Prince Malchezaar + triggers.push_back(new TriggerNode("prince malchezaar bot is enfeebled", + NextAction::array(0, new NextAction("prince malchezaar enfeebled avoid hazard", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("prince malchezaar infernals are spawned", + NextAction::array(0, new NextAction("prince malchezaar non tank avoid infernal", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("prince malchezaar boss engaged by main tank", + NextAction::array(0, new NextAction("prince malchezaar main tank movement", ACTION_EMERGENCY + 6), nullptr) + )); + + // Nightbane + triggers.push_back(new TriggerNode("nightbane boss engaged by main tank", + NextAction::array(0, new NextAction("nightbane ground phase position boss", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane ranged bots are in charred earth", + NextAction::array(0, new NextAction("nightbane ground phase rotate ranged positions", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane main tank is susceptible to fear", + NextAction::array(0, new NextAction("nightbane cast fear ward on main tank", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane pets ignore collision to chase flying boss", + NextAction::array(0, new NextAction("nightbane control pet aggression", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane boss is flying", + NextAction::array(0, new NextAction("nightbane flight phase movement", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane need to manage timers and trackers", + NextAction::array(0, new NextAction("nightbane manage timers and trackers", ACTION_EMERGENCY + 10), nullptr) + )); } void RaidKarazhanStrategy::InitMultipliers(std::vector& multipliers) { - multipliers.push_back(new KarazhanShadeOfAranMultiplier(botAI)); - multipliers.push_back(new KarazhanNetherspiteBlueAndGreenBeamMultiplier(botAI)); - multipliers.push_back(new KarazhanNetherspiteRedBeamMultiplier(botAI)); - multipliers.push_back(new KarazhanPrinceMalchezaarMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanStayStackedMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanWaitForDpsMultiplier(botAI)); + multipliers.push_back(new TheCuratorDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new TheCuratorDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new ShadeOfAranArcaneExplosionDisableChargeMultiplier(botAI)); + multipliers.push_back(new ShadeOfAranFlameWreathDisableMovementMultiplier(botAI)); + multipliers.push_back(new NetherspiteKeepBlockingBeamMultiplier(botAI)); + multipliers.push_back(new NetherspiteWaitForDpsMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarEnfeebleKeepDistanceMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new NightbaneDisablePetsMultiplier(botAI)); + multipliers.push_back(new NightbaneWaitForDpsMultiplier(botAI)); + multipliers.push_back(new NightbaneDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new NightbaneDisableMovementMultiplier(botAI)); } diff --git a/src/strategy/raids/karazhan/RaidKarazhanStrategy.h b/src/strategy/raids/karazhan/RaidKarazhanStrategy.h index c03e42856f..7d6b16deef 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanStrategy.h +++ b/src/strategy/raids/karazhan/RaidKarazhanStrategy.h @@ -7,7 +7,7 @@ class RaidKarazhanStrategy : public Strategy { public: - RaidKarazhanStrategy(PlayerbotAI* ai) : Strategy(ai) {} + RaidKarazhanStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} std::string const getName() override { return "karazhan"; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h b/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h index 0b6a29e828..fa2b7d37a5 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h @@ -9,31 +9,255 @@ class RaidKarazhanTriggerContext : public NamedObjectContext public: RaidKarazhanTriggerContext() { - creators["karazhan attumen the huntsman"] = &RaidKarazhanTriggerContext::karazhan_attumen_the_huntsman; - creators["karazhan moroes"] = &RaidKarazhanTriggerContext::karazhan_moroes; - creators["karazhan maiden of virtue"] = &RaidKarazhanTriggerContext::karazhan_maiden_of_virtue; - creators["karazhan big bad wolf"] = &RaidKarazhanTriggerContext::karazhan_big_bad_wolf; - creators["karazhan romulo and julianne"] = &RaidKarazhanTriggerContext::karazhan_romulo_and_julianne; - creators["karazhan wizard of oz"] = &RaidKarazhanTriggerContext::karazhan_wizard_of_oz; - creators["karazhan the curator"] = &RaidKarazhanTriggerContext::karazhan_the_curator; - creators["karazhan terestian illhoof"] = &RaidKarazhanTriggerContext::karazhan_terestian_illhoof; - creators["karazhan shade of aran"] = &RaidKarazhanTriggerContext::karazhan_shade_of_aran; - creators["karazhan netherspite"] = &RaidKarazhanTriggerContext::karazhan_netherspite; - creators["karazhan prince malchezaar"] = &RaidKarazhanTriggerContext::karazhan_prince_malchezaar; + // Trash + creators["mana warp is about to explode"] = + &RaidKarazhanTriggerContext::mana_warp_is_about_to_explode; + + // Attumen the Huntsman + creators["attumen the huntsman need target priority"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_need_target_priority; + + creators["attumen the huntsman attumen spawned"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_attumen_spawned; + + creators["attumen the huntsman attumen is mounted"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_attumen_is_mounted; + + creators["attumen the huntsman boss wipes aggro when mounting"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_boss_wipes_aggro_when_mounting; + + // Moroes + creators["moroes boss engaged by main tank"] = + &RaidKarazhanTriggerContext::moroes_boss_engaged_by_main_tank; + + creators["moroes need target priority"] = + &RaidKarazhanTriggerContext::moroes_need_target_priority; + + // Maiden of Virtue + creators["maiden of virtue healers are stunned by repentance"] = + &RaidKarazhanTriggerContext::maiden_of_virtue_healers_are_stunned_by_repentance; + + creators["maiden of virtue holy wrath deals chain damage"] = + &RaidKarazhanTriggerContext::maiden_of_virtue_holy_wrath_deals_chain_damage; + + // The Big Bad Wolf + creators["big bad wolf boss engaged by tank"] = + &RaidKarazhanTriggerContext::big_bad_wolf_boss_engaged_by_tank; + + creators["big bad wolf boss is chasing little red riding hood"] = + &RaidKarazhanTriggerContext::big_bad_wolf_boss_is_chasing_little_red_riding_hood; + + // Romulo and Julianne + creators["romulo and julianne both bosses revived"] = + &RaidKarazhanTriggerContext::romulo_and_julianne_both_bosses_revived; + + // The Wizard of Oz + creators["wizard of oz need target priority"] = + &RaidKarazhanTriggerContext::wizard_of_oz_need_target_priority; + + creators["wizard of oz strawman is vulnerable to fire"] = + &RaidKarazhanTriggerContext::wizard_of_oz_strawman_is_vulnerable_to_fire; + + // The Curator + creators["the curator astral flare spawned"] = + &RaidKarazhanTriggerContext::the_curator_astral_flare_spawned; + + creators["the curator boss engaged by tanks"] = + &RaidKarazhanTriggerContext::the_curator_boss_engaged_by_tanks; + + creators["the curator astral flares cast arcing sear"] = + &RaidKarazhanTriggerContext::the_curator_astral_flares_cast_arcing_sear; + + // Terestian Illhoof + creators["terestian illhoof need target priority"] = + &RaidKarazhanTriggerContext::terestian_illhoof_need_target_priority; + + // Shade of Aran + creators["shade of aran arcane explosion is casting"] = + &RaidKarazhanTriggerContext::shade_of_aran_arcane_explosion_is_casting; + + creators["shade of aran flame wreath is active"] = + &RaidKarazhanTriggerContext::shade_of_aran_flame_wreath_is_active; + + creators["shade of aran conjured elementals summoned"] = + &RaidKarazhanTriggerContext::shade_of_aran_conjured_elementals_summoned; + + creators["shade of aran boss uses counterspell and blizzard"] = + &RaidKarazhanTriggerContext::shade_of_aran_boss_uses_counterspell_and_blizzard; + + // Netherspite + creators["netherspite red beam is active"] = + &RaidKarazhanTriggerContext::netherspite_red_beam_is_active; + + creators["netherspite blue beam is active"] = + &RaidKarazhanTriggerContext::netherspite_blue_beam_is_active; + + creators["netherspite green beam is active"] = + &RaidKarazhanTriggerContext::netherspite_green_beam_is_active; + + creators["netherspite bot is not beam blocker"] = + &RaidKarazhanTriggerContext::netherspite_bot_is_not_beam_blocker; + + creators["netherspite boss is banished"] = + &RaidKarazhanTriggerContext::netherspite_boss_is_banished; + + creators["netherspite need to manage timers and trackers"] = + &RaidKarazhanTriggerContext::netherspite_need_to_manage_timers_and_trackers; + + // Prince Malchezaar + creators["prince malchezaar bot is enfeebled"] = + &RaidKarazhanTriggerContext::prince_malchezaar_bot_is_enfeebled; + + creators["prince malchezaar infernals are spawned"] = + &RaidKarazhanTriggerContext::prince_malchezaar_infernals_are_spawned; + + creators["prince malchezaar boss engaged by main tank"] = + &RaidKarazhanTriggerContext::prince_malchezaar_boss_engaged_by_main_tank; + + // Nightbane + creators["nightbane boss engaged by main tank"] = + &RaidKarazhanTriggerContext::nightbane_boss_engaged_by_main_tank; + + creators["nightbane ranged bots are in charred earth"] = + &RaidKarazhanTriggerContext::nightbane_ranged_bots_are_in_charred_earth; + + creators["nightbane main tank is susceptible to fear"] = + &RaidKarazhanTriggerContext::nightbane_main_tank_is_susceptible_to_fear; + + creators["nightbane pets ignore collision to chase flying boss"] = + &RaidKarazhanTriggerContext::nightbane_pets_ignore_collision_to_chase_flying_boss; + + creators["nightbane boss is flying"] = + &RaidKarazhanTriggerContext::nightbane_boss_is_flying; + + creators["nightbane need to manage timers and trackers"] = + &RaidKarazhanTriggerContext::nightbane_need_to_manage_timers_and_trackers; } private: - static Trigger* karazhan_attumen_the_huntsman(PlayerbotAI* botAI) { return new KarazhanAttumenTheHuntsmanTrigger(botAI); } - static Trigger* karazhan_moroes(PlayerbotAI* botAI) { return new KarazhanMoroesTrigger(botAI); } - static Trigger* karazhan_maiden_of_virtue(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtueTrigger(botAI); } - static Trigger* karazhan_big_bad_wolf(PlayerbotAI* botAI) { return new KarazhanBigBadWolfTrigger(botAI); } - static Trigger* karazhan_romulo_and_julianne(PlayerbotAI* botAI) { return new KarazhanRomuloAndJulianneTrigger(botAI); } - static Trigger* karazhan_wizard_of_oz(PlayerbotAI* botAI) { return new KarazhanWizardOfOzTrigger(botAI); } - static Trigger* karazhan_the_curator(PlayerbotAI* botAI) { return new KarazhanTheCuratorTrigger(botAI); } - static Trigger* karazhan_terestian_illhoof(PlayerbotAI* botAI) { return new KarazhanTerestianIllhoofTrigger(botAI); } - static Trigger* karazhan_shade_of_aran(PlayerbotAI* botAI) { return new KarazhanShadeOfAranTrigger(botAI); } - static Trigger* karazhan_netherspite(PlayerbotAI* botAI) { return new KarazhanNetherspiteTrigger(botAI); } - static Trigger* karazhan_prince_malchezaar(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarTrigger(botAI); } + // Trash + static Trigger* mana_warp_is_about_to_explode( + PlayerbotAI* botAI) { return new ManaWarpIsAboutToExplodeTrigger(botAI); } + + // Attumen the Huntsman + static Trigger* attumen_the_huntsman_need_target_priority( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanNeedTargetPriorityTrigger(botAI); } + + static Trigger* attumen_the_huntsman_attumen_spawned( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenSpawnedTrigger(botAI); } + + static Trigger* attumen_the_huntsman_attumen_is_mounted( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenIsMountedTrigger(botAI); } + + static Trigger* attumen_the_huntsman_boss_wipes_aggro_when_mounting( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger(botAI); } + + // Moroes + static Trigger* moroes_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new MoroesBossEngagedByMainTankTrigger(botAI); } + + static Trigger* moroes_need_target_priority( + PlayerbotAI* botAI) { return new MoroesNeedTargetPriorityTrigger(botAI); } + + // Maiden of Virtue + static Trigger* maiden_of_virtue_healers_are_stunned_by_repentance( + PlayerbotAI* botAI) { return new MaidenOfVirtueHealersAreStunnedByRepentanceTrigger(botAI); } + + static Trigger* maiden_of_virtue_holy_wrath_deals_chain_damage( + PlayerbotAI* botAI) { return new MaidenOfVirtueHolyWrathDealsChainDamageTrigger(botAI); } + + // The Big Bad Wolf + static Trigger* big_bad_wolf_boss_engaged_by_tank( + PlayerbotAI* botAI) { return new BigBadWolfBossEngagedByTankTrigger(botAI); } + + static Trigger* big_bad_wolf_boss_is_chasing_little_red_riding_hood( + PlayerbotAI* botAI) { return new BigBadWolfBossIsChasingLittleRedRidingHoodTrigger(botAI); } + + // Romulo and Julianne + static Trigger* romulo_and_julianne_both_bosses_revived( + PlayerbotAI* botAI) { return new RomuloAndJulianneBothBossesRevivedTrigger(botAI); } + + // The Wizard of Oz + static Trigger* wizard_of_oz_need_target_priority( + PlayerbotAI* botAI) { return new WizardOfOzNeedTargetPriorityTrigger(botAI); } + + static Trigger* wizard_of_oz_strawman_is_vulnerable_to_fire( + PlayerbotAI* botAI) { return new WizardOfOzStrawmanIsVulnerableToFireTrigger(botAI); } + + // The Curator + static Trigger* the_curator_astral_flare_spawned( + PlayerbotAI* botAI) { return new TheCuratorAstralFlareSpawnedTrigger(botAI); } + + static Trigger* the_curator_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new TheCuratorBossEngagedByTanksTrigger(botAI); } + + static Trigger* the_curator_astral_flares_cast_arcing_sear( + PlayerbotAI* botAI) { return new TheCuratorBossAstralFlaresCastArcingSearTrigger(botAI); } + + // Terestian Illhoof + static Trigger* terestian_illhoof_need_target_priority( + PlayerbotAI* botAI) { return new TerestianIllhoofNeedTargetPriorityTrigger(botAI); } + + // Shade of Aran + static Trigger* shade_of_aran_arcane_explosion_is_casting( + PlayerbotAI* botAI) { return new ShadeOfAranArcaneExplosionIsCastingTrigger(botAI); } + + static Trigger* shade_of_aran_flame_wreath_is_active( + PlayerbotAI* botAI) { return new ShadeOfAranFlameWreathIsActiveTrigger(botAI); } + + static Trigger* shade_of_aran_conjured_elementals_summoned( + PlayerbotAI* botAI) { return new ShadeOfAranConjuredElementalsSummonedTrigger(botAI); } + + static Trigger* shade_of_aran_boss_uses_counterspell_and_blizzard( + PlayerbotAI* botAI) { return new ShadeOfAranBossUsesCounterspellAndBlizzardTrigger(botAI); } + + // Netherspite + static Trigger* netherspite_red_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteRedBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_blue_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteBlueBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_green_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteGreenBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_bot_is_not_beam_blocker( + PlayerbotAI* botAI) { return new NetherspiteBotIsNotBeamBlockerTrigger(botAI); } + + static Trigger* netherspite_boss_is_banished( + PlayerbotAI* botAI) { return new NetherspiteBossIsBanishedTrigger(botAI); } + + static Trigger* netherspite_need_to_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NetherspiteNeedToManageTimersAndTrackersTrigger(botAI); } + + // Prince Malchezaar + static Trigger* prince_malchezaar_bot_is_enfeebled( + PlayerbotAI* botAI) { return new PrinceMalchezaarBotIsEnfeebledTrigger(botAI); } + + static Trigger* prince_malchezaar_infernals_are_spawned( + PlayerbotAI* botAI) { return new PrinceMalchezaarInfernalsAreSpawnedTrigger(botAI); } + + static Trigger* prince_malchezaar_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new PrinceMalchezaarBossEngagedByMainTankTrigger(botAI); } + + // Nightbane + static Trigger* nightbane_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new NightbaneBossEngagedByMainTankTrigger(botAI); } + + static Trigger* nightbane_ranged_bots_are_in_charred_earth( + PlayerbotAI* botAI) { return new NightbaneRangedBotsAreInCharredEarthTrigger(botAI); } + + static Trigger* nightbane_main_tank_is_susceptible_to_fear( + PlayerbotAI* botAI) { return new NightbaneMainTankIsSusceptibleToFearTrigger(botAI); } + + static Trigger* nightbane_pets_ignore_collision_to_chase_flying_boss( + PlayerbotAI* botAI) { return new NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger(botAI); } + + static Trigger* nightbane_boss_is_flying( + PlayerbotAI* botAI) { return new NightbaneBossIsFlyingTrigger(botAI); } + + static Trigger* nightbane_need_to_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NightbaneNeedToManageTimersAndTrackersTrigger(botAI); } }; -#endif +#endif \ No newline at end of file diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp b/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp index f70ae70fde..e39006f76e 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp @@ -3,17 +3,64 @@ #include "RaidKarazhanActions.h" #include "Playerbots.h" -bool KarazhanAttumenTheHuntsmanTrigger::IsActive() +using namespace KarazhanHelpers; + +bool ManaWarpIsAboutToExplodeTrigger::IsActive() +{ + Unit* manaWarp = AI_VALUE2(Unit*, "find target", "mana warp"); + return manaWarp && manaWarp->GetHealthPct() < 15; +} + +bool AttumenTheHuntsmanNeedTargetPriorityTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + return midnight != nullptr; +} + +bool AttumenTheHuntsmanAttumenSpawnedTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN); + return attumen != nullptr; +} + +bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + return attumenMounted && attumenMounted->GetVictim() != bot; +} + +bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() { - RaidKarazhanHelpers helpers(botAI); - Unit* boss = helpers.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + return midnight != nullptr; } -bool KarazhanMoroesTrigger::IsActive() +bool MoroesBossEngagedByMainTankTrigger::IsActive() { + if (!botAI->IsMainTank(bot)) + return false; + Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes"); + return moroes != nullptr; +} + +bool MoroesNeedTargetPriorityTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); @@ -21,39 +68,67 @@ bool KarazhanMoroesTrigger::IsActive() Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - return ((moroes && moroes->IsAlive()) || - (dorothea && dorothea->IsAlive()) || - (catriona && catriona->IsAlive()) || - (keira && keira->IsAlive()) || - (rafe && rafe->IsAlive()) || - (robin && robin->IsAlive()) || - (crispin && crispin->IsAlive())); + Unit* target = GetFirstAliveUnit({ dorothea, catriona, keira, rafe, robin, crispin }); + return target != nullptr; } -bool KarazhanMaidenOfVirtueTrigger::IsActive() +bool MaidenOfVirtueHealersAreStunnedByRepentanceTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + if (!botAI->IsTank(bot)) + return false; - return boss && boss->IsAlive(); + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + return maiden && maiden->GetVictim() == bot; } -bool KarazhanBigBadWolfTrigger::IsActive() +bool MaidenOfVirtueHolyWrathDealsChainDamageTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + if (!botAI->IsRanged(bot)) + return false; - return boss && boss->IsAlive(); + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + return maiden != nullptr; } -bool KarazhanRomuloAndJulianneTrigger::IsActive() +bool BigBadWolfBossEngagedByTankTrigger::IsActive() { - Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!botAI->IsTank(bot) || bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + return false; + + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + return wolf != nullptr; +} + +bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() +{ + if (!bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + return false; + + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + return wolf != nullptr; +} + +bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() +{ + if (!IsMapIDTimerManager(botAI, bot)) + return false; + Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); + if (!romulo) + return false; - return julianne && julianne->IsAlive() && romulo && romulo->IsAlive(); + Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!julianne) + return false; + + return true; } -bool KarazhanWizardOfOzTrigger::IsActive() +bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { + if (!IsMapIDTimerManager(botAI, bot)) + return false; + Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); @@ -61,45 +136,249 @@ bool KarazhanWizardOfOzTrigger::IsActive() Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - return ((dorothee && dorothee->IsAlive()) || - (tito && tito->IsAlive()) || - (roar && roar->IsAlive()) || - (strawman && strawman->IsAlive()) || - (tinhead && tinhead->IsAlive()) || - (crone && crone->IsAlive())); + Unit* target = GetFirstAliveUnit({ dorothee, tito, roar, strawman, tinhead, crone }); + return target != nullptr; +} + +bool WizardOfOzStrawmanIsVulnerableToFireTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE) + return false; + + Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); + return strawman && strawman->IsAlive(); +} + +bool TheCuratorAstralFlareSpawnedTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); + return flare != nullptr; +} + +bool TheCuratorBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + return curator != nullptr; +} + +bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + return curator != nullptr; } -bool KarazhanTheCuratorTrigger::IsActive() +bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + return illhoof != nullptr; } -bool KarazhanTerestianIllhoofTrigger::IsActive() +bool ShadeOfAranArcaneExplosionIsCastingTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION) && + !IsFlameWreathActive(botAI, bot); +} - return boss && boss->IsAlive(); +bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && IsFlameWreathActive(botAI, bot); } -bool KarazhanShadeOfAranTrigger::IsActive() +// Exclusion of Banish is so the player may Banish elementals if they wish +bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); + return elemental && elemental->IsAlive() && + !elemental->HasAura(SPELL_WARLOCK_BANISH); } -bool KarazhanNetherspiteTrigger::IsActive() +bool ShadeOfAranBossUsesCounterspellAndBlizzardTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!botAI->IsRanged(bot)) + return false; - return boss && boss->IsAlive(); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && !(aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) && + !IsFlameWreathActive(botAI, bot); } -bool KarazhanPrinceMalchezaarTrigger::IsActive() +bool NetherspiteRedBeamIsActiveTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; - return boss && boss->IsAlive(); + Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + return redPortal != nullptr; +} + +bool NetherspiteBlueBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + return bluePortal != nullptr; +} + +bool NetherspiteGreenBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + return greenPortal != nullptr; +} + +bool NetherspiteBotIsNotBeamBlockerTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + return bot != redBlocker && bot != blueBlocker && bot != greenBlocker; +} + +bool NetherspiteBossIsBanishedTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || !netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + std::vector voidZones = GetAllVoidZones(botAI, bot); + for (Unit* vz : voidZones) + { + if (bot->GetExactDist2d(vz) < 4.0f) + return true; + } + + return false; +} + +bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() +{ + if (!botAI->IsTank(bot) && !IsMapIDTimerManager(botAI, bot)) + return false; + + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + return netherspite != nullptr; +} + +bool PrinceMalchezaarBotIsEnfeebledTrigger::IsActive() +{ + return bot->HasAura(SPELL_ENFEEBLE); +} + +bool PrinceMalchezaarInfernalsAreSpawnedTrigger::IsActive() +{ + if (botAI->IsMainTank(bot) || bot->HasAura(SPELL_ENFEEBLE)) + return false; + + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + return malchezaar != nullptr; +} + +bool PrinceMalchezaarBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + return malchezaar != nullptr; +} + +bool NightbaneBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane && nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z; +} + +bool NightbaneRangedBotsAreInCharredEarthTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane && nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z; +} + +bool NightbaneMainTankIsSusceptibleToFearTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && + botAI->CanCastSpell("fear ward", mainTank); +} + +bool NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Pet* pet = bot->GetPet(); + return pet && pet->IsAlive(); +} + +bool NightbaneBossIsFlyingTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + return false; + + const time_t now = std::time(nullptr); + const uint8 flightPhaseDurationSeconds = 35; + + return nightbaneFlightPhaseStartTimer.find(KARAZHAN_MAP_ID) != nightbaneFlightPhaseStartTimer.end() && + (now - nightbaneFlightPhaseStartTimer[KARAZHAN_MAP_ID] < flightPhaseDurationSeconds); +} + +bool NightbaneNeedToManageTimersAndTrackersTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane != nullptr; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggers.h b/src/strategy/raids/karazhan/RaidKarazhanTriggers.h index 69f441525b..e3465ef1e8 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggers.h @@ -3,80 +3,298 @@ #include "Trigger.h" -class KarazhanAttumenTheHuntsmanTrigger : public Trigger +class ManaWarpIsAboutToExplodeTrigger : public Trigger { public: - KarazhanAttumenTheHuntsmanTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan attumen the huntsman") {} + ManaWarpIsAboutToExplodeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mana warp is about to explode") {} bool IsActive() override; }; -class KarazhanMoroesTrigger : public Trigger +class AttumenTheHuntsmanNeedTargetPriorityTrigger : public Trigger { public: - KarazhanMoroesTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan moroes") {} + AttumenTheHuntsmanNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman need target priority") {} bool IsActive() override; }; -class KarazhanMaidenOfVirtueTrigger : public Trigger +class AttumenTheHuntsmanAttumenSpawnedTrigger : public Trigger { public: - KarazhanMaidenOfVirtueTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan maiden of virtue") {} + AttumenTheHuntsmanAttumenSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman attumen spawned") {} bool IsActive() override; }; -class KarazhanBigBadWolfTrigger : public Trigger +class AttumenTheHuntsmanAttumenIsMountedTrigger : public Trigger { public: - KarazhanBigBadWolfTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan big bad wolf") {} + AttumenTheHuntsmanAttumenIsMountedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman attumen is mounted") {} bool IsActive() override; }; -class KarazhanRomuloAndJulianneTrigger : public Trigger +class AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger : public Trigger { public: - KarazhanRomuloAndJulianneTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan romulo and julianne") {} + AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman boss wipes aggro when mounting") {} bool IsActive() override; }; -class KarazhanWizardOfOzTrigger : public Trigger +class MoroesBossEngagedByMainTankTrigger : public Trigger { public: - KarazhanWizardOfOzTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan wizard of oz") {} + MoroesBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "moroes boss engaged by main tank") {} bool IsActive() override; }; -class KarazhanTheCuratorTrigger : public Trigger +class MoroesNeedTargetPriorityTrigger : public Trigger { public: - KarazhanTheCuratorTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan the curator") {} + MoroesNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "moroes need target priority") {} bool IsActive() override; }; -class KarazhanTerestianIllhoofTrigger : public Trigger +class MaidenOfVirtueHealersAreStunnedByRepentanceTrigger : public Trigger { public: - KarazhanTerestianIllhoofTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan terestian illhoof") {} + MaidenOfVirtueHealersAreStunnedByRepentanceTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "maiden of virtue healers are stunned by repentance") {} bool IsActive() override; }; -class KarazhanShadeOfAranTrigger : public Trigger +class MaidenOfVirtueHolyWrathDealsChainDamageTrigger : public Trigger { public: - KarazhanShadeOfAranTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan shade of aran") {} + MaidenOfVirtueHolyWrathDealsChainDamageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "maiden of virtue holy wrath deals chain damage") {} bool IsActive() override; }; -class KarazhanNetherspiteTrigger : public Trigger +class BigBadWolfBossEngagedByTankTrigger : public Trigger { public: - KarazhanNetherspiteTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan netherspite") {} + BigBadWolfBossEngagedByTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "big bad wolf boss engaged by tank") {} bool IsActive() override; }; -class KarazhanPrinceMalchezaarTrigger : public Trigger +class BigBadWolfBossIsChasingLittleRedRidingHoodTrigger : public Trigger { public: - KarazhanPrinceMalchezaarTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan prince malchezaar") {} + BigBadWolfBossIsChasingLittleRedRidingHoodTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "big bad wolf boss is chasing little red riding hood") {} + bool IsActive() override; +}; + +class RomuloAndJulianneBothBossesRevivedTrigger : public Trigger +{ +public: + RomuloAndJulianneBothBossesRevivedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "romulo and julianne both bosses revived") {} + bool IsActive() override; +}; + +class WizardOfOzNeedTargetPriorityTrigger : public Trigger +{ +public: + WizardOfOzNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "wizard of oz need target priority") {} + bool IsActive() override; +}; + +class WizardOfOzStrawmanIsVulnerableToFireTrigger : public Trigger +{ +public: + WizardOfOzStrawmanIsVulnerableToFireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "wizard of oz strawman is vulnerable to fire") {} + bool IsActive() override; +}; +class TheCuratorAstralFlareSpawnedTrigger : public Trigger +{ +public: + TheCuratorAstralFlareSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator astral flare spawned") {} + bool IsActive() override; +}; + +class TheCuratorBossEngagedByTanksTrigger : public Trigger +{ +public: + TheCuratorBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator boss engaged by tanks") {} + bool IsActive() override; +}; + +class TheCuratorBossAstralFlaresCastArcingSearTrigger : public Trigger +{ +public: + TheCuratorBossAstralFlaresCastArcingSearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator astral flares cast arcing sear") {} + bool IsActive() override; +}; + +class TerestianIllhoofNeedTargetPriorityTrigger : public Trigger +{ +public: + TerestianIllhoofNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "terestian illhoof need target priority") {} + bool IsActive() override; +}; + +class ShadeOfAranArcaneExplosionIsCastingTrigger : public Trigger +{ +public: + ShadeOfAranArcaneExplosionIsCastingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran arcane explosion is casting") {} + bool IsActive() override; +}; + +class ShadeOfAranFlameWreathIsActiveTrigger : public Trigger +{ +public: + ShadeOfAranFlameWreathIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran flame wreath is active") {} + bool IsActive() override; +}; + +class ShadeOfAranConjuredElementalsSummonedTrigger : public Trigger +{ +public: + ShadeOfAranConjuredElementalsSummonedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran conjured elementals summoned") {} + bool IsActive() override; +}; + +class ShadeOfAranBossUsesCounterspellAndBlizzardTrigger : public Trigger +{ +public: + ShadeOfAranBossUsesCounterspellAndBlizzardTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran boss uses counterspell and blizzard") {} + bool IsActive() override; +}; + +class NetherspiteRedBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteRedBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite red beam is active") {} + bool IsActive() override; +}; + +class NetherspiteBlueBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteBlueBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite blue beam is active") {} + bool IsActive() override; +}; + +class NetherspiteGreenBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteGreenBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite green beam is active") {} + bool IsActive() override; +}; + +class NetherspiteBotIsNotBeamBlockerTrigger : public Trigger +{ +public: + NetherspiteBotIsNotBeamBlockerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite bot is not beam blocker") {} + bool IsActive() override; +}; + +class NetherspiteBossIsBanishedTrigger : public Trigger +{ +public: + NetherspiteBossIsBanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite boss is banished") {} + bool IsActive() override; +}; + +class NetherspiteNeedToManageTimersAndTrackersTrigger : public Trigger +{ +public: + NetherspiteNeedToManageTimersAndTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite need to manage timers and trackers") {} + bool IsActive() override; +}; + +class PrinceMalchezaarBotIsEnfeebledTrigger : public Trigger +{ +public: + PrinceMalchezaarBotIsEnfeebledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar bot is enfeebled") {} + bool IsActive() override; +}; + +class PrinceMalchezaarInfernalsAreSpawnedTrigger : public Trigger +{ +public: + PrinceMalchezaarInfernalsAreSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar infernals are spawned") {} + bool IsActive() override; +}; + +class PrinceMalchezaarBossEngagedByMainTankTrigger : public Trigger +{ +public: + PrinceMalchezaarBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar boss engaged by main tank") {} + bool IsActive() override; +}; + +class NightbaneBossEngagedByMainTankTrigger : public Trigger +{ +public: + NightbaneBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane boss engaged by main tank") {} + bool IsActive() override; +}; + +class NightbaneRangedBotsAreInCharredEarthTrigger : public Trigger +{ +public: + NightbaneRangedBotsAreInCharredEarthTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane ranged bots are in charred earth") {} + bool IsActive() override; +}; + +class NightbaneMainTankIsSusceptibleToFearTrigger : public Trigger +{ +public: + NightbaneMainTankIsSusceptibleToFearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane main tank is susceptible to fear") {} + bool IsActive() override; +}; + +class NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger : public Trigger +{ +public: + NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane pets ignore collision to chase flying boss") {} + bool IsActive() override; +}; + +class NightbaneBossIsFlyingTrigger : public Trigger +{ +public: + NightbaneBossIsFlyingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane boss is flying") {} + bool IsActive() override; +}; + +class NightbaneNeedToManageTimersAndTrackersTrigger : public Trigger +{ +public: + NightbaneNeedToManageTimersAndTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane need to manage timers and trackers") {} bool IsActive() override; }; From 3aeca14031742b7f72949360678aaf4ab2596cc0 Mon Sep 17 00:00:00 2001 From: Celandriel <22352763+Celandriel@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:31:58 +0000 Subject: [PATCH 17/20] Create guild mangaer for bots." --- conf/playerbots.conf.dist | 2 +- src/AiFactory.cpp | 2 +- src/PlayerbotAI.cpp | 16 +- src/PlayerbotAIConfig.cpp | 2 + src/PlayerbotAIConfig.h | 1 - src/PlayerbotGuildMgr.cpp | 312 +++++++++++++++++++++++++++ src/PlayerbotGuildMgr.h | 66 ++++++ src/PlayerbotMgr.cpp | 3 +- src/Playerbots.cpp | 4 +- src/RandomPlayerbotFactory.cpp | 182 +--------------- src/RandomPlayerbotFactory.h | 1 - src/factory/PlayerbotFactory.cpp | 48 ++--- src/strategy/actions/GroupAction.cpp | 0 src/strategy/actions/GroupAction.h | 0 14 files changed, 412 insertions(+), 227 deletions(-) create mode 100644 src/PlayerbotGuildMgr.cpp create mode 100644 src/PlayerbotGuildMgr.h create mode 100644 src/strategy/actions/GroupAction.cpp create mode 100644 src/strategy/actions/GroupAction.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index a782479550..cc06208909 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -630,7 +630,7 @@ AiPlayerbot.RandomBotHordeRatio = 50 AiPlayerbot.DisableDeathKnightLogin = 0 # Enable simulated expansion limitation for talents and glyphs -# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 +# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 # and 7 rows plus the middle talent of the 8th row for bots from level 61 until level 71 # Default: 0 (disabled) AiPlayerbot.LimitTalentsExpansion = 0 diff --git a/src/AiFactory.cpp b/src/AiFactory.cpp index 5d6377f466..0d90743ff2 100644 --- a/src/AiFactory.cpp +++ b/src/AiFactory.cpp @@ -668,7 +668,7 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const // nonCombatEngine->addStrategy("guild"); nonCombatEngine->addStrategy("grind", false); - if (sPlayerbotAIConfig->enableNewRpgStrategy) + if (sPlayerbotAIConfig->enableNewRpgStrategy) { nonCombatEngine->addStrategy("new rpg", false); } diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index c1878632c6..f176607482 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -43,6 +43,7 @@ #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "PlayerbotMgr.h" +#include "PlayerbotGuildMgr.h" #include "Playerbots.h" #include "PointMovementGenerator.h" #include "PositionValue.h" @@ -5814,19 +5815,10 @@ bool PlayerbotAI::CanMove() return bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FLIGHT_MOTION_TYPE; } - +//TODO Verify where this is actually called and the implementation of it. bool PlayerbotAI::IsRealGuild(uint32 guildId) { - Guild* guild = sGuildMgr->GetGuildById(guildId); - if (!guild) - { - return false; - } - uint32 leaderAccount = sCharacterCache->GetCharacterAccountIdByGuid(guild->GetLeaderGUID()); - if (!leaderAccount) - return false; - - return !(sPlayerbotAIConfig->IsInRandomAccountList(leaderAccount)); + return sPlayerbotGuildMgr->IsRealGuild(guildId); } bool PlayerbotAI::IsInRealGuild() @@ -5834,7 +5826,7 @@ bool PlayerbotAI::IsInRealGuild() if (!bot->GetGuildId()) return false; - return IsRealGuild(bot->GetGuildId()); + return sPlayerbotGuildMgr->IsRealGuild(bot->GetGuildId()); } void PlayerbotAI::QueueChatResponse(const ChatQueuedReply chatReply) { chatReplies.push_back(std::move(chatReply)); } diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index db69387e6f..45f880d213 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -10,6 +10,7 @@ #include "PlayerbotDungeonSuggestionMgr.h" #include "PlayerbotFactory.h" #include "Playerbots.h" +#include "PlayerbotGuildMgr.h" #include "RandomItemMgr.h" #include "RandomPlayerbotFactory.h" #include "RandomPlayerbotMgr.h" @@ -661,6 +662,7 @@ bool PlayerbotAIConfig::Initialize() sRandomPlayerbotMgr->Init(); } + sPlayerbotGuildMgr->Init(); sRandomItemMgr->Init(); sRandomItemMgr->InitAfterAhBot(); sPlayerbotTextMgr->LoadBotTexts(); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 83a6a20b9a..76177395f6 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -269,7 +269,6 @@ class PlayerbotAIConfig bool deleteRandomBotAccounts; uint32 randomBotGuildCount, randomBotGuildSizeMax; bool deleteRandomBotGuilds; - std::vector randomBotGuilds; std::vector pvpProhibitedZoneIds; std::vector pvpProhibitedAreaIds; bool fastReactInBG; diff --git a/src/PlayerbotGuildMgr.cpp b/src/PlayerbotGuildMgr.cpp new file mode 100644 index 0000000000..3a4611d636 --- /dev/null +++ b/src/PlayerbotGuildMgr.cpp @@ -0,0 +1,312 @@ +#include "PlayerbotGuildMgr.h" +#include "Player.h" +#include "PlayerbotAIConfig.h" +#include "DatabaseEnv.h" +#include "Guild.h" +#include "GuildMgr.h" +#include "RandomPlayerbotMgr.h" +#include "ScriptMgr.h" + +PlayerbotGuildMgr::PlayerbotGuildMgr() +{ + _randomBotGuildCount = sPlayerbotAIConfig->randomBotGuildCount; + _randomBotGuildSizeMax = sPlayerbotAIConfig->randomBotGuildSizeMax; +} + +void PlayerbotGuildMgr::Init() +{ + _guildCache.clear(); + if (sPlayerbotAIConfig->deleteRandomBotGuilds) + DeleteBotGuilds(); + + LoadGuildNames(); + ValidateGuildCache(); +} + +bool PlayerbotGuildMgr::CreateGuild(Player* player, std::string guildName) +{ + Guild* guild = new Guild(); + if (!guild->Create(player, guildName)) + { + LOG_ERROR("playerbots", "Error creating guild [ {} ] with leader [ {} ]", guildName, + player->GetName()); + delete guild; + return false; + } + sGuildMgr->AddGuild(guild); + + LOG_DEBUG("playerbots", "Guild created: id={} name='{}'", guild->GetId(), guildName); + SetGuildEmblem(guild->GetId()); + + GuildCache entry; + entry.name = guildName; + entry.memberCount = 1; + entry.status = 1; + entry.maxMembers = _randomBotGuildSizeMax; + entry.faction = player->GetTeamId(); + + _guildCache[guild->GetId()] = entry; + return true; +} + +bool PlayerbotGuildMgr::SetGuildEmblem(uint32 guildId) +{ + Guild* guild = sGuildMgr->GetGuildById(guildId); + if (!guild) + return false; + + // create random emblem + uint32 st, cl, br, bc, bg; + bg = urand(0, 51); + bc = urand(0, 17); + cl = urand(0, 17); + br = urand(0, 7); + st = urand(0, 180); + + LOG_DEBUG("playerbots", + "[TABARD] new guild id={} random -> style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", + guild->GetId(), st, cl, br, bc, bg); + + // populate guild table with a random tabard design + CharacterDatabase.Execute( + "UPDATE guild SET EmblemStyle={}, EmblemColor={}, BorderStyle={}, BorderColor={}, BackgroundColor={} " + "WHERE guildid={}", + st, cl, br, bc, bg, guild->GetId()); + LOG_DEBUG("playerbots", "[TABARD] UPDATE done for guild id={}", guild->GetId()); + + // Immediate reading for log + if (QueryResult qr = CharacterDatabase.Query( + "SELECT EmblemStyle,EmblemColor,BorderStyle,BorderColor,BackgroundColor FROM guild WHERE guildid={}", + guild->GetId())) + { + Field* f = qr->Fetch(); + LOG_DEBUG("playerbots", + "[TABARD] DB check guild id={} => style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", + guild->GetId(), f[0].Get(), f[1].Get(), f[2].Get(), f[3].Get(), f[4].Get()); + } + return true; +} + + +std::string PlayerbotGuildMgr::AssignToGuild(Player* player) +{ + if (!player) + return ""; + + LOG_DEBUG("playerbots", "Assigning player [{}] to a guild", player->GetName()); + + int playerFaction = player->GetTeamId(); + std::vector partiallyfilledguilds; + partiallyfilledguilds.reserve(_guildCache.size()); + + for (auto& keyValue : _guildCache) + { + GuildCache& cached = keyValue.second; + if (cached.status == 1 && cached.faction == playerFaction) + partiallyfilledguilds.push_back(&cached); + } + + if (!partiallyfilledguilds.empty()) + { + size_t idx = static_cast(urand(0, static_cast(partiallyfilledguilds.size()) - 1)); + return (partiallyfilledguilds[idx]->name); + } + // No partial guilds: determine type and pick an available one + std::random_device rd; + std::mt19937 g(rd()); + + + std::shuffle(guildNames.begin(), guildNames.end(), g); + + for (const auto& name : guildNames) + { + bool match = false; + for (auto& keyValue : _guildCache) + { + if (keyValue.second.name == name) + match = true; + } + if (!match) + { + LOG_INFO("playerbots","Assigning player [{}] to guild [{}]", player->GetName(), name); + return name; + } + } + return ""; +} + +void PlayerbotGuildMgr::OnGuildUpdate(Guild* guild) +{ + auto it = _guildCache.find(guild->GetId()); + if (it == _guildCache.end()) + return; + + GuildCache& entry = it->second; + entry.memberCount++; + + if (entry.memberCount >= entry.maxMembers) + entry.status = 2; // Full +} + +void PlayerbotGuildMgr::ResetGuildCache() +{ + for (auto it = _guildCache.begin(); it != _guildCache.end();) + { + GuildCache& cached = it->second; + cached.memberCount = 0; + cached.faction = 2; + cached.status = 0; + } +} + +void PlayerbotGuildMgr::LoadGuildNames() +{ + LOG_INFO("playerbots", "Loading guild names from playerbots_guild_names..."); + + QueryResult result = CharacterDatabase.Query("SELECT name_id, name FROM playerbots_guild_names"); + + if (!result) + { + LOG_ERROR("playerbots", "No entries found in playerbots_guild_names. List is empty."); + return; + } + + do + { + Field* fields = result->Fetch(); + guildNames.push_back(fields[1].Get()); + } while (result->NextRow()); + LOG_INFO("playerbots", "Loaded {} guild entries from playerbots_guild_names table.", guildNames.size()); +} + +void PlayerbotGuildMgr::ValidateGuildCache() +{ + QueryResult result = CharacterDatabase.Query("SELECT guildid, name FROM guild"); + if (!result) + { + LOG_ERROR("playerbots", "No guilds found in database, resetting guild cache"); + ResetGuildCache(); + return; + } + + std::unordered_map dbGuilds; + do + { + Field* fields = result->Fetch(); + uint32 guildId = fields[0].Get(); + std::string guildName = fields[1].Get(); + guildNames.push_back(guildName); + } while (result->NextRow()); + + for (auto it = dbGuilds.begin(); it != dbGuilds.end(); it++) + { + uint32 guildId = it->first; + GuildCache cache; + cache.name = it->second; + cache.maxMembers = _randomBotGuildSizeMax; + + Guild* guild = sGuildMgr ->GetGuildById(guildId); + if (!guild) + continue; + + cache.memberCount = guild->GetMemberCount(); + ObjectGuid leaderGuid = guild->GetLeaderGUID(); + CharacterCacheEntry const* leaderEntry = sCharacterCache->GetCharacterCacheByGuid(leaderGuid); + uint32 leaderAccount = leaderEntry->AccountId; + cache.hasRealPlayer = !(sPlayerbotAIConfig->IsInRandomAccountList(leaderAccount)); + cache.faction = Player::TeamIdForRace(leaderEntry->Race); + if (cache.memberCount == 0) + cache.status = 0; // empty + else if (cache.memberCount < cache.maxMembers) + cache.status = 1; // partially filled + else + cache.status = 2; // full + + _guildCache.insert_or_assign(guildId, cache); + } +} + +void PlayerbotGuildMgr::DeleteBotGuilds() +{ + LOG_INFO("playerbots", "Deleting random bot guilds..."); + std::vector randomBots; + + PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BOT); + stmt->SetData(0, "add"); + if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt)) + { + do + { + Field* fields = result->Fetch(); + uint32 bot = fields[0].Get(); + randomBots.push_back(bot); + } while (result->NextRow()); + } + + for (std::vector::iterator i = randomBots.begin(); i != randomBots.end(); ++i) + { + if (Guild* guild = sGuildMgr->GetGuildByLeader(ObjectGuid::Create(*i))) + guild->Disband(); + } + LOG_INFO("playerbots", "Random bot guilds deleted"); +} + +bool PlayerbotGuildMgr::IsRealGuild(Player* bot) +{ + if (!bot) + return false; + uint32 guildId = bot->GetGuildId(); + if (!guildId) + return false; + + return IsRealGuild(guildId); +} + +bool PlayerbotGuildMgr::IsRealGuild(uint32 guildId) +{ + if (!guildId) + return false; + + auto it = _guildCache.find(guildId); + if (it == _guildCache.end()) + return false; + + return it->second.hasRealPlayer; +} + +class BotGuildCacheWorldScript : public WorldScript +{ + public: + + BotGuildCacheWorldScript() : WorldScript("BotGuildCacheWorldScript"), _validateTimer(0){} + + void OnStartup() override + { + sPlayerbotGuildMgr->LoadGuildNames(); + LOG_INFO("server.loading", "Bot guild cache initialized"); + sPlayerbotGuildMgr->ValidateGuildCache(); + LOG_INFO("server.loading", "Bot guild cache validated"); + } + + void OnUpdate(uint32 diff) override + { + _validateTimer += diff; + + if (_validateTimer >= _validateInterval) // Validate every hour + { + _validateTimer = 0; + sPlayerbotGuildMgr->ValidateGuildCache(); + LOG_INFO("playerbots", "Schedueled guild cache validation"); + } + return; + } + + private: + uint32 _validateInterval = HOUR*IN_MILLISECONDS; + uint32 _validateTimer; +}; + +void PlayerBotsGuildValidationScript() +{ + new BotGuildCacheWorldScript(); +} \ No newline at end of file diff --git a/src/PlayerbotGuildMgr.h b/src/PlayerbotGuildMgr.h new file mode 100644 index 0000000000..4fb99ebf5f --- /dev/null +++ b/src/PlayerbotGuildMgr.h @@ -0,0 +1,66 @@ +#ifndef _PLAYERBOT_PLAYERBOTGUILDMGR_H +#define _PLAYERBOT_PLAYERBOTGUILDMGR_H + +#include "Guild.h" +#include "Player.h" +#include "PlayerbotAI.h" + +constexpr std::array GuilderMap = +{ + GuilderType::SOLO, + GuilderType::TINY, + GuilderType::SMALL, + GuilderType::MEDIUM, + GuilderType::LARGE, + GuilderType::VERY_LARGE +}; + +class PlayerbotAI; + +class PlayerbotGuildMgr +{ +public: + static PlayerbotGuildMgr* instance() + { + static PlayerbotGuildMgr instance; + return &instance; + } + + void Init(); + std::string AssignToGuild(Player* player); + void LoadGuildNames(); + void ValidateGuildCache(); + void ResetGuildCache(); + bool CreateGuild(Player* player, std::string guildName); + void OnGuildUpdate (Guild* guild); + bool SetGuildEmblem(uint32 guildId); + void DeleteBotGuilds(); + bool IsRealGuild(uint32 guildId); + bool IsRealGuild(Player* bot); + +private: + PlayerbotGuildMgr(); + int _maxIndex; + int _randomBotGuildCount; + int _randomBotGuildSizeMax; + std::vector guildNames; + + struct GuildCache + { + std::string name; + uint8 status; + uint8 size; + uint32 maxMembers = 0; + uint32 memberCount = 0; + uint8 faction = 0; + bool hasRealPlayer = false; + }; + std::unordered_map _guildCache; +}; + + +void PlayerBotsGuildValidationScript(); + +#define sPlayerbotGuildMgr PlayerbotGuildMgr::instance() + +#endif \ No newline at end of file diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index d96851dafc..fc78461797 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -31,6 +31,7 @@ #include "PlayerbotSecurity.h" #include "PlayerbotWorldThreadProcessor.h" #include "Playerbots.h" +#include "PlayerbotGuildMgr.h" #include "RandomPlayerbotMgr.h" #include "SharedDefines.h" #include "WorldSession.h" @@ -1174,7 +1175,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (ObjectAccessor::FindConnectedPlayer(guid)) continue; uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(guid); - if (guildId && PlayerbotAI::IsRealGuild(guildId)) + if (guildId && sPlayerbotGuildMgr->IsRealGuild(guildId)) continue; AddPlayerBot(guid, master->GetSession()->GetAccountId()); messages.push_back("Add class " + std::string(charname)); diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index ed4e482636..e4c07c4b05 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -25,6 +25,7 @@ #include "Metric.h" #include "PlayerScript.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotGuildMgr.h" #include "PlayerbotWorldThreadProcessor.h" #include "RandomPlayerbotMgr.h" #include "ScriptMgr.h" @@ -482,6 +483,7 @@ void AddPlayerbotsScripts() new PlayerbotsWorldScript(); new PlayerbotsScript(); new PlayerBotsBGScript(); - AddSC_playerbots_commandscript(); + PlayerBotsGuildValidationScript(); } + diff --git a/src/RandomPlayerbotFactory.cpp b/src/RandomPlayerbotFactory.cpp index 4be2d0edfc..5a4672f5eb 100644 --- a/src/RandomPlayerbotFactory.cpp +++ b/src/RandomPlayerbotFactory.cpp @@ -11,6 +11,7 @@ #include "GuildMgr.h" #include "PlayerbotFactory.h" #include "Playerbots.h" +#include "PlayerbotGuildMgr.h" #include "ScriptMgr.h" #include "SharedDefines.h" #include "SocialMgr.h" @@ -754,187 +755,6 @@ void RandomPlayerbotFactory::CreateRandomBots() sPlayerbotAIConfig->randomBotAccounts.size(), totalRandomBotChars); } -void RandomPlayerbotFactory::CreateRandomGuilds() -{ - std::vector randomBots; - - PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BOT); - stmt->SetData(0, "add"); - if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt)) - { - do - { - Field* fields = result->Fetch(); - uint32 bot = fields[0].Get(); - randomBots.push_back(bot); - } while (result->NextRow()); - } - - if (sPlayerbotAIConfig->deleteRandomBotGuilds) - { - LOG_INFO("playerbots", "Deleting random bot guilds..."); - for (std::vector::iterator i = randomBots.begin(); i != randomBots.end(); ++i) - { - if (Guild* guild = sGuildMgr->GetGuildByLeader(ObjectGuid::Create(*i))) - guild->Disband(); - } - - LOG_INFO("playerbots", "Random bot guilds deleted"); - } - - std::unordered_set botAccounts; - botAccounts.reserve(sPlayerbotAIConfig->randomBotAccounts.size()); - for (uint32 acc : sPlayerbotAIConfig->randomBotAccounts) - botAccounts.insert(acc); - - // Recount bot guilds directly from the database (does not depend on connected bots) - uint32 guildNumber = 0; - sPlayerbotAIConfig->randomBotGuilds.clear(); - sPlayerbotAIConfig->randomBotGuilds.shrink_to_fit(); // avoids accumulating old capacity - - if (!botAccounts.empty()) - { - if (QueryResult res = CharacterDatabase.Query( - // We only retrieve what is necessary (guildid, leader account) - "SELECT g.guildid, c.account " - "FROM guild g JOIN characters c ON g.leaderguid = c.guid")) - { - do - { - Field* f = res->Fetch(); - const uint32 guildId = f[0].Get(); - const uint32 accountId = f[1].Get(); - - // Determine if guild leader's account is a bot account. - if (botAccounts.find(accountId) != botAccounts.end()) - { - ++guildNumber; - sPlayerbotAIConfig->randomBotGuilds.push_back(guildId); - } - } while (res->NextRow()); - } - } - - LOG_INFO("playerbots", "{}/{} random bot guilds exist in guild table",guildNumber, sPlayerbotAIConfig->randomBotGuildCount); - if (guildNumber >= sPlayerbotAIConfig->randomBotGuildCount) - { - LOG_DEBUG("playerbots", "No new random guilds required"); - return; - } - - // We list the available leaders (online bots, not in guilds) - GuidVector availableLeaders; - availableLeaders.reserve(randomBots.size()); // limit reallocs - for (const uint32 botLowGuid : randomBots) - { - ObjectGuid leader = ObjectGuid::Create(botLowGuid); - if (sGuildMgr->GetGuildByLeader(leader)) - { - // already GuildLeader -> ignored - continue; - } - else - { - if (Player* player = ObjectAccessor::FindPlayer(leader)) - { - if (!player->GetGuildId()) - availableLeaders.push_back(leader); - } - } - } - LOG_DEBUG("playerbots", "{} available leaders for new guilds found", availableLeaders.size()); - - // Create up to randomBotGuildCount by counting only EFFECTIVE creations - uint32 createdThisRun = 0; - for (; guildNumber < sPlayerbotAIConfig->randomBotGuildCount; /* ++guildNumber -> done only if creation */) - { - std::string const guildName = CreateRandomGuildName(); - if (guildName.empty()) - break; // no more names available in playerbots_guild_names - - if (sGuildMgr->GetGuildByName(guildName)) - continue; // name already taken, skip - - if (availableLeaders.empty()) - { - LOG_ERROR("playerbots", "No leaders for random guilds available"); - break; // no more leaders: we can no longer progress without distorting the counter - } - - uint32 index = urand(0, availableLeaders.size() - 1); - ObjectGuid leader = availableLeaders[index]; - availableLeaders.erase(availableLeaders.begin() + index); // Removes the chosen leader to avoid re-selecting it repeatedly - - Player* player = ObjectAccessor::FindPlayer(leader); - if (!player) - { - LOG_ERROR("playerbots", "ObjectAccessor Cannot find player to set leader for guild {} . Skipped...", - guildName.c_str()); - // we will try with other leaders in the next round (guildNumber is not incremented) - continue; - } - - if (player->GetGuildId()) - { - // leader already in guild -> we don't advance the counter, we move on to the next one - continue; - } - - LOG_DEBUG("playerbots", "Creating guild name='{}' leader='{}'...", guildName.c_str(), player->GetName().c_str()); - - Guild* guild = new Guild(); - if (!guild->Create(player, guildName)) - { - LOG_ERROR("playerbots", "Error creating guild [ {} ] with leader [ {} ]", guildName.c_str(), - player->GetName().c_str()); - delete guild; - continue; - } - - sGuildMgr->AddGuild(guild); - - LOG_DEBUG("playerbots", "Guild created: id={} name='{}'", guild->GetId(), guildName.c_str()); - - // create random emblem - uint32 st, cl, br, bc, bg; - bg = urand(0, 51); - bc = urand(0, 17); - cl = urand(0, 17); - br = urand(0, 7); - st = urand(0, 180); - - LOG_DEBUG("playerbots", - "[TABARD] new guild id={} random -> style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", - guild->GetId(), st, cl, br, bc, bg); - - // populate guild table with a random tabard design - CharacterDatabase.Execute( - "UPDATE guild SET EmblemStyle={}, EmblemColor={}, BorderStyle={}, BorderColor={}, BackgroundColor={} " - "WHERE guildid={}", - st, cl, br, bc, bg, guild->GetId()); - LOG_DEBUG("playerbots", "[TABARD] UPDATE done for guild id={}", guild->GetId()); - - // Immediate reading for log - if (QueryResult qr = CharacterDatabase.Query( - "SELECT EmblemStyle,EmblemColor,BorderStyle,BorderColor,BackgroundColor FROM guild WHERE guildid={}", - guild->GetId())) - { - Field* f = qr->Fetch(); - LOG_DEBUG("playerbots", - "[TABARD] DB check guild id={} => style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", - guild->GetId(), f[0].Get(), f[1].Get(), f[2].Get(), f[3].Get(), f[4].Get()); - } - - sPlayerbotAIConfig->randomBotGuilds.push_back(guild->GetId()); - // The guild is only counted if it is actually created - ++guildNumber; - ++createdThisRun; - } - - // Shows the true total and how many were created during this run - LOG_INFO("playerbots", "{} random bot guilds created this run)", createdThisRun); -} - std::string const RandomPlayerbotFactory::CreateRandomGuildName() { std::string guildName = ""; diff --git a/src/RandomPlayerbotFactory.h b/src/RandomPlayerbotFactory.h index d6c19e454b..92f2c9f33c 100644 --- a/src/RandomPlayerbotFactory.h +++ b/src/RandomPlayerbotFactory.h @@ -51,7 +51,6 @@ class RandomPlayerbotFactory Player* CreateRandomBot(WorldSession* session, uint8 cls, std::unordered_map>& names); static void CreateRandomBots(); - static void CreateRandomGuilds(); static void CreateRandomArenaTeams(ArenaType slot, uint32 count); static std::string const CreateRandomGuildName(); static uint32 CalculateTotalAccountCount(); diff --git a/src/factory/PlayerbotFactory.cpp b/src/factory/PlayerbotFactory.cpp index 50a216c970..37dd3906b2 100644 --- a/src/factory/PlayerbotFactory.cpp +++ b/src/factory/PlayerbotFactory.cpp @@ -30,6 +30,7 @@ #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" +#include "PlayerbotGuildMgr.h" #include "Playerbots.h" #include "QuestDef.h" #include "RandomItemMgr.h" @@ -3965,45 +3966,36 @@ void PlayerbotFactory::InitInventoryEquip() void PlayerbotFactory::InitGuild() { if (bot->GetGuildId()) - return; - - // bot->SaveToDB(false, false); - - // add guild tabard - if (bot->GetGuildId() && !bot->HasItemCount(5976, 1)) - StoreItem(5976, 1); - - if (sPlayerbotAIConfig->randomBotGuilds.empty()) - RandomPlayerbotFactory::CreateRandomGuilds(); - - std::vector guilds; - for (std::vector::iterator i = sPlayerbotAIConfig->randomBotGuilds.begin(); - i != sPlayerbotAIConfig->randomBotGuilds.end(); ++i) - guilds.push_back(*i); - - if (guilds.empty()) { - LOG_ERROR("playerbots", "No random guilds available"); + if (!bot->HasItemCount(5976, 1) && bot->GetLevel() > 9) + StoreItem(5976, 1); return; } - int index = urand(0, guilds.size() - 1); - uint32 guildId = guilds[index]; - Guild* guild = sGuildMgr->GetGuildById(guildId); + std::string guildName = sPlayerbotGuildMgr->AssignToGuild(bot); + if (guildName.empty()) + return; + + Guild* guild = sGuildMgr->GetGuildByName(guildName); if (!guild) { - LOG_ERROR("playerbots", "Invalid guild {}", guildId); + if (!sPlayerbotGuildMgr->CreateGuild(bot, guildName)) + LOG_ERROR("playerbots","Failed to create guild {} for bot {}", guildName, bot->GetName()); return; } - - if (guild->GetMemberSize() < urand(10, sPlayerbotAIConfig->randomBotGuildSizeMax)) - guild->AddMember(bot->GetGUID(), urand(GR_OFFICER, GR_INITIATE)); - + else + { + if (guild->AddMember(bot->GetGUID(),urand(GR_OFFICER, GR_INITIATE))) + { + LOG_DEBUG("playerbots","Bot {} joined guild {}.", bot->GetName(), guildName); + sPlayerbotGuildMgr->OnGuildUpdate(guild); + } + else + LOG_ERROR("playerbots","Bot {} failed to join guild {}.", bot->GetName(), guildName); + } // add guild tabard if (bot->GetGuildId() && bot->GetLevel() > 9 && urand(0, 4) && !bot->HasItemCount(5976, 1)) StoreItem(5976, 1); - - // bot->SaveToDB(false, false); } void PlayerbotFactory::InitImmersive() diff --git a/src/strategy/actions/GroupAction.cpp b/src/strategy/actions/GroupAction.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/strategy/actions/GroupAction.h b/src/strategy/actions/GroupAction.h new file mode 100644 index 0000000000..e69de29bb2 From 04bb31498b6d60c478892e83cc498bb7933e1ca0 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:56:08 -0800 Subject: [PATCH 18/20] fixes --- src/AiFactory.cpp | 2 +- src/PlayerbotGuildMgr.cpp | 6 +++--- src/PlayerbotGuildMgr.h | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/AiFactory.cpp b/src/AiFactory.cpp index 0d90743ff2..5d6377f466 100644 --- a/src/AiFactory.cpp +++ b/src/AiFactory.cpp @@ -668,7 +668,7 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const // nonCombatEngine->addStrategy("guild"); nonCombatEngine->addStrategy("grind", false); - if (sPlayerbotAIConfig->enableNewRpgStrategy) + if (sPlayerbotAIConfig->enableNewRpgStrategy) { nonCombatEngine->addStrategy("new rpg", false); } diff --git a/src/PlayerbotGuildMgr.cpp b/src/PlayerbotGuildMgr.cpp index 3a4611d636..58746abe64 100644 --- a/src/PlayerbotGuildMgr.cpp +++ b/src/PlayerbotGuildMgr.cpp @@ -19,7 +19,7 @@ void PlayerbotGuildMgr::Init() if (sPlayerbotAIConfig->deleteRandomBotGuilds) DeleteBotGuilds(); - LoadGuildNames(); + LoadGuildNames(); ValidateGuildCache(); } @@ -118,7 +118,7 @@ std::string PlayerbotGuildMgr::AssignToGuild(Player* player) std::shuffle(guildNames.begin(), guildNames.end(), g); - for (const auto& name : guildNames) + for (auto& name : guildNames) { bool match = false; for (auto& keyValue : _guildCache) @@ -195,7 +195,7 @@ void PlayerbotGuildMgr::ValidateGuildCache() Field* fields = result->Fetch(); uint32 guildId = fields[0].Get(); std::string guildName = fields[1].Get(); - guildNames.push_back(guildName); + dbGuilds[guildId] = guildName; } while (result->NextRow()); for (auto it = dbGuilds.begin(); it != dbGuilds.end(); it++) diff --git a/src/PlayerbotGuildMgr.h b/src/PlayerbotGuildMgr.h index 4fb99ebf5f..56dc5b4def 100644 --- a/src/PlayerbotGuildMgr.h +++ b/src/PlayerbotGuildMgr.h @@ -49,7 +49,6 @@ class PlayerbotGuildMgr { std::string name; uint8 status; - uint8 size; uint32 maxMembers = 0; uint32 memberCount = 0; uint8 faction = 0; From 28a1d83886d9de456462834cd887ca598c8e33f5 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:07:17 -0800 Subject: [PATCH 19/20] Formating fixes, and modify bot guild shuffle. --- src/PlayerbotGuildMgr.cpp | 54 ++++++++++++++++++++++++--------------- src/PlayerbotGuildMgr.h | 4 +-- src/Playerbots.cpp | 1 - 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/PlayerbotGuildMgr.cpp b/src/PlayerbotGuildMgr.cpp index 58746abe64..498a5f38e4 100644 --- a/src/PlayerbotGuildMgr.cpp +++ b/src/PlayerbotGuildMgr.cpp @@ -55,7 +55,7 @@ bool PlayerbotGuildMgr::SetGuildEmblem(uint32 guildId) if (!guild) return false; - // create random emblem + // create random emblem uint32 st, cl, br, bc, bg; bg = urand(0, 51); bc = urand(0, 17); @@ -87,7 +87,6 @@ bool PlayerbotGuildMgr::SetGuildEmblem(uint32 guildId) return true; } - std::string PlayerbotGuildMgr::AssignToGuild(Player* player) { if (!player) @@ -111,27 +110,16 @@ std::string PlayerbotGuildMgr::AssignToGuild(Player* player) size_t idx = static_cast(urand(0, static_cast(partiallyfilledguilds.size()) - 1)); return (partiallyfilledguilds[idx]->name); } - // No partial guilds: determine type and pick an available one - std::random_device rd; - std::mt19937 g(rd()); - - std::shuffle(guildNames.begin(), guildNames.end(), g); - - for (auto& name : guildNames) + for (auto key : _shuffled_guild_keys) { - bool match = false; - for (auto& keyValue : _guildCache) + if (_guildNames[key]) { - if (keyValue.second.name == name) - match = true; - } - if (!match) - { - LOG_INFO("playerbots","Assigning player [{}] to guild [{}]", player->GetName(), name); - return name; + LOG_INFO("playerbots","Assigning player [{}] to guild [{}]", player->GetName(), key); + return key; } } + LOG_ERROR("playerbots","No available guild names left."); return ""; } @@ -143,9 +131,17 @@ void PlayerbotGuildMgr::OnGuildUpdate(Guild* guild) GuildCache& entry = it->second; entry.memberCount++; - if (entry.memberCount >= entry.maxMembers) entry.status = 2; // Full + std::string guildName = guild->GetName(); + for (auto it : _guildNames) + { + if (it.first == guildName) + { + it.second = false; + break; + } + } } void PlayerbotGuildMgr::ResetGuildCache() @@ -174,9 +170,17 @@ void PlayerbotGuildMgr::LoadGuildNames() do { Field* fields = result->Fetch(); - guildNames.push_back(fields[1].Get()); + _guildNames[fields[1].Get()] = true; } while (result->NextRow()); - LOG_INFO("playerbots", "Loaded {} guild entries from playerbots_guild_names table.", guildNames.size()); + + for (const auto& pair : _guildNames) + _shuffled_guild_keys.push_back(pair.first); + + std::random_device rd; + std::mt19937 g(rd()); + + std::shuffle(_shuffled_guild_keys.begin(), _shuffled_guild_keys.end(), g); + LOG_INFO("playerbots", "Loaded {} guild entries from playerbots_guild_names table.", _guildNames.size()); } void PlayerbotGuildMgr::ValidateGuildCache() @@ -223,6 +227,14 @@ void PlayerbotGuildMgr::ValidateGuildCache() cache.status = 2; // full _guildCache.insert_or_assign(guildId, cache); + for (auto it : _guildNames) + { + if (it.first == cache.name) + { + it.second = false; + break; + } + } } } diff --git a/src/PlayerbotGuildMgr.h b/src/PlayerbotGuildMgr.h index 56dc5b4def..d05c6845c2 100644 --- a/src/PlayerbotGuildMgr.h +++ b/src/PlayerbotGuildMgr.h @@ -43,7 +43,7 @@ class PlayerbotGuildMgr int _maxIndex; int _randomBotGuildCount; int _randomBotGuildSizeMax; - std::vector guildNames; + std::unordered_map _guildNames; struct GuildCache { @@ -55,9 +55,9 @@ class PlayerbotGuildMgr bool hasRealPlayer = false; }; std::unordered_map _guildCache; + std::vector _shuffled_guild_keys; }; - void PlayerBotsGuildValidationScript(); #define sPlayerbotGuildMgr PlayerbotGuildMgr::instance() diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index e4c07c4b05..3f39d10de6 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -486,4 +486,3 @@ void AddPlayerbotsScripts() AddSC_playerbots_commandscript(); PlayerBotsGuildValidationScript(); } - From 207f5036dfd1817ac9adce061f2360e9a7c9366b Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:10:28 -0800 Subject: [PATCH 20/20] . --- src/PlayerbotGuildMgr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PlayerbotGuildMgr.cpp b/src/PlayerbotGuildMgr.cpp index 498a5f38e4..13068d7205 100644 --- a/src/PlayerbotGuildMgr.cpp +++ b/src/PlayerbotGuildMgr.cpp @@ -173,7 +173,7 @@ void PlayerbotGuildMgr::LoadGuildNames() _guildNames[fields[1].Get()] = true; } while (result->NextRow()); - for (const auto& pair : _guildNames) + for (auto& pair : _guildNames) _shuffled_guild_keys.push_back(pair.first); std::random_device rd;