From ca19548cc5f0ee5d4210d46788b5697c1433aa86 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:21:17 +0100 Subject: [PATCH 01/87] Fix transport boarding when master is on a transport (Zep/Boats) (#1830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary This PR improves Follow related behaviour when the master is on a transport (zeppelin/boat). It makes follow actions safer and less disruptive by: Detecting when the master is on a transport and handling boarding correctly Avoiding teleport-under-floor issues by using a small positional offset when teleporting the bot near the master Preventing movement conflicts between MoveSpline/MotionMaster and the transport driver by forcing a MotionMaster cleanup and MoveIdle after boarding Clearing movement flags (forward / walking) after boarding so the bot does not remain in a walking/march state Next-check delay after boarding to allow the server to update transport/position state Before this change, bots get stuck when attempting to board Fight the server-side transport movement because local MoveSpline/MotionMaster was still active Repeatedly attempt movement on every follow tick while already a passenger, causing jitter and CPU/noise This PR reduces stuck/jitter cases, avoids conflicting movement commands, and makes boarding more robust. **Key changes** Check master->GetTransport() and handle three main cases: If bot already passenger of same transport: stabilize (StopMoving, Clear(true), MoveIdle, StopMovingOnCurrentPos) and set a longer next-check delay; return false (no new movement in theory). If bot passenger of another transport: do nothing (avoid conflicting behaviour). If bot not a passenger of master transport: teleport bot near master (with offsets) and call Transport::AddPassenger(bot, true), then force: bot->StopMoving() bot->GetMotionMaster()->Clear(true) bot->GetMotionMaster()->MoveIdle() Remove movement flags MOVEMENTFLAG_FORWARD and MOVEMENTFLAG_WALKING SetNextCheckDelay to random 1000–2500 ms Log boarding with bot name, transport GUID and coordinates Preserve earlier follow logic when master is not on a transport Tests performed Manual tests on a local server: Master on boat/zeppelin -> bot teleports to a safe offset position and becomes a passenger without getting stuck Bot already passenger on same transport -> bot no longer issues movement commands and stabilizes Bot on a different transport -> no boarding attempt for master's transport (no interference) Movement flags cleared after boarding; bot stops local movement and does not fight server transport movement Now the bots follow their masters in the zeppelins and boats, although sometimes they move around a bit inside when the zeppelin starts (they must have smoked something bad). --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Base/Actions/FollowActions.cpp | 193 +++++++++++++++++++++++ src/Mgr/Travel/TravelNode.cpp | 217 ++++++-------------------- 2 files changed, 244 insertions(+), 166 deletions(-) diff --git a/src/Ai/Base/Actions/FollowActions.cpp b/src/Ai/Base/Actions/FollowActions.cpp index c27de01ffcc..48ce74ad347 100644 --- a/src/Ai/Base/Actions/FollowActions.cpp +++ b/src/Ai/Base/Actions/FollowActions.cpp @@ -5,18 +5,211 @@ #include "FollowActions.h" +#include +#include +#include + #include "Event.h" #include "Formations.h" #include "LastMovementValue.h" +#include "MotionMaster.h" #include "PlayerbotAI.h" #include "Playerbots.h" #include "ServerFacade.h" +#include "Transport.h" +#include "Map.h" + +namespace +{ + Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z) + { + if (!map || !ref) + return nullptr; + + std::array const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f }; + for (float const pz : probes) + { + if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref)) + return t; + } + + return nullptr; + } + + // Attempts to find a point on the leader's transport that is closer to the bot, + // by probing along the segment from master -> bot and returning the last point + // that is still detected as being on the expected transport. + bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref, + float masterX, float masterY, float masterZ, + float botX, float botY, float botZ, + float& outX, float& outY, float& outZ) + { + if (!map || !expectedTransport || !ref) + return false; + + uint32 const phaseMask = ref->GetPhaseMask(); + + // Ensure master is actually detected on that transport (tolerant). + if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport) + return false; + + // The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z. + float const probeZ = std::max(masterZ, botZ); + + // Adaptive step count: small platforms need tighter sampling. + float const dx2 = botX - masterX; + float const dy2 = botY - masterY; + float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2); + int32 const steps = std::clamp(static_cast(dist2d / 0.75f), 10, 28); + + float const dx = (botX - masterX) / static_cast(steps); + float const dy = (botY - masterY) / static_cast(steps); + + // Master must actually be on the expected transport for this to work. + if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport) + return false; + + float lastX = masterX; + float lastY = masterY; + bool found = false; + + for (int32 i = 1; i <= steps; ++i) + { + float const px = masterX + dx * i; + float const py = masterY + dy * i; + + Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ); + if (t != expectedTransport) + break; + + lastX = px; + lastY = py; + found = true; + } + + if (!found) + return false; + + outX = lastX; + outY = lastY; + outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat + return true; + } +} bool FollowAction::Execute(Event /*event*/) { Formation* formation = AI_VALUE(Formation*, "formation"); std::string const target = formation->GetTargetName(); + // Transport handling for moving transports only (boats/zeppelins). + Player* master = botAI->GetMaster(); + if (master && master->IsInWorld() && bot->IsInWorld() && bot->GetMapId() == master->GetMapId()) + { + Map* map = master->GetMap(); + uint32 const mapId = bot->GetMapId(); + Transport* transport = nullptr; + bool masterOnTransport = false; + + if (master->GetTransport()) + { + transport = master->GetTransport(); + masterOnTransport = true; + } + else if (map) + { + transport = GetTransportForPosTolerant(map, master, master->GetPhaseMask(), + master->GetPositionX(), master->GetPositionY(), master->GetPositionZ()); + masterOnTransport = (transport != nullptr); + } + + // Ignore static transports (elevators/trams): only keep boats/zeppelins here. + if (transport && transport->IsStaticTransport()) + transport = nullptr; + + if (transport && map && bot->GetTransport() != transport) + { + float const botProbeZ = std::max(bot->GetPositionZ(), transport->GetPositionZ()); + Transport* botSurfaceTransport = GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), + bot->GetPositionX(), bot->GetPositionY(), botProbeZ); + + if (botSurfaceTransport == transport) + { + transport->AddPassenger(bot, true); + bot->StopMovingOnCurrentPos(); + return true; + } + + float const boardingAssistDistance = 60.0f; + float const dist2d = ServerFacade::instance().GetDistance2d(bot, master); + bool const inAssist = ServerFacade::instance().IsDistanceLessOrEqualThan(dist2d, boardingAssistDistance); + + if (inAssist) + { + float destX = masterOnTransport ? master->GetPositionX() : transport->GetPositionX(); + float destY = masterOnTransport ? master->GetPositionY() : transport->GetPositionY(); + float destZ = masterOnTransport ? master->GetPositionZ() : transport->GetPositionZ(); + float edgeX = 0.0f; + float edgeY = 0.0f; + float edgeZ = 0.0f; + + if (masterOnTransport && + FindBoardingPointOnTransport(map, transport, master, + master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(), + bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + edgeX, edgeY, edgeZ)) + { + destX = edgeX; + destY = edgeY; + destZ = edgeZ; + } + + MovementPriority const priority = botAI->GetState() == BOT_STATE_COMBAT + ? MovementPriority::MOVEMENT_COMBAT + : MovementPriority::MOVEMENT_NORMAL; + + bool const movingAllowed = IsMovingAllowed(mapId, destX, destY, destZ); + bool const dupMove = IsDuplicateMove(mapId, destX, destY, destZ); + bool const waiting = IsWaitingForLastMove(priority); + + if (movingAllowed && !dupMove && !waiting) + { + if (bot->IsSitState()) + bot->SetStandState(UNIT_STAND_STATE_STAND); + + if (bot->IsNonMeleeSpellCast(true)) + { + bot->CastStop(); + botAI->InterruptSpell(); + } + + if (MotionMaster* mm = bot->GetMotionMaster()) + { + mm->MovePoint( + /*id*/ 0, + /*coords*/ destX, destY, destZ, + /*forcedMovement*/ FORCED_MOVEMENT_NONE, + /*speed*/ 0.0f, + /*orientation*/ 0.0f, + /*generatePath*/ false, + /*forceDestination*/ false); + } + else + return false; + + float delay = 1000.0f * MoveDelay(bot->GetExactDist(destX, destY, destZ)); + delay = std::clamp(delay, 0.0f, static_cast(sPlayerbotAIConfig.maxWaitForMove)); + + AI_VALUE(LastMovement&, "last movement") + .Set(mapId, destX, destY, destZ, bot->GetOrientation(), delay, priority); + ClearIdleState(); + return true; + } + } + } + } + // end unified transport handling + bool moved = false; if (!target.empty()) { diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 100fdd15560..3e304677fe3 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -1753,190 +1753,75 @@ void TravelNodeMap::generateTransportNodes() for (auto const& itr : *sObjectMgr->GetGameObjectTemplates()) { GameObjectTemplate const* data = &itr.second; - if (data && (data->type == GAMEOBJECT_TYPE_TRANSPORT || data->type == GAMEOBJECT_TYPE_MO_TRANSPORT)) - { - TransportAnimation const* animation = sTransportMgr->GetTransportAnimInfo(itr.first); + if (!data || (data->type != GAMEOBJECT_TYPE_TRANSPORT && data->type != GAMEOBJECT_TYPE_MO_TRANSPORT)) + continue; - uint32 pathId = data->moTransport.taxiPathId; - float moveSpeed = data->moTransport.moveSpeed; - if (pathId >= sTaxiPathNodesByPath.size()) - continue; + uint32 pathId = data->moTransport.taxiPathId; + float moveSpeed = data->moTransport.moveSpeed; + if (pathId >= sTaxiPathNodesByPath.size()) + continue; - TaxiPathNodeList const& path = sTaxiPathNodesByPath[pathId]; + TaxiPathNodeList const& path = sTaxiPathNodesByPath[pathId]; - std::vector ppath; - TravelNode* prevNode = nullptr; + // Keep only transports with taxi paths (boats/zeppelins). + if (path.empty()) + continue; - // Elevators/Trams - if (path.empty()) - { - if (animation) - { - TransportPathContainer aPath = animation->Path; - float timeStart; + std::vector ppath; + TravelNode* prevNode = nullptr; - for (auto& transport : WorldPosition().getGameObjectsNear(0, itr.first)) - { - prevNode = nullptr; - WorldPosition basePos(transport->mapid, transport->posX, transport->posY, transport->posZ, - transport->orientation); - WorldPosition lPos = WorldPosition(); - - for (auto& p : aPath) - { - float dx = -1 * p.second->X; - float dy = -1 * p.second->Y; - - WorldPosition pos = - WorldPosition(basePos.GetMapId(), basePos.GetPositionX() + dx, - basePos.GetPositionY() + dy, basePos.GetPositionZ() + p.second->Z, - basePos.GetOrientation()); - - if (prevNode) - { - ppath.push_back(pos); - } - - if (pos.distance(&lPos) == 0) - { - TravelNode* node = - TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - - if (!prevNode) - { - ppath.push_back(pos); - timeStart = p.second->TimeSeg; - } - else - { - float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f; - - TravelNodePath travelPath(0.1f, totalTime, (uint8)TravelNodePathType::transport, - itr.first, true); - node->setPathTo(prevNode, travelPath); - ppath.clear(); - ppath.push_back(pos); - timeStart = p.second->TimeSeg; - } - - prevNode = node; - } - - lPos = pos; - } - - if (prevNode) - { - for (auto& p : aPath) - { - float dx = -1 * p.second->X; - float dy = -1 * p.second->Y; - WorldPosition pos = - WorldPosition(basePos.GetMapId(), basePos.GetPositionX() + dx, - basePos.GetPositionY() + dy, basePos.GetPositionZ() + p.second->Z, - basePos.GetOrientation()); - - ppath.push_back(pos); - - if (pos.distance(&lPos) == 0) - { - TravelNode* node = - TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - if (node != prevNode) - { - if (p.second->TimeSeg < timeStart) - timeStart = 0; - - float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f; - - TravelNodePath travelPath(0.1f, totalTime, (uint8)TravelNodePathType::transport, - itr.first, true); - travelPath.setPath(ppath); - node->setPathTo(prevNode, travelPath); - ppath.clear(); - ppath.push_back(pos); - timeStart = p.second->TimeSeg; - } - } - - lPos = pos; - } - } - - ppath.clear(); - } - } - } - else // Boats/Zepelins - { - // Loop over the path and connect stop locations. - for (auto& p : path) - { - WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); + // Loop over the path and connect stop locations. + for (auto& p : path) + { + WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); - // if (data->displayId == 3015) - // pos.setZ(pos.getZ() + 6.0f); - // else if (data->displayId == 3031) - // pos.setZ(pos.getZ() - 17.0f); + if (prevNode) + ppath.push_back(pos); - if (prevNode) - { - ppath.push_back(pos); - } + if (p->delay > 0) + { + TravelNode* node = TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - if (p->delay > 0) - { - TravelNode* node = TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - - if (!prevNode) - { - ppath.push_back(pos); - } - else - { - TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, true); - travelPath.setPathAndCost(ppath, moveSpeed); - node->setPathTo(prevNode, travelPath); - ppath.clear(); - ppath.push_back(pos); - } - - prevNode = node; - } + if (!prevNode) + { + ppath.push_back(pos); } - - if (prevNode) + else { - // Continue from start until first stop and connect to end. - for (auto& p : path) - { - WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); + TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, true); + travelPath.setPathAndCost(ppath, moveSpeed); + node->setPathTo(prevNode, travelPath); + ppath.clear(); + ppath.push_back(pos); + } - // if (data->displayId == 3015) - // pos.setZ(pos.getZ() + 6.0f); - // else if (data->displayId == 3031) - // pos.setZ(pos.getZ() - 17.0f); + prevNode = node; + } + } + + if (!prevNode) + continue; - ppath.push_back(pos); + // Continue from start until first stop and connect to end. + for (auto& p : path) + { + WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); + ppath.push_back(pos); - if (p->delay > 0) - { - TravelNode* node = TravelNodeMap::instance().getNode(pos, nullptr, 5.0f); + if (p->delay > 0) + { + TravelNode* node = TravelNodeMap::instance().getNode(pos, nullptr, 5.0f); - if (node != prevNode) - { - TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, - true); - travelPath.setPathAndCost(ppath, moveSpeed); + if (node != prevNode) + { + TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, true); + travelPath.setPathAndCost(ppath, moveSpeed); - node->setPathTo(prevNode, travelPath); - } - } - } + node->setPathTo(prevNode, travelPath); } - ppath.clear(); } } + ppath.clear(); } } From dca0be2932b42a53a43b68f1829799342eed522e Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 13 Mar 2026 16:21:37 -0500 Subject: [PATCH 02/87] Updates to SSC Strategies (#2138) # Pull Request Most of the changes are not functional but are to modify style based on comments received to the TK PR (e.g., eliminate nesting of if statements) and leverage general boss helpers. There is some reordering of returns and other changes to try to consolidate or clean-up the code (such as removing unnecessary parameters). The strategies themselves have only minor changes. - Main tank no longer uses tangential movement for Lurker Spout, unlike other bots. The MT will just call moves directly to a position behind Lurker. This is because I found tangential movement was taking too long for the MT to get in place since it starts right in front of Lurker. - Vashj MT group Shaman will now use Grounding Totem without actually switching to the Grounding Totem strategy. I have now eliminated all strategy swaps, which I dislike because they persist after the encounter, and it's better not to mess with player strategies since presumably people are generally using Windfury or Wrath of Air for the Air Totem. - I made a ton of changes to Vashj core passing as I noticed the existing logic is nonfunctional in several ways. It generally works fine ingame, but the changes should make things much smoother. For example, the storing of the nearest trigger NPC for generators in the existing strategy is useless because it relies on insert_or_assign for an unordered map that will continue to run during the course of the core passing logic, and a similar issue exists with respect to the map to store the last time a bot held a core. The result is that there is slight movement of bots when the core is flying through the air and not held by any bot because the trigger for core passing does not fire during that period. In practice, the time is brief enough that the sequence works OK, but it looks stupid because the bots should not be moving at all. So that should be fixed. There is a known issue re: core passing that would take extreme effort to fix and I am not going to do it because it is a fringe situation. There are a couple of spots where the Tainted Elemental can rarely spawn that can result in a straight-line passing sequence to the nearest generator that is blocked by LoS. Fixing this would be extremely difficult and niche, so what you will need to do if this happens is to just command your bots to destroy the core and try again with the next spawn. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? I have attempted to streamline methods and even remove some. The strategy is admittedly somewhat performance heavy due to the need for function calls such as iterating over inventory items. However, the new version should be less performance intensive than the merged strategy--for example, there were places where all members of the raid would have their inventory checked, but I've now limited the check to only the 5 core handler bots. I've run the instance with pmon on, and there are no methods that stand out as particularly resource intensive when not in a boss encounter, and I view that as the most important thing (though I make effort to reduce performance impact during encounters also). Expensive checks that are unavoidable for the strategy to work such as grid searches are gated behind cheaper checks. --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it Enter SSC with a raid group and run the instance, including all bosses. Every boss should be killable, and every major mechanic should be addressed by bots. I will work with Dreathean to get the Wiki up soon so that should be a reference for testing strategies. ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [x] Yes (**explain below**) Only within the context of strategies, which are basically all new decision branches, and there are some tweaks here to what is currently merged. Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [ ] No - - [x] Yes (**explain why**) I'm sure if you have a large server, with multiple raid groups running the instance at the same time, the performance impact could be significant. But I have done my best to limit it, and I think some notable performance impact is unavoidable with the current framework for raid strategies. --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Only for strategies in the instance. If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [x] More complex behavior is optional and thereby configurable There should be no impact if co +ssc and nc +ssc are not added to bots. Because raid strategies are currently not removed after leaving an instance, players should manually remove them (or reset botAI). This is a general issue that needs to be addressed with the module. --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. GPT-4 because I don't like to use up my premium requests in CoPilot and I generally like it better than GPT-5 =P I use LLMs to draft code snippets but do review everything and have become less-and-less reliant over time. I don't use agent mode, only ask. For this PR, I had it do the updated version of AnyRecentCoreInInventory(), which is more complicated than before and uses indexing for each bot to consider status of only prior bots in the passing chain. Everything else either I wrote or could have written but had the AI help and just edited afterward to save time. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- .../Action/RaidSSCActions.cpp | 1430 +++++++---------- .../Action/RaidSSCActions.h | 26 +- .../Multiplier/RaidSSCMultipliers.cpp | 485 +++--- .../Multiplier/RaidSSCMultipliers.h | 13 + .../RaidSSCActionContext.h | 11 +- .../RaidSSCTriggerContext.h | 11 +- .../Strategy/RaidSSCStrategy.cpp | 9 +- .../Strategy/RaidSSCStrategy.h | 5 + .../Trigger/RaidSSCTriggers.cpp | 346 ++-- .../Trigger/RaidSSCTriggers.h | 13 +- .../Util/RaidSSCHelpers.cpp | 305 ++-- .../SerpentshrineCavern/Util/RaidSSCHelpers.h | 40 +- 12 files changed, 1184 insertions(+), 1510 deletions(-) diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp index 7aa3eda35bd..0b31a1c13d8 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp @@ -1,3 +1,8 @@ +/* + * 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 "RaidSSCActions.h" #include "RaidSSCHelpers.h" #include "AiFactory.h" @@ -19,42 +24,40 @@ bool SerpentShrineCavernEraseTimersAndTrackersAction::Execute(Event /*event*/) const ObjectGuid guid = bot->GetGUID(); bool erased = false; - if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable") && + (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0 || + hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0 || + hydrossNatureDpsWaitTimer.erase(instanceId) > 0 || + hydrossFrostDpsWaitTimer.erase(instanceId) > 0)) { - if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) - erased = true; - if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) - erased = true; - if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) - erased = true; - if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "the lurker below")) + + if (!AI_VALUE2(Unit*, "find target", "the lurker below") && + (lurkerRangedPositions.erase(guid) > 0 || + lurkerSpoutTimer.erase(instanceId) > 0)) { - if (lurkerRangedPositions.erase(guid) > 0) - erased = true; - if (lurkerSpoutTimer.erase(instanceId) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && + karathressDpsWaitTimer.erase(instanceId) > 0) { - if (karathressDpsWaitTimer.erase(instanceId) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker") && + (tidewalkerTankStep.erase(guid) > 0 || + tidewalkerRangedStep.erase(guid) > 0)) { - if (tidewalkerTankStep.erase(guid) > 0) - erased = true; - if (tidewalkerRangedStep.erase(guid) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + + if (!AI_VALUE2(Unit*, "find target", "lady vashj") && + hasReachedVashjRangedPosition.erase(guid) > 0) { - if (vashjRangedPositions.erase(guid) > 0) - erased = true; - if (hasReachedVashjRangedPosition.erase(guid) > 0) - erased = true; + erased = true; } return erased; @@ -74,21 +77,18 @@ bool UnderbogColossusEscapeToxicPoolAction::Execute(Event /*event*/) return false; float radius = dynObj->GetRadius(); - if (radius <= 0.0f) + const SpellInfo* sInfo = sSpellMgr->GetSpellInfo(dynObj->GetSpellId()); + if (radius <= 0.0f && sInfo) { - const SpellInfo* sInfo = sSpellMgr->GetSpellInfo(dynObj->GetSpellId()); - if (sInfo) + for (int e = 0; e < MAX_SPELL_EFFECTS; ++e) { - for (int e = 0; e < MAX_SPELL_EFFECTS; ++e) + auto const& eff = sInfo->Effects[e]; + if (eff.Effect == SPELL_EFFECT_SCHOOL_DAMAGE || + (eff.Effect == SPELL_EFFECT_APPLY_AURA && + eff.ApplyAuraName == SPELL_AURA_PERIODIC_DAMAGE)) { - auto const& eff = sInfo->Effects[e]; - if (eff.Effect == SPELL_EFFECT_SCHOOL_DAMAGE || - (eff.Effect == SPELL_EFFECT_APPLY_AURA && - eff.ApplyAuraName == SPELL_AURA_PERIODIC_DAMAGE)) - { - radius = eff.CalcRadius(); - break; - } + radius = eff.CalcRadius(); + break; } } } @@ -207,15 +207,13 @@ bool HydrossTheUnstablePositionFrostTankAction::Execute(Event /*event*/) } } - if (hydross->HasAura(SPELL_CORRUPTION)) + const Position& position = HYDROSS_FROST_TANK_POSITION; + if (hydross->HasAura(SPELL_CORRUPTION) && + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) { - const Position& position = HYDROSS_FROST_TANK_POSITION; - if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) - { - return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -289,15 +287,13 @@ bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) } } - if (!hydross->HasAura(SPELL_CORRUPTION)) + const Position& position = HYDROSS_NATURE_TANK_POSITION; + if (!hydross->HasAura(SPELL_CORRUPTION) && + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) { - const Position& position = HYDROSS_NATURE_TANK_POSITION; - if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) - { - return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -305,8 +301,7 @@ bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) bool HydrossTheUnstablePrioritizeElementalAddsAction::Execute(Event /*event*/) { - Unit* waterElemental = GetFirstAliveUnitByEntry(botAI, NPC_PURE_SPAWN_OF_HYDROSS); - if (waterElemental) + if (Unit* waterElemental = GetFirstAliveUnitByEntry(botAI, NPC_PURE_SPAWN_OF_HYDROSS)) { if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) MarkTargetWithSkull(bot, waterElemental); @@ -336,20 +331,10 @@ bool HydrossTheUnstableFrostPhaseSpreadOutAction::Execute(Event /*event*/) if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) return false; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || member == bot || !member->IsAlive()) - continue; - - constexpr float safeDistance = 6.0f; - constexpr uint32 minInterval = 1000; - if (bot->GetExactDist2d(member) < safeDistance) - return FleePosition(member->GetPosition(), safeDistance, minInterval); - } - } + constexpr float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); return false; } @@ -360,33 +345,17 @@ bool HydrossTheUnstableMisdirectBossToTankAction::Execute(Event /*event*/) if (!hydross) return false; - if (Group* group = bot->GetGroup()) - { - if (TryMisdirectToFrostTank(hydross, group)) - return true; - - if (TryMisdirectToNatureTank(hydross, group)) - return true; - } - - return false; + return TryMisdirectToFrostTank(hydross) || TryMisdirectToNatureTank(hydross); } bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToFrostTank( - Unit* hydross, Group* group) + Unit* hydross) { - Player* frostTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - frostTank = member; - break; - } - } + Player* frostTank = GetGroupMainTank(botAI, bot); + if (!frostTank) + return false; - if (HasNoMarkOfHydross(bot) && !hydross->HasAura(SPELL_CORRUPTION) && frostTank) + if (HasNoMarkOfHydross(bot) && !hydross->HasAura(SPELL_CORRUPTION)) { if (botAI->CanCastSpell("misdirection", frostTank)) return botAI->CastSpell("misdirection", frostTank); @@ -399,20 +368,13 @@ bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToFrostTank( } bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToNatureTank( - Unit* hydross, Group* group) + Unit* hydross) { - Player* natureTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && botAI->IsAssistTankOfIndex(member, 0, true)) - { - natureTank = member; - break; - } - } + Player* natureTank = GetGroupAssistTank(botAI, bot, 0); + if (!natureTank) + return false; - if (HasNoMarkOfCorruption(bot) && hydross->HasAura(SPELL_CORRUPTION) && natureTank) + if (HasNoMarkOfCorruption(bot) && hydross->HasAura(SPELL_CORRUPTION)) { if (botAI->CanCastSpell("misdirection", natureTank)) return botAI->CastSpell("misdirection", natureTank); @@ -480,33 +442,28 @@ bool HydrossTheUnstableManageTimersAction::Execute(Event /*event*/) const time_t now = std::time(nullptr); bool changed = false; + if (!hydross->HasAura(SPELL_CORRUPTION)) { - if (hydrossFrostDpsWaitTimer.try_emplace(instanceId, now).second) + if (hydrossFrostDpsWaitTimer.try_emplace(instanceId, now).second || + hydrossNatureDpsWaitTimer.erase(instanceId) > 0 || + hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) changed = true; - if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) + + if (HasMarkOfHydrossAt100Percent(bot) && + hydrossChangeToNaturePhaseTimer.try_emplace(instanceId, now).second) changed = true; - if (HasMarkOfHydrossAt100Percent(bot)) - { - if (hydrossChangeToNaturePhaseTimer.try_emplace(instanceId, now).second) - changed = true; - } } else { - if (hydrossNatureDpsWaitTimer.try_emplace(instanceId, now).second) + if (hydrossNatureDpsWaitTimer.try_emplace(instanceId, now).second || + hydrossFrostDpsWaitTimer.erase(instanceId) > 0 || + hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) changed = true; - if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) + + if (HasMarkOfCorruptionAt100Percent(bot) && + hydrossChangeToFrostPhaseTimer.try_emplace(instanceId, now).second) changed = true; - if (HasMarkOfCorruptionAt100Percent(bot)) - { - if (hydrossChangeToFrostPhaseTimer.try_emplace(instanceId, now).second) - changed = true; - } } return changed; @@ -521,17 +478,19 @@ bool TheLurkerBelowRunAroundBehindBossAction::Execute(Event /*event*/) if (!lurker) return false; - float radius = frand(20.0f, 21.0f); + float radius = frand(19.0f, 20.0f); float botAngle = std::atan2( bot->GetPositionY() - lurker->GetPositionY(), bot->GetPositionX() - lurker->GetPositionX()); float relativeAngle = Position::NormalizeOrientation(botAngle - lurker->GetOrientation()); constexpr float safeArc = M_PI / 2.0f; - if (std::fabs(Position::NormalizeOrientation(relativeAngle - M_PI)) > safeArc / 2.0f) + if (!botAI->IsMainTank(bot) && + std::fabs(Position::NormalizeOrientation(relativeAngle - M_PI)) > safeArc / 2.0f) { float tangentAngle = botAngle + (relativeAngle > M_PI ? -0.1f : 0.1f); float moveX = lurker->GetPositionX() + radius * std::cos(tangentAngle); float moveY = lurker->GetPositionY() + radius * std::sin(tangentAngle); + botAI->Reset(); return MoveTo(SSC_MAP_ID, moveX, moveY, lurker->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); @@ -541,6 +500,7 @@ bool TheLurkerBelowRunAroundBehindBossAction::Execute(Event /*event*/) float behindAngle = lurker->GetOrientation() + M_PI + frand(-0.5f, 0.5f) * safeArc; float targetX = lurker->GetPositionX() + radius * std::cos(behindAngle); float targetY = lurker->GetPositionY() + radius * std::sin(behindAngle); + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) { botAI->Reset(); @@ -611,7 +571,7 @@ bool TheLurkerBelowSpreadRangedInArcAction::Execute(Event /*event*/) float angle = (count == 1) ? arcCenter : (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); - float radius = 28.0f; + constexpr float radius = 27.0f; float targetX = lurker->GetPositionX() + radius * std::sin(angle); float targetY = lurker->GetPositionY() + radius * std::cos(angle); @@ -638,38 +598,24 @@ bool TheLurkerBelowSpreadRangedInArcAction::Execute(Event /*event*/) // the first 3 will each pick up 1 Guardian bool TheLurkerBelowTanksPickUpAddsAction::Execute(Event /*event*/) { - Player* mainTank = nullptr; - Player* firstAssistTank = nullptr; - Player* secondAssistTank = nullptr; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if (!mainTank && botAI->IsMainTank(member)) - mainTank = member; - else if (!firstAssistTank && botAI->IsAssistTankOfIndex(member, 0, true)) - firstAssistTank = member; - else if (!secondAssistTank && botAI->IsAssistTankOfIndex(member, 1, true)) - secondAssistTank = member; - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); if (!mainTank || !firstAssistTank || !secondAssistTank) return false; std::vector guardians; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto guid : npcs) + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + + for (auto guid : attackers) { Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_COILFANG_GUARDIAN) + if (unit && unit->IsAlive() && + unit->GetEntry() == NPC_COILFANG_GUARDIAN) + { guardians.push_back(unit); + } } if (guardians.size() < 3) @@ -692,7 +638,8 @@ bool TheLurkerBelowTanksPickUpAddsAction::Execute(Event /*event*/) { MarkTargetWithIcon(bot, guardian, rtiIndices[i]); SetRtiTarget(botAI, rtiNames[i], guardian); - if (bot->GetTarget() != guardian->GetGUID()) + + if (bot->GetVictim() != guardian) return Attack(guardian); } } @@ -709,18 +656,24 @@ bool TheLurkerBelowManageSpoutTimerAction::Execute(Event /*event*/) const uint32 instanceId = lurker->GetMap()->GetInstanceId(); const time_t now = std::time(nullptr); + bool changed = false; + auto it = lurkerSpoutTimer.find(instanceId); if (it != lurkerSpoutTimer.end() && it->second <= now) { lurkerSpoutTimer.erase(it); + changed = true; it = lurkerSpoutTimer.end(); } const time_t spoutCastTime = 20; if (IsLurkerCastingSpout(lurker) && it == lurkerSpoutTimer.end()) + { lurkerSpoutTimer.try_emplace(instanceId, now + spoutCastTime); + changed = true; + } - return false; + return changed; } // Leotheras the Blind @@ -737,15 +690,16 @@ bool LeotherasTheBlindTargetSpellbindersAction::Execute(Event /*event*/) // Use tank strategy for Demon Form and DPS strategy for Human Form bool LeotherasTheBlindDemonFormTankAttackBossAction::Execute(Event /*event*/) { + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + Unit* innerDemon = nullptr; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) + for (auto guid : attackers) { Unit* unit = botAI->GetUnit(guid); Creature* creature = unit ? unit->ToCreature() : nullptr; - if (creature && creature->GetEntry() == NPC_INNER_DEMON - && creature->GetSummonerGUID() == bot->GetGUID()) + if (creature && creature->GetEntry() == NPC_INNER_DEMON && + creature->GetSummonerGUID() == bot->GetGUID()) { innerDemon = creature; break; @@ -755,7 +709,7 @@ bool LeotherasTheBlindDemonFormTankAttackBossAction::Execute(Event /*event*/) if (innerDemon) return false; - if (Unit* leotherasDemon = GetActiveLeotherasDemon(botAI)) + if (Unit* leotherasDemon = GetActiveLeotherasDemon(bot)) { MarkTargetWithSquare(bot, leotherasDemon); SetRtiTarget(botAI, "square", leotherasDemon); @@ -781,7 +735,7 @@ bool LeotherasTheBlindMeleeTanksDontAttackDemonFormAction::Execute(Event /*event bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) { constexpr float safeDistFromBoss = 15.0f; - Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasHuman = GetLeotherasHuman(bot); if (leotherasHuman && bot->GetExactDist2d(leotherasHuman) < safeDistFromBoss && leotherasHuman->GetVictim() != bot) { @@ -789,11 +743,10 @@ bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) return FleePosition(leotherasHuman->GetPosition(), safeDistFromBoss, minInterval); } - Group* group = bot->GetGroup(); - if (!group) + if (!GetActiveLeotherasDemon(bot)) return false; - if (GetActiveLeotherasDemon(botAI)) + if (Group* group = bot->GetGroup()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -804,9 +757,9 @@ bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) constexpr uint32 minInterval = 0; if (GetLeotherasDemonFormTank(bot) == member) { - constexpr float safeDistFromTank = 10.0f; - if (bot->GetExactDist2d(member) < safeDistFromTank) - return FleePosition(member->GetPosition(), safeDistFromTank, minInterval); + constexpr float safeDistFromMember = 10.0f; + if (bot->GetExactDist2d(member) < safeDistFromMember) + return FleePosition(member->GetPosition(), safeDistFromMember, minInterval); } else { @@ -822,7 +775,7 @@ bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) bool LeotherasTheBlindRunAwayFromWhirlwindAction::Execute(Event /*event*/) { - if (Unit* leotherasHuman = GetLeotherasHuman(botAI)) + if (Unit* leotherasHuman = GetLeotherasHuman(bot)) { float currentDistance = bot->GetExactDist2d(leotherasHuman); constexpr float safeDistance = 25.0f; @@ -843,7 +796,7 @@ bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) if (botAI->CanCastSpell("cloak of shadows", bot)) return botAI->CastSpell("cloak of shadows", bot); - Unit* leotheras = GetPhase2LeotherasDemon(botAI); + Unit* leotheras = GetPhase2LeotherasDemon(bot); if (!leotheras) return false; @@ -853,12 +806,8 @@ bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) float currentDistance = bot->GetExactDist2d(demonVictim); constexpr float safeDistance = 10.0f; - if (currentDistance < safeDistance) - { - botAI->Reset(); - if (demonVictim != bot) - return MoveAway(demonVictim, safeDistance - currentDistance); - } + if (currentDistance < safeDistance && demonVictim != bot) + return MoveAway(demonVictim, safeDistance - currentDistance); return false; } @@ -866,15 +815,16 @@ bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) // Hardcoded actions for healers and bear tanks to kill Inner Demons bool LeotherasTheBlindDestroyInnerDemonAction::Execute(Event /*event*/) { + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + Unit* innerDemon = nullptr; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) + for (auto guid : attackers) { Unit* unit = botAI->GetUnit(guid); Creature* creature = unit ? unit->ToCreature() : nullptr; - if (creature && creature->GetEntry() == NPC_INNER_DEMON - && creature->GetSummonerGUID() == bot->GetGUID()) + if (creature && creature->GetEntry() == NPC_INNER_DEMON && + creature->GetSummonerGUID() == bot->GetGUID()) { innerDemon = creature; break; @@ -910,37 +860,34 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleFeralTankStrategy(Unit* inn bot->RemoveAura(SPELL_BEAR_FORM); bool casted = false; - if (!bot->HasAura(SPELL_CAT_FORM) && botAI->CanCastSpell("cat form", bot)) - { - if (botAI->CastSpell("cat form", bot)) - casted = true; - } - if (botAI->CanCastSpell("berserk", bot)) - { - if (botAI->CastSpell("berserk", bot)) - casted = true; - } - if (bot->GetPower(POWER_ENERGY) < 30 && botAI->CanCastSpell("tiger's fury", bot)) - { - if (botAI->CastSpell("tiger's fury", bot)) - casted = true; - } - if (bot->GetComboPoints() >= 4 && botAI->CanCastSpell("ferocious bite", innerDemon)) - { - if (botAI->CastSpell("ferocious bite", innerDemon)) - casted = true; - } + + if (!bot->HasAura(SPELL_CAT_FORM) && + botAI->CanCastSpell("cat form", bot) && + botAI->CastSpell("cat form", bot)) + casted = true; + + if (botAI->CanCastSpell("berserk", bot) && + botAI->CastSpell("berserk", bot)) + casted = true; + + if (bot->GetPower(POWER_ENERGY) < 30 && + botAI->CanCastSpell("tiger's fury", bot) && + botAI->CastSpell("tiger's fury", bot)) + casted = true; + + if (bot->GetComboPoints() >= 4 && + botAI->CanCastSpell("ferocious bite", innerDemon) && + botAI->CastSpell("ferocious bite", innerDemon)) + casted = true; + if (bot->GetComboPoints() == 0 && innerDemon->GetHealthPct() > 25.0f && - botAI->CanCastSpell("rake", innerDemon)) - { - if (botAI->CastSpell("rake", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("mangle (cat)", innerDemon)) - { - if (botAI->CastSpell("mangle (cat)", innerDemon)) - casted = true; - } + botAI->CanCastSpell("rake", innerDemon) && + botAI->CastSpell("rake", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("mangle (cat)", innerDemon) && + botAI->CastSpell("mangle (cat)", innerDemon)) + casted = true; return casted; } @@ -953,52 +900,44 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerD bot->RemoveAura(SPELL_TREE_OF_LIFE); bool casted = false; - if (botAI->CanCastSpell("barkskin", bot)) - { - if (botAI->CastSpell("barkskin", bot)) - casted = true; - } - if (botAI->CanCastSpell("wrath", innerDemon)) - { - if (botAI->CastSpell("wrath", innerDemon)) - casted = true; - } + + if (botAI->CanCastSpell("barkskin", bot) && + botAI->CastSpell("barkskin", bot)) + casted = true; + + if (botAI->CanCastSpell("wrath", innerDemon) && + botAI->CastSpell("wrath", innerDemon)) + casted = true; return casted; } else if (bot->getClass() == CLASS_PALADIN) { bool casted = false; - if (botAI->CanCastSpell("avenging wrath", bot)) - { - if (botAI->CastSpell("avenging wrath", bot)) - casted = true; - } - if (botAI->CanCastSpell("consecration", bot)) - { - if (botAI->CastSpell("consecration", bot)) - casted = true; - } - if (botAI->CanCastSpell("exorcism", innerDemon)) - { - if (botAI->CastSpell("exorcism", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("hammer of wrath", innerDemon)) - { - if (botAI->CastSpell("hammer of wrath", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("holy shock", innerDemon)) - { - if (botAI->CastSpell("holy shock", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("judgment of light", innerDemon)) - { - if (botAI->CastSpell("judgment of light", innerDemon)) - casted = true; - } + + if (botAI->CanCastSpell("avenging wrath", bot) && + botAI->CastSpell("avenging wrath", bot)) + casted = true; + + if (botAI->CanCastSpell("consecration", bot) && + botAI->CastSpell("consecration", bot)) + casted = true; + + if (botAI->CanCastSpell("exorcism", innerDemon) && + botAI->CastSpell("exorcism", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("hammer of wrath", innerDemon) && + botAI->CastSpell("hammer of wrath", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("holy shock", innerDemon) && + botAI->CastSpell("holy shock", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("judgement of light", innerDemon) && + botAI->CastSpell("judgement of light", innerDemon)) + casted = true; return casted; } @@ -1010,21 +949,18 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerD else if (bot->getClass() == CLASS_SHAMAN) { bool casted = false; - if (botAI->CanCastSpell("earth shock", innerDemon)) - { - if (botAI->CastSpell("earth shock", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("chain lightning", innerDemon)) - { - if (botAI->CastSpell("chain lightning", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("lightning bolt", innerDemon)) - { - if (botAI->CastSpell("lightning bolt", innerDemon)) - casted = true; - } + + if (botAI->CanCastSpell("earth shock", innerDemon) && + botAI->CastSpell("earth shock", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("chain lightning", innerDemon) && + botAI->CastSpell("chain lightning", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("lightning bolt", innerDemon) && + botAI->CastSpell("lightning bolt", innerDemon)) + casted = true; return casted; } @@ -1035,7 +971,7 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerD // Everybody except the Warlock tank should focus on Leotheras in Phase 3 bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/) { - Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasHuman = GetLeotherasHuman(bot); if (!leotherasHuman) return false; @@ -1045,23 +981,27 @@ bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/ if (bot->GetTarget() != leotherasHuman->GetGUID()) return Attack(leotherasHuman); - Unit* leotherasDemon = GetPhase3LeotherasDemon(botAI); - if (leotherasDemon) + Unit* leotherasDemon = GetPhase3LeotherasDemon(bot); + if (!leotherasDemon) + return false; + + if (leotherasHuman->GetVictim() != bot) + return false; + + Unit* demonTarget = leotherasDemon->GetVictim(); + if (!demonTarget) + return false; + + constexpr float safeDistanceFromDemon = 20.0f; + if (leotherasHuman->GetExactDist2d(demonTarget) < safeDistanceFromDemon) { - if (leotherasHuman->GetVictim() == bot) - { - Unit* demonTarget = leotherasDemon->GetVictim(); - if (demonTarget && leotherasHuman->GetExactDist2d(demonTarget) < 20.0f) - { - float angle = atan2(bot->GetPositionY() - demonTarget->GetPositionY(), - bot->GetPositionX() - demonTarget->GetPositionX()); - float targetX = bot->GetPositionX() + 25.0f * std::cos(angle); - float targetY = bot->GetPositionY() + 25.0f * std::sin(angle); + float angle = atan2(bot->GetPositionY() - demonTarget->GetPositionY(), + bot->GetPositionX() - demonTarget->GetPositionX()); + float targetX = bot->GetPositionX() + safeDistanceFromDemon * std::cos(angle); + float targetY = bot->GetPositionY() + safeDistanceFromDemon * std::sin(angle); - return MoveTo(SSC_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, - false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - } + return MoveTo(SSC_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; @@ -1070,28 +1010,13 @@ bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/ // Misdirect to Warlock tank or to main tank if there is no Warlock tank bool LeotherasTheBlindMisdirectBossToDemonFormTankAction::Execute(Event /*event*/) { - Unit* leotherasDemon = GetActiveLeotherasDemon(botAI); + Unit* leotherasDemon = GetActiveLeotherasDemon(bot); if (!leotherasDemon) return false; - Player* demonFormTank = GetLeotherasDemonFormTank(bot); - Player* targetTank = demonFormTank; - + Player* targetTank = GetLeotherasDemonFormTank(bot); if (!targetTank) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - targetTank = member; - break; - } - } - } - } + targetTank = GetGroupMainTank(botAI, bot); if (!targetTank) return false; @@ -1117,43 +1042,37 @@ bool LeotherasTheBlindManageDpsWaitTimersAction::Execute(Event /*event*/) bool changed = false; // Encounter start/reset: clear all timers - if (leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + if (leotheras->HasAura(SPELL_LEOTHERAS_BANISHED) && + (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0 || + leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0 || + leotherasFinalPhaseDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (leotherasFinalPhaseDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } // Human Phase - Unit* leotherasHuman = GetLeotherasHuman(botAI); - Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); - if (leotherasHuman && !leotherasPhase3Demon) + Unit* leotherasHuman = GetLeotherasHuman(bot); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(bot); + if (leotherasHuman && !leotherasPhase3Demon && + (leotherasHumanFormDpsWaitTimer.try_emplace(instanceId, now).second || + leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasHumanFormDpsWaitTimer.try_emplace(instanceId, now).second) - changed = true; - if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } // Demon Phase - else if (Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI)) + else if (GetPhase2LeotherasDemon(bot) && + (leotherasDemonFormDpsWaitTimer.try_emplace(instanceId, now).second || + leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasDemonFormDpsWaitTimer.try_emplace(instanceId, now).second) - changed = true; - if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } // Final Phase (<15% HP) - else if (leotherasHuman && leotherasPhase3Demon) + else if (leotherasHuman && leotherasPhase3Demon && + (leotherasFinalPhaseDpsWaitTimer.try_emplace(instanceId, now).second || + leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0 || + leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasFinalPhaseDpsWaitTimer.try_emplace(instanceId, now).second) - changed = true; - if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } return changed; @@ -1367,41 +1286,17 @@ bool FathomLordKarathressMisdirectBossesToTanksAction::Execute(Event /*event*/) if (hunterIndex == 0) { bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 0, false)) - { - tankTarget = member; - break; - } - } + tankTarget = GetGroupAssistTank(botAI, bot, 0); } else if (hunterIndex == 1) { bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 2, false)) - { - tankTarget = member; - break; - } - } + tankTarget = GetGroupAssistTank(botAI, bot, 2); } else if (hunterIndex == 2) { bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 1, false)) - { - tankTarget = member; - break; - } - } + tankTarget = GetGroupAssistTank(botAI, bot, 1); } if (!bossTarget || !tankTarget) @@ -1529,11 +1424,9 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) bool FathomLordKarathressManageDpsTimerAction::Execute(Event /*event*/) { Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); - if (!karathress) - return false; - - karathressDpsWaitTimer.try_emplace( - karathress->GetMap()->GetInstanceId(), std::time(nullptr)); + if (karathress && karathressDpsWaitTimer.try_emplace( + karathress->GetMap()->GetInstanceId(), std::time(nullptr)).second) + return true; return false; } @@ -1546,20 +1439,7 @@ bool MorogrimTidewalkerMisdirectBossToMainTankAction::Execute(Event /*event*/) if (!tidewalker) 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 && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); if (!mainTank) return false; @@ -1757,8 +1637,7 @@ bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) // Phase 3: No fixed position, but move Vashj away from Enchanted Elementals else if (IsLadyVashjInPhase3(botAI)) { - Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); - if (enchanted) + if (Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental")) { float currentDistance = bot->GetExactDist2d(enchanted); constexpr float safeDistance = 10.0f; @@ -1775,70 +1654,52 @@ bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) bool LadyVashjPhase1SpreadRangedInArcAction::Execute(Event /*event*/) { std::vector spreadMembers; - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && GET_PLAYERBOT_AI(member)) - { - if (botAI->IsRanged(member)) - spreadMembers.push_back(member); - } - } + Player* member = ref->GetSource(); + if (member && GET_PLAYERBOT_AI(member) && botAI->IsRanged(member)) + spreadMembers.push_back(member); } const ObjectGuid guid = bot->GetGUID(); - - auto itPos = vashjRangedPositions.find(guid); auto itReached = hasReachedVashjRangedPosition.find(guid); - if (itPos == vashjRangedPositions.end()) - { - auto it = std::find(spreadMembers.begin(), spreadMembers.end(), bot); - size_t botIndex = (it != spreadMembers.end()) ? - std::distance(spreadMembers.begin(), it) : 0; - size_t count = spreadMembers.size(); - if (count == 0) - return false; - - const Position& center = VASHJ_PLATFORM_CENTER_POSITION; - constexpr float minRadius = 20.0f; - constexpr float maxRadius = 30.0f; - - constexpr float arcCenter = M_PI / 2.0f; // North - constexpr float arcSpan = M_PI; // 180° - constexpr float arcStart = arcCenter - arcSpan / 2.0f; - float angle; - if (count == 1) - angle = arcCenter; - else - angle = arcStart + (static_cast(botIndex) / (count - 1)) * arcSpan; + auto it = std::find(spreadMembers.begin(), spreadMembers.end(), bot); + size_t botIndex = (it != spreadMembers.end()) ? + std::distance(spreadMembers.begin(), it) : 0; + size_t count = spreadMembers.size(); + if (count == 0) + return false; - float radius = frand(minRadius, maxRadius); - float targetX = center.GetPositionX() + radius * std::cos(angle); - float targetY = center.GetPositionY() + radius * std::sin(angle); + constexpr float arcCenter = M_PI / 2.0f; // North + constexpr float arcSpan = M_PI; // 180° + constexpr float arcStart = arcCenter - arcSpan / 2.0f; - auto res = vashjRangedPositions.try_emplace(guid, Position(targetX, targetY, center.GetPositionZ())); - itPos = res.first; - hasReachedVashjRangedPosition.try_emplace(guid, false); - itReached = hasReachedVashjRangedPosition.find(guid); - } + float angle; + if (count == 1) + angle = arcCenter; + else + angle = arcStart + (static_cast(botIndex) / (count - 1)) * arcSpan; - if (itPos == vashjRangedPositions.end()) - return false; + const Position& center = VASHJ_PLATFORM_CENTER_POSITION; + float radius = 25.0f; + float targetX = center.GetPositionX() + radius * std::cos(angle); + float targetY = center.GetPositionY() + radius * std::sin(angle); + float targetZ = center.GetPositionZ(); - Position position = itPos->second; if (itReached == hasReachedVashjRangedPosition.end() || !(itReached->second)) { - if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) { - return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, false, + hasReachedVashjRangedPosition.try_emplace(guid, false); + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - if (itReached != hasReachedVashjRangedPosition.end()) - itReached->second = true; + hasReachedVashjRangedPosition[guid] = true; } return false; @@ -1847,37 +1708,19 @@ bool LadyVashjPhase1SpreadRangedInArcAction::Execute(Event /*event*/) // For absorbing Shock Burst bool LadyVashjSetGroundingTotemInMainTankGroupAction::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 && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); if (!mainTank) return false; - if (bot->GetExactDist2d(mainTank) > 25.0f) - { - return MoveInside(SSC_MAP_ID, mainTank->GetPositionX(), mainTank->GetPositionY(), - mainTank->GetPositionZ(), 20.0f, MovementPriority::MOVEMENT_COMBAT); - } - - if (!botAI->HasStrategy("grounding", BotState::BOT_STATE_COMBAT)) - botAI->ChangeStrategy("+grounding", BotState::BOT_STATE_COMBAT); + if (mainTank->HasAura(SPELL_GROUNDING_TOTEM_EFFECT)) + return false; - if (!bot->HasAura(SPELL_GROUNDING_TOTEM_EFFECT) && - botAI->CanCastSpell("grounding totem", bot)) - return botAI->CastSpell("grounding totem", bot); + constexpr float distFromTank = 25.0f; + if (bot->GetDistance(mainTank) > distFromTank) + return MoveTo(mainTank, distFromTank, MovementPriority::MOVEMENT_COMBAT); - return false; + return botAI->CanCastSpell("grounding totem", bot) && + botAI->CastSpell("grounding totem", bot); } bool LadyVashjMisdirectBossToMainTankAction::Execute(Event /*event*/) @@ -1886,20 +1729,7 @@ bool LadyVashjMisdirectBossToMainTankAction::Execute(Event /*event*/) if (!vashj) 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 && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); if (!mainTank) return false; @@ -1919,19 +1749,8 @@ bool LadyVashjStaticChargeMoveAwayFromGroupAction::Execute(Event /*event*/) return false; // If the main tank has Static Charge, other group members should move away - Player* mainTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member) && - member->HasAura(SPELL_STATIC_CHARGE)) - { - mainTank = member; - break; - } - } - - if (mainTank && bot != mainTank) + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && bot != mainTank && mainTank->HasAura(SPELL_STATIC_CHARGE)) { float currentDistance = bot->GetExactDist2d(mainTank); constexpr float safeDistance = 11.0f; @@ -1979,7 +1798,7 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) } auto const& attackers = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); Unit* target = nullptr; Unit* enchanted = nullptr; Unit* elite = nullptr; @@ -2019,7 +1838,7 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) break; case NPC_TOXIC_SPOREBAT: - if (!sporebat || bot->GetExactDist2d(unit) < bot->GetExactDist2d(sporebat)) + if (!sporebat || bot->GetDistance(unit) < bot->GetDistance(sporebat)) sporebat = unit; break; @@ -2067,10 +1886,10 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) SetRtiTarget(botAI, "diamond", vashj); targets = { vashj }; } - else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + else if (botAI->HasCheat(BotCheatMask::raid) && + botAI->IsAssistTankOfIndex(bot, 0, true)) { - if (botAI->HasCheat(BotCheatMask::raid)) - targets = { strider, elite, enchanted, vashj }; + targets = { strider, elite, enchanted, vashj }; } else targets = { elite, strider, enchanted, vashj }; @@ -2114,22 +1933,8 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) return Attack(target); // If bots have wandered too far from the center, move them back - if (bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 55.0f) - { - Player* designatedLooter = GetDesignatedCoreLooter(bot->GetGroup(), botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(bot->GetGroup(), botAI); - // A bot will not move back to the middle if (1) there is a Tainted Elemental, and - // (2) the bot is either the designated looter or the first core passer - if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) - { - if ((designatedLooter && designatedLooter == bot) || - (firstCorePasser && firstCorePasser == bot)) - return false; - } - - return MoveInside(SSC_MAP_ID, center.GetPositionX(), center.GetPositionY(), - center.GetPositionZ(), 40.0f, MovementPriority::MOVEMENT_COMBAT); - } + if (bot->GetExactDist2d(vashj) > maxPursueRange) + return MoveTo(vashj, maxPursueRange - 10.0f, MovementPriority::MOVEMENT_FORCED); return false; } @@ -2144,24 +1949,11 @@ bool LadyVashjMisdirectStriderToFirstAssistTankAction::Execute(Event /*event*/) if (bot->getClass() != CLASS_HUNTER) return false; - Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + Unit* strider = AI_VALUE2(Unit*, "find target", "coilfang strider"); if (!strider) return false; - Player* firstAssistTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && botAI->IsAssistTankOfIndex(member, 0, true)) - { - firstAssistTank = member; - break; - } - } - } - + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); if (!firstAssistTank || strider->GetVictim() == firstAssistTank) return false; @@ -2180,7 +1972,7 @@ bool LadyVashjTankAttackAndMoveAwayStriderAction::Execute(Event /*event*/) if (!vashj) return false; - Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + Unit* strider = AI_VALUE2(Unit*, "find target", "coilfang strider"); if (!strider) return false; @@ -2192,53 +1984,41 @@ bool LadyVashjTankAttackAndMoveAwayStriderAction::Execute(Event /*event*/) if (!bot->HasAura(SPELL_FEAR_WARD)) bot->AddAura(SPELL_FEAR_WARD, bot); - if (botAI->IsAssistTankOfIndex(bot, 0, true) && - bot->GetTarget() != strider->GetGUID()) + if (botAI->IsAssistTankOfIndex(bot, 0, true) && bot->GetTarget() != strider->GetGUID()) return Attack(strider); - if (strider->GetVictim() == bot) - { - float currentDistance = bot->GetExactDist2d(vashj); - constexpr float safeDistance = 28.0f; - - if (currentDistance < safeDistance) - return MoveAway(vashj, safeDistance - currentDistance); - } + float currentDistance = bot->GetExactDist2d(vashj); + constexpr float safeDistance = 28.0f; + if (strider->GetVictim() == bot && currentDistance < safeDistance) + return MoveAway(vashj, safeDistance - currentDistance); return false; } // Don't move away if raid cheats are enabled, or in any case if the bot is a tank - if (!botAI->HasCheat(BotCheatMask::raid) || !botAI->IsTank(bot)) + if (!botAI->HasCheat(BotCheatMask::raid)) { float currentDistance = bot->GetExactDist2d(strider); constexpr float safeDistance = 20.0f; - if (currentDistance < safeDistance) + if (!botAI->IsTank(bot) && currentDistance < safeDistance) return MoveAway(strider, safeDistance - currentDistance); - } - - // Try to root/slow the Strider if it is not tankable (poor man's kiting strategy) - if (!botAI->HasCheat(BotCheatMask::raid)) - { - if (!strider->HasAura(SPELL_HEAVY_NETHERWEAVE_NET)) - { - Item* net = bot->GetItemByEntry(ITEM_HEAVY_NETHERWEAVE_NET); - if (net && botAI->HasItemInInventory(ITEM_HEAVY_NETHERWEAVE_NET) && - botAI->CanCastSpell("heavy netherweave net", strider)) - return botAI->CastSpell("heavy netherweave net", strider); - } + // Try to root/slow the Strider if it is not tankable (poor man's kiting strategy) if (!botAI->HasAura("frost shock", strider) && bot->getClass() == CLASS_SHAMAN && botAI->CanCastSpell("frost shock", strider)) + { return botAI->CastSpell("frost shock", strider); - - if (!strider->HasAura(SPELL_CURSE_OF_EXHAUSTION) && bot->getClass() == CLASS_WARLOCK && - botAI->CanCastSpell("curse of exhaustion", strider)) + } + else if (!strider->HasAura(SPELL_CURSE_OF_EXHAUSTION) && bot->getClass() == CLASS_WARLOCK && + botAI->CanCastSpell("curse of exhaustion", strider)) + { return botAI->CastSpell("curse of exhaustion", strider); - - if (!strider->HasAura(SPELL_SLOW) && bot->getClass() == CLASS_MAGE && - botAI->CanCastSpell("slow", strider)) + } + else if (!strider->HasAura(SPELL_SLOW) && bot->getClass() == CLASS_MAGE && + botAI->CanCastSpell("slow", strider)) + { return botAI->CastSpell("slow", strider); + } } return false; @@ -2252,7 +2032,7 @@ bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) if (!tainted) return false; - if (bot->GetExactDist2d(tainted) >= 10.0f) + if (bot->GetExactDist2d(tainted) > 10.0f) { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); @@ -2276,84 +2056,55 @@ bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) return false; } -bool LadyVashjLootTaintedCoreAction::Execute(Event) +bool LadyVashjLootTaintedCoreAction::Execute(Event /*event*/) { Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); if (!vashj) return false; - auto const& corpses = context->GetValue("nearest corpses")->Get(); - const float maxLootRange = sPlayerbotAIConfig.lootDistance; + Group* group = bot->GetGroup(); + if (!group) + return false; - for (auto const& guid : corpses) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - LootObject loot(bot, guid); - if (!loot.IsLootPossible(bot)) - continue; - - WorldObject* object = loot.GetWorldObject(bot); - if (!object) - continue; - - Creature* creature = object->ToCreature(); - if (!creature || creature->GetEntry() != NPC_TAINTED_ELEMENTAL || creature->IsAlive()) - continue; - - context->GetValue("loot target")->Set(loot); - - float dist = bot->GetDistance(object); - if (dist > maxLootRange) - return MoveTo(object, 2.0f, MovementPriority::MOVEMENT_FORCED); - - OpenLootAction open(botAI); - if (!open.Execute(Event())) + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) return false; + } - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) - return true; - } - } + constexpr float searchRadius = 150.0f; + Creature* elemental = bot->FindNearestCreature(NPC_TAINTED_ELEMENTAL, searchRadius, false); - const ObjectGuid botGuid = bot->GetGUID(); - const ObjectGuid corpseGuid = guid; - constexpr uint8 coreIndex = 0; + if (!elemental || elemental->IsAlive()) + return false; - botAI->AddTimedEvent([botGuid, corpseGuid, coreIndex, vashj]() - { - Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); - if (!receiver) - return; + LootObject loot(bot, elemental->GetGUID()); + if (!loot.IsLootPossible(bot)) + return false; - if (Group* group = receiver->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) - return; - } - } + context->GetValue("loot target")->Set(loot); - receiver->SetLootGUID(corpseGuid); + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + constexpr float distFromObject = 2.0f; - WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); - *packet << coreIndex; - receiver->GetSession()->QueuePacket(packet); + if (bot->GetDistance(elemental) > maxLootRange) + return MoveTo(elemental, distFromObject, MovementPriority::MOVEMENT_FORCED); - const uint32 instanceId = vashj->GetMap()->GetInstanceId(); - const time_t now = std::time(nullptr); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - }, 600); + OpenLootAction open(botAI); + if (!open.Execute(Event())) + return false; - return true; - } + bot->SetLootGUID(elemental->GetGUID()); + constexpr uint8 coreIndex = 0; + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << coreIndex; + bot->GetSession()->QueuePacket(packet); - return false; + const time_t now = std::time(nullptr); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + + return true; } bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) @@ -2362,40 +2113,31 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) if (!vashj) return false; - Group* group = bot->GetGroup(); - if (!group) - return false; - - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - if (!designatedLooter) - return false; + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); + Player* secondCorePasser = GetSecondTaintedCorePasser(botAI, bot); + Player* thirdCorePasser = GetThirdTaintedCorePasser(botAI, bot); + Player* fourthCorePasser = GetFourthTaintedCorePasser(botAI, bot); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); - Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); const uint32 instanceId = vashj->GetMap()->GetInstanceId(); Unit* closestTrigger = nullptr; - if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) + if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + (closestTrigger = GetNearestActiveShieldGeneratorTriggerByEntry(tainted))) { - closestTrigger = GetNearestActiveShieldGeneratorTriggerByEntry(tainted); - if (closestTrigger) - nearestTriggerGuid.insert_or_assign(instanceId, closestTrigger->GetGUID()); + nearestTriggerGuid.try_emplace(instanceId, closestTrigger->GetGUID()); } auto itSnap = nearestTriggerGuid.find(instanceId); if (itSnap != nearestTriggerGuid.end() && !itSnap->second.IsEmpty()) { - Unit* snapUnit = botAI->GetUnit(itSnap->second); - if (snapUnit) + if (Unit* snapUnit = botAI->GetUnit(itSnap->second)) closestTrigger = snapUnit; else nearestTriggerGuid.erase(instanceId); } - if (!firstCorePasser || !secondCorePasser || !thirdCorePasser || - !fourthCorePasser || !closestTrigger) + if (!closestTrigger) return false; // Not gated behind CheatMask because the auto application of Fear Ward is necessary @@ -2408,63 +2150,57 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) if (!item || !botAI->HasItemInInventory(ITEM_TAINTED_CORE)) { // Passer order: HealAssistantOfIndex 0, 1, 2, then RangedDpsAssistantOfIndex 0 - if (bot == firstCorePasser) + if (bot == firstCorePasser && + LineUpFirstCorePasser(designatedLooter, closestTrigger)) { - if (LineUpFirstCorePasser(designatedLooter, closestTrigger)) - return true; + return true; } - else if (bot == secondCorePasser) + else if (bot == secondCorePasser && + LineUpSecondCorePasser(firstCorePasser, closestTrigger)) { - if (LineUpSecondCorePasser(firstCorePasser, closestTrigger)) - return true; + return true; } - else if (bot == thirdCorePasser) + else if (bot == thirdCorePasser && LineUpThirdCorePasser( + designatedLooter, firstCorePasser, secondCorePasser, closestTrigger)) { - if (LineUpThirdCorePasser(designatedLooter, firstCorePasser, - secondCorePasser, closestTrigger)) - return true; + return true; } - else if (bot == fourthCorePasser) + else if (bot == fourthCorePasser && LineUpFourthCorePasser( + firstCorePasser, secondCorePasser, thirdCorePasser, closestTrigger)) { - if (LineUpFourthCorePasser(firstCorePasser, secondCorePasser, - thirdCorePasser, closestTrigger)) - return true; + return true; } } else if (item && botAI->HasItemInInventory(ITEM_TAINTED_CORE)) { // Designated core looter logic // Applicable only if cheat mode is on and thus looter is a bot - if (bot == designatedLooter) + if (bot == designatedLooter && + IsFirstCorePasserInPosition(designatedLooter, firstCorePasser, closestTrigger)) { - if (IsFirstCorePasserInIntendedPosition( - designatedLooter, firstCorePasser, closestTrigger)) + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { - const time_t now = std::time(nullptr); - auto it = lastImbueAttempt.find(instanceId); - if (it == lastImbueAttempt.end() || (now - it->second) >= 2) - { - lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - botAI->ImbueItem(item, firstCorePasser); - intendedLineup.erase(bot->GetGUID()); - ScheduleTransferCoreAfterImbue(botAI, bot, firstCorePasser); - return true; - } + lastImbueAttempt.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, firstCorePasser); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + ScheduleTransferCoreAfterImbue(botAI, bot, firstCorePasser); + return true; } } // First core passer: receive core from looter at the top of the stairs, // pass to second core passer - else if (bot == firstCorePasser) + else if (bot == firstCorePasser && + IsSecondCorePasserInPosition(firstCorePasser, secondCorePasser, closestTrigger)) { const time_t now = std::time(nullptr); auto it = lastImbueAttempt.find(instanceId); if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); botAI->ImbueItem(item, secondCorePasser); - intendedLineup.erase(bot->GetGUID()); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); ScheduleTransferCoreAfterImbue(botAI, bot, secondCorePasser); return true; } @@ -2472,55 +2208,41 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) // Second core passer: if closest usable generator is within passing distance // of the first passer, move to the generator; otherwise, move as close as // possible to the generator while staying in passing range - else if (bot == secondCorePasser) + else if (bot == secondCorePasser && !UseCoreOnNearestGenerator(instanceId) && + IsThirdCorePasserInPosition(secondCorePasser, thirdCorePasser, closestTrigger)) { - if (!UseCoreOnNearestGenerator(instanceId)) + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { - if (IsThirdCorePasserInIntendedPosition( - secondCorePasser, thirdCorePasser, closestTrigger)) - { - const time_t now = std::time(nullptr); - auto it = lastImbueAttempt.find(instanceId); - if (it == lastImbueAttempt.end() || (now - it->second) >= 2) - { - lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - botAI->ImbueItem(item, thirdCorePasser); - intendedLineup.erase(bot->GetGUID()); - ScheduleTransferCoreAfterImbue(botAI, bot, thirdCorePasser); - return true; - } - } + lastImbueAttempt.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, thirdCorePasser); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + ScheduleTransferCoreAfterImbue(botAI, bot, thirdCorePasser); + return true; } } // Third core passer: if closest usable generator is within passing distance // of the second passer, move to the generator; otherwise, move as close as // possible to the generator while staying in passing range - else if (bot == thirdCorePasser) + else if (bot == thirdCorePasser && !UseCoreOnNearestGenerator(instanceId) && + IsFourthCorePasserInPosition(thirdCorePasser, fourthCorePasser, closestTrigger)) { - if (!UseCoreOnNearestGenerator(instanceId)) + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { - if (IsFourthCorePasserInIntendedPosition( - thirdCorePasser, fourthCorePasser, closestTrigger)) - { - const time_t now = std::time(nullptr); - auto it = lastImbueAttempt.find(instanceId); - if (it == lastImbueAttempt.end() || (now - it->second) >= 2) - { - lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - botAI->ImbueItem(item, fourthCorePasser); - intendedLineup.erase(bot->GetGUID()); - ScheduleTransferCoreAfterImbue(botAI, bot, fourthCorePasser); - return true; - } - } + lastImbueAttempt.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, fourthCorePasser); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + ScheduleTransferCoreAfterImbue(botAI, bot, fourthCorePasser); + return true; } } // Fourth core passer: the fourth passer is rarely needed and no more than // four ever should be, so it should use the Core on the nearest generator - else if (bot == fourthCorePasser) - UseCoreOnNearestGenerator(instanceId); + else if (bot == fourthCorePasser && UseCoreOnNearestGenerator(instanceId)) + return true; } return false; @@ -2533,15 +2255,25 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( const float centerY = VASHJ_PLATFORM_CENTER_POSITION.GetPositionY(); constexpr float radius = 57.5f; - float mx = designatedLooter->GetPositionX(); - float my = designatedLooter->GetPositionY(); - float angle = atan2(my - centerY, mx - centerX); + auto it = intendedLineup.find(bot->GetGUID()); + if (it == intendedLineup.end()) + { + float mx = designatedLooter->GetPositionX(); + float my = designatedLooter->GetPositionY(); + float angle = atan2(my - centerY, mx - centerX); + + float targetX = centerX + radius * std::cos(angle); + float targetY = centerY + radius * std::sin(angle); + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; - float targetX = centerX + radius * std::cos(angle); - float targetY = centerY + radius * std::sin(angle); - constexpr float targetZ = 41.097f; + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + it = intendedLineup.find(bot->GetGUID()); + } - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, targetZ)); + const Position& pos = it->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); @@ -2552,143 +2284,183 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( bool LadyVashjPassTheTaintedCoreAction::LineUpSecondCorePasser( Player* firstCorePasser, Unit* closestTrigger) { - float fx = firstCorePasser->GetPositionX(); - float fy = firstCorePasser->GetPositionY(); + auto itFirst = intendedLineup.find(firstCorePasser->GetGUID()); + if (itFirst == intendedLineup.end()) + return false; - float dx = closestTrigger->GetPositionX() - fx; - float dy = closestTrigger->GetPositionY() - fy; - float distToTrigger = firstCorePasser->GetExactDist2d(closestTrigger); + const Position& firstLineup = itFirst->second; - if (distToTrigger == 0.0f) - return false; + auto itSecond = intendedLineup.find(bot->GetGUID()); + if (itSecond == intendedLineup.end()) + { + float fx = itFirst->second.GetPositionX(); + float fy = itFirst->second.GetPositionY(); - dx /= distToTrigger; dy /= distToTrigger; + float dx = closestTrigger->GetPositionX() - fx; + float dy = closestTrigger->GetPositionY() - fy; + float distToTrigger = std::sqrt(dx * dx + dy * dy); - // Target is on a line between firstCorePasser and closestTrigger - float targetX, targetY, targetZ; - // If firstCorePasser is within thresholdDist of closestTrigger, - // go to nearTriggerDist short of closestTrigger - constexpr float thresholdDist = 40.0f; - constexpr float nearTriggerDist = 1.5f; - // If firstCorePasser is not thresholdDist yards from closestTrigger, - // go to farDistance from firstCorePasser - constexpr float farDistance = 38.0f; + if (distToTrigger == 0.0f) + return false; - if (distToTrigger <= thresholdDist) - { - float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); - targetX = fx + dx * moveDist; - targetY = fy + dy * moveDist; - } - else - { - targetX = fx + dx * farDistance; - targetY = fy + dy * farDistance; + dx /= distToTrigger; dy /= distToTrigger; + + float targetX, targetY; + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = fx + dx * moveDist; + targetY = fy + dy * moveDist; + } + else + { + targetX = fx + dx * farDistance; + targetY = fy + dy * farDistance; + } + + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + itSecond = intendedLineup.find(bot->GetGUID()); } - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + const Position& pos = itSecond->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); } bool LadyVashjPassTheTaintedCoreAction::LineUpThirdCorePasser( - Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) + Player* designatedLooter, Player* firstCorePasser, + Player* secondCorePasser, Unit* closestTrigger) { - // Wait to move until it is clear that a third passer is needed - bool needThird = - (IsFirstCorePasserInIntendedPosition(designatedLooter, firstCorePasser, closestTrigger) && + bool needThirdPasser = + (IsFirstCorePasserInPosition(designatedLooter, firstCorePasser, closestTrigger) && firstCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || - (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + (IsSecondCorePasserInPosition(firstCorePasser, secondCorePasser, closestTrigger) && secondCorePasser->GetExactDist2d(closestTrigger) > 4.0f); - if (!needThird) + if (!needThirdPasser) + return false; + + auto itSecond = intendedLineup.find(secondCorePasser->GetGUID()); + if (itSecond == intendedLineup.end()) return false; - float sx = secondCorePasser->GetPositionX(); - float sy = secondCorePasser->GetPositionY(); + auto itThird = intendedLineup.find(bot->GetGUID()); + if (itThird == intendedLineup.end()) + { + float sx = itSecond->second.GetPositionX(); + float sy = itSecond->second.GetPositionY(); - float dx = closestTrigger->GetPositionX() - sx; - float dy = closestTrigger->GetPositionY() - sy; - float distToTrigger = secondCorePasser->GetExactDist2d(closestTrigger); + float dx = closestTrigger->GetPositionX() - sx; + float dy = closestTrigger->GetPositionY() - sy; + float distToTrigger = std::sqrt(dx * dx + dy * dy); - if (distToTrigger == 0.0f) - return false; + if (distToTrigger == 0.0f) + return false; - dx /= distToTrigger; dy /= distToTrigger; + dx /= distToTrigger; dy /= distToTrigger; - float targetX, targetY, targetZ; - constexpr float thresholdDist = 40.0f; - constexpr float nearTriggerDist = 1.5f; - constexpr float farDistance = 38.0f; + float targetX, targetY; + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + constexpr float farDistance = 38.0f; - if (distToTrigger <= thresholdDist) - { - float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); - targetX = sx + dx * moveDist; - targetY = sy + dy * moveDist; - } - else - { - targetX = sx + dx * farDistance; - targetY = sy + dy * farDistance; + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = sx + dx * moveDist; + targetY = sy + dy * moveDist; + } + else + { + targetX = sx + dx * farDistance; + targetY = sy + dy * farDistance; + } + + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + itThird = intendedLineup.find(bot->GetGUID()); } - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + const Position& pos = itThird->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - - return false; } bool LadyVashjPassTheTaintedCoreAction::LineUpFourthCorePasser( - Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) + Player* firstCorePasser, Player* secondCorePasser, + Player* thirdCorePasser, Unit* closestTrigger) { - // Wait to move until it is clear that a fourth passer is needed - bool needFourth = - (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + bool needFourthPasser = + (IsSecondCorePasserInPosition(firstCorePasser, secondCorePasser, closestTrigger) && secondCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || - (IsThirdCorePasserInIntendedPosition(secondCorePasser, thirdCorePasser, closestTrigger) && + (IsThirdCorePasserInPosition(secondCorePasser, thirdCorePasser, closestTrigger) && thirdCorePasser->GetExactDist2d(closestTrigger) > 4.0f); - if (!needFourth) + if (!needFourthPasser) return false; - float sx = thirdCorePasser->GetPositionX(); - float sy = thirdCorePasser->GetPositionY(); + auto itThird = intendedLineup.find(thirdCorePasser->GetGUID()); + if (itThird == intendedLineup.end()) + return false; - float tx = closestTrigger->GetPositionX(); - float ty = closestTrigger->GetPositionY(); + auto itFourth = intendedLineup.find(bot->GetGUID()); + if (itFourth == intendedLineup.end()) + { + float sx = itThird->second.GetPositionX(); + float sy = itThird->second.GetPositionY(); - float dx = tx - sx; - float dy = ty - sy; - float distToTrigger = thirdCorePasser->GetExactDist2d(closestTrigger); + float tx = closestTrigger->GetPositionX(); + float ty = closestTrigger->GetPositionY(); - if (distToTrigger == 0.0f) - return false; + float dx = tx - sx; + float dy = ty - sy; + float distToTrigger = std::sqrt(dx * dx + dy * dy); - dx /= distToTrigger; dy /= distToTrigger; + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; - constexpr float nearTriggerDist = 1.5f; - float targetX = tx - dx * nearTriggerDist; - float targetY = ty - dy * nearTriggerDist; + constexpr float nearTriggerDist = 1.5f; + float targetX = tx - dx * nearTriggerDist; + float targetY = ty - dy * nearTriggerDist; + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; + + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + itFourth = intendedLineup.find(bot->GetGUID()); + } - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + const Position& pos = itFourth->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); } // The next four functions check if the respective passer is <= 2 yards of their intended // position and are used to determine when the prior bot in the chain can pass the core -bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInIntendedPosition( +bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInPosition( Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger) { auto itSnap = intendedLineup.find(firstCorePasser->GetGUID()); @@ -2702,7 +2474,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInIntendedPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInIntendedPosition( +bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInPosition( Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) { auto itSnap = intendedLineup.find(secondCorePasser->GetGUID()); @@ -2716,7 +2488,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInIntendedPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInIntendedPosition( +bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInPosition( Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) { auto itSnap = intendedLineup.find(thirdCorePasser->GetGUID()); @@ -2730,7 +2502,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInIntendedPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInIntendedPosition( +bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInPosition( Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger) { auto itSnap = intendedLineup.find(fourthCorePasser->GetGUID()); @@ -2799,17 +2571,11 @@ bool LadyVashjPassTheTaintedCoreAction::UseCoreOnNearestGenerator(const uint32 i return false; GameObject* generator = botAI->GetGameObject(nearestGen->guid); - if (!generator) - return false; - - if (bot->GetExactDist2d(generator) > 4.5f) + if (!generator || bot->GetExactDist2d(generator) > 4.5f) return false; Item* core = bot->GetItemByEntry(ITEM_TAINTED_CORE); - if (!core) - return false; - - if (bot->CanUseItem(core) != EQUIP_ERR_OK) + if (!core || bot->CanUseItem(core) != EQUIP_ERR_OK) return false; if (bot->IsNonMeleeSpellCast(false)) @@ -2845,9 +2611,18 @@ bool LadyVashjPassTheTaintedCoreAction::UseCoreOnNearestGenerator(const uint32 i packet << generator->GetGUID().WriteAsPacked(); bot->GetSession()->HandleUseItemOpcode(packet); - nearestTriggerGuid.erase(instanceId); + lastImbueAttempt.erase(instanceId); - lastCoreInInventoryTime.erase(instanceId); + auto coreHandlers = GetCoreHandlers(botAI, bot); + for (Player* handler : coreHandlers) + { + if (handler) + { + intendedLineup.erase(handler->GetGUID()); + lastCoreInInventoryTime.erase(handler->GetGUID()); + } + } + return true; } @@ -2864,35 +2639,12 @@ bool LadyVashjDestroyTaintedCoreAction::Execute(Event /*event*/) return false; } -// This needs to be separate from the general map erasing logic because -// Bots may end up out of combat during the Vashj encounter -bool LadyVashjEraseCorePassingTrackersAction::Execute(Event /*event*/) -{ - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - if (!vashj) - return false; - - const uint32 instanceId = vashj->GetMap()->GetInstanceId(); - - bool erased = false; - if (nearestTriggerGuid.erase(instanceId) > 0) - erased = true; - if (lastImbueAttempt.erase(instanceId) > 0) - erased = true; - if (lastCoreInInventoryTime.erase(instanceId) > 0) - erased = true; - if (intendedLineup.erase(bot->GetGUID()) > 0) - erased = true; - - return erased; -} - // The standard "avoid aoe" strategy does work for Toxic Spores, but this method // provides more buffer distance and limits the area in which bots can move // so that they do not go down the stairs bool LadyVashjAvoidToxicSporesAction::Execute(Event /*event*/) { - auto const& spores = GetAllSporeDropTriggers(botAI, bot); + auto const& spores = GetAllSporeDropTriggers(bot); if (spores.empty()) return false; @@ -2910,16 +2662,21 @@ bool LadyVashjAvoidToxicSporesAction::Execute(Event /*event*/) if (!inDanger) return false; + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + const Position& vashjCenter = VASHJ_PLATFORM_CENTER_POSITION; constexpr float maxRadius = 60.0f; Position safestPos = FindSafestNearbyPosition(spores, vashjCenter, maxRadius, hazardRadius); + bool backwards = vashj->GetVictim() == bot; + MovementPriority priority = backwards ? + MovementPriority::MOVEMENT_FORCED : MovementPriority::MOVEMENT_COMBAT; - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - bool backwards = (vashj && vashj->GetVictim() == bot); return MoveTo(SSC_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), safestPos.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, backwards); + priority, true, backwards); } Position LadyVashjAvoidToxicSporesAction::FindSafestNearbyPosition( @@ -3015,19 +2772,18 @@ bool LadyVashjAvoidToxicSporesAction::IsPathSafeFromSpores(const Position& start // When Toxic Sporebats spit poison, they summon "Spore Drop Trigger" NPCs // that create the toxic pools -std::vector LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers( - PlayerbotAI* botAI, Player* bot) +std::vector LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(Player* bot) { std::vector sporeDropTriggers; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + std::list creatureList; + constexpr float searchRadius = 50.0f; + + bot->GetCreatureListWithEntryInGrid(creatureList, NPC_SPORE_DROP_TRIGGER, searchRadius); + + for (Creature* creature : creatureList) { - constexpr float maxSearchRadius = 40.0f; - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == NPC_SPORE_DROP_TRIGGER && - bot->GetExactDist2d(unit) < maxSearchRadius) - sporeDropTriggers.push_back(unit); + if (creature && creature->IsAlive()) + sporeDropTriggers.push_back(creature); } return sporeDropTriggers; @@ -3040,7 +2796,7 @@ bool LadyVashjUseFreeActionAbilitiesAction::Execute(Event /*event*/) return false; auto const& spores = - LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(botAI, bot); + LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(bot); constexpr float toxicSporeRadius = 6.0f; // If Rogues are Entangled and either have Static Charge or @@ -3109,19 +2865,13 @@ bool LadyVashjUseFreeActionAbilitiesAction::Execute(Event /*event*/) { // Priority 1: Entangled in Toxic Spores (prefer main tank) Player* toxicTarget = mainTankToxic ? mainTankToxic : anyToxic; - if (toxicTarget) - { - if (botAI->CanCastSpell("hand of freedom", toxicTarget)) - return botAI->CastSpell("hand of freedom", toxicTarget); - } + if (toxicTarget && botAI->CanCastSpell("hand of freedom", toxicTarget)) + return botAI->CastSpell("hand of freedom", toxicTarget); // Priority 2: Entangled with Static Charge (prefer main tank) Player* staticTarget = mainTankStatic ? mainTankStatic : anyStatic; - if (staticTarget) - { - if (botAI->CanCastSpell("hand of freedom", staticTarget)) - return botAI->CastSpell("hand of freedom", staticTarget); - } + if (staticTarget && botAI->CanCastSpell("hand of freedom", staticTarget)) + return botAI->CastSpell("hand of freedom", staticTarget); } return false; diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h index cbd23740210..08ad4c48a82 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCACTIONS_H #define _PLAYERBOT_RAIDSSCACTIONS_H @@ -75,8 +80,8 @@ class HydrossTheUnstableMisdirectBossToTankAction : public Action bool Execute(Event event) override; private: - bool TryMisdirectToFrostTank(Unit* hydross, Group* group); - bool TryMisdirectToNatureTank(Unit* hydross, Group* group); + bool TryMisdirectToFrostTank(Unit* hydross); + bool TryMisdirectToNatureTank(Unit* hydross); }; class HydrossTheUnstableStopDpsUponPhaseChangeAction : public Action @@ -413,10 +418,10 @@ class LadyVashjPassTheTaintedCoreAction : public MovementAction bool LineUpSecondCorePasser(Player* firstCorePasser, Unit* closestTrigger); bool LineUpThirdCorePasser(Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); bool LineUpFourthCorePasser(Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); - bool IsFirstCorePasserInIntendedPosition(Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger); - bool IsSecondCorePasserInIntendedPosition(Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); - bool IsThirdCorePasserInIntendedPosition(Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); - bool IsFourthCorePasserInIntendedPosition(Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger); + bool IsFirstCorePasserInPosition(Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger); + bool IsSecondCorePasserInPosition(Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); + bool IsThirdCorePasserInPosition(Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); + bool IsFourthCorePasserInPosition(Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger); void ScheduleTransferCoreAfterImbue(PlayerbotAI* botAI, Player* giver, Player* receiver); bool UseCoreOnNearestGenerator(const uint32 instanceId); }; @@ -428,19 +433,12 @@ class LadyVashjDestroyTaintedCoreAction : public Action bool Execute(Event event) override; }; -class LadyVashjEraseCorePassingTrackersAction : public Action -{ -public: - LadyVashjEraseCorePassingTrackersAction(PlayerbotAI* botAI, std::string const name = "lady vashj erase core passing trackers") : Action(botAI, name) {} - bool Execute(Event event) override; -}; - class LadyVashjAvoidToxicSporesAction : public MovementAction { public: LadyVashjAvoidToxicSporesAction(PlayerbotAI* botAI, std::string const name = "lady vashj avoid toxic spores") : MovementAction(botAI, name) {} bool Execute(Event event) override; - static std::vector GetAllSporeDropTriggers(PlayerbotAI* botAI, Player* bot); + static std::vector GetAllSporeDropTriggers(Player* bot); private: Position FindSafestNearbyPosition(const std::vector& spores, const Position& position, float maxRadius, float hazardRadius); diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp index c99cafa3c7f..c841c7dcb5e 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp @@ -1,3 +1,8 @@ +/* + * 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 "RaidSSCMultipliers.h" #include "RaidSSCActions.h" #include "RaidSSCHelpers.h" @@ -28,12 +33,10 @@ using namespace SerpentShrineCavernHelpers; float UnderbogColossusEscapeToxicPoolMultiplier::GetValue(Action* action) { - if (bot->HasAura(SPELL_TOXIC_POOL)) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + if (bot->HasAura(SPELL_TOXIC_POOL) && + dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -53,16 +56,16 @@ float HydrossTheUnstableDisableTankActionsMultiplier::GetValue(Action* action) dynamic_cast(action)) return 0.0f; + if ((botAI->IsMainTank(bot) && !hydross->HasAura(SPELL_CORRUPTION)) || + (botAI->IsAssistTankOfIndex(bot, 0, true) && hydross->HasAura(SPELL_CORRUPTION))) + return 1.0f; + if (dynamic_cast(action) || dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action) && !dynamic_cast(action))) - { - if ((botAI->IsMainTank(bot) && hydross->HasAura(SPELL_CORRUPTION)) || - (botAI->IsAssistTankOfIndex(bot, 0, true) && !hydross->HasAura(SPELL_CORRUPTION))) - return 0.0f; - } + return 0.0f; return 1.0f; } @@ -97,13 +100,13 @@ float HydrossTheUnstableWaitForDpsMultiplier::GetValue(Action* action) bool aboutToChange = (itPhase != hydrossChangeToFrostPhaseTimer.end() && (now - itPhase->second) > phaseChangeWaitSeconds); - if (justChanged || aboutToChange) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } + if (!justChanged && !aboutToChange) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } if (hydross->HasAura(SPELL_CORRUPTION) && !botAI->IsAssistTankOfIndex(bot, 0, true)) @@ -116,13 +119,13 @@ float HydrossTheUnstableWaitForDpsMultiplier::GetValue(Action* action) bool aboutToChange = (itPhase != hydrossChangeToNaturePhaseTimer.end() && (now - itPhase->second) > phaseChangeWaitSeconds); - if (justChanged || aboutToChange) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } + if (!justChanged && !aboutToChange) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } return 1.0f; @@ -133,11 +136,9 @@ float HydrossTheUnstableControlMisdirectionMultiplier::GetValue(Action* action) if (bot->getClass() != CLASS_HUNTER) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "hydross the unstable")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (AI_VALUE2(Unit*, "find target", "hydross the unstable") && + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -175,14 +176,14 @@ float TheLurkerBelowMaintainRangedSpreadMultiplier::GetValue(Action* action) if (!botAI->IsRanged(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "the lurker below")) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "the lurker below")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -215,11 +216,11 @@ float TheLurkerBelowDisableTankAssistMultiplier::GetValue(Action* action) ++tankCount; } - if (tankCount >= 3) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (tankCount < 3) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -234,22 +235,18 @@ float LeotherasTheBlindAvoidWhirlwindMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return 1.0f; - Unit* leotherasHuman = GetLeotherasHuman(botAI); - if (!leotherasHuman) + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras || (!leotheras->HasAura(SPELL_WHIRLWIND) && + !leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL))) return 1.0f; - if (!leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && - (leotherasHuman->HasAura(SPELL_WHIRLWIND) || - leotherasHuman->HasAura(SPELL_WHIRLWIND_CHANNEL))) - { - if (dynamic_cast(action)) - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; - if (dynamic_cast(action) && - !dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -262,10 +259,10 @@ float LeotherasTheBlindDisableTankActionsMultiplier::GetValue(Action* action) if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return 1.0f; - if (GetPhase2LeotherasDemon(botAI) && dynamic_cast(action)) + if (GetPhase2LeotherasDemon(bot) && dynamic_cast(action)) return 0.0f; - if (!GetPhase3LeotherasDemon(botAI) && dynamic_cast(action)) + if (!GetPhase3LeotherasDemon(bot) && dynamic_cast(action)) return 0.0f; return 1.0f; @@ -273,21 +270,21 @@ float LeotherasTheBlindDisableTankActionsMultiplier::GetValue(Action* action) float LeotherasTheBlindFocusOnInnerDemonMultiplier::GetValue(Action* action) { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) - { - 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) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + 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) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -297,19 +294,19 @@ float LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier::GetValue(Action* actio if (botAI->IsRanged(bot) || botAI->IsTank(bot)) return 1.0f; - if (!GetPhase2LeotherasDemon(botAI)) + if (!GetPhase2LeotherasDemon(bot)) return 1.0f; Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); - if (chaosBlast && chaosBlast->GetStackAmount() >= 5) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!chaosBlast || chaosBlast->GetStackAmount() < 5) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -330,8 +327,8 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) const time_t now = std::time(nullptr); constexpr uint8 dpsWaitSecondsPhase1 = 5; - Unit* leotherasHuman = GetLeotherasHuman(botAI); - Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); + Unit* leotherasHuman = GetLeotherasHuman(bot); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(bot); if (leotherasHuman && !leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && !leotherasPhase3Demon) { @@ -345,12 +342,12 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } } constexpr uint8 dpsWaitSecondsPhase2 = 12; - Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI); + Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(bot); Player* demonFormTank = GetLeotherasDemonFormTank(bot); if (leotherasPhase2Demon) { @@ -367,7 +364,7 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } } @@ -384,7 +381,7 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } } @@ -398,12 +395,12 @@ float LeotherasTheBlindDelayBloodlustAndHeroismMultiplier::GetValue(Action* acti return 1.0f; Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); - if (leotheras && leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) - { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!leotheras || !leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -447,14 +444,12 @@ float FathomLordKarathressDisableAoeMultiplier::GetValue(Action* action) if (!botAI->IsDps(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) - { - if (auto castSpellAction = dynamic_cast(action)) - { - if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) - return 0.0f; - } - } + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (castSpellAction && castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return 0.0f; return 1.0f; } @@ -464,11 +459,11 @@ float FathomLordKarathressControlMisdirectionMultiplier::GetValue(Action* action if (bot->getClass() != CLASS_HUNTER) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -494,7 +489,7 @@ float FathomLordKarathressWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } return 1.0f; @@ -505,12 +500,12 @@ float FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier::GetValue if (!botAI->IsAssistHealOfIndex(bot, 0, true)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "fathom-guard caribdis")) - { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "fathom-guard caribdis")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -526,12 +521,12 @@ float MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier::GetValue(Action* act if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) return 1.0f; - if (!AI_VALUE2(Unit*, "find target", "tidewalker lurker")) - { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (AI_VALUE2(Unit*, "find target", "tidewalker lurker")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -541,11 +536,11 @@ float MorogrimTidewalkerDisableTankActionsMultiplier::GetValue(Action* action) if (!botAI->IsMainTank(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -556,17 +551,14 @@ float MorogrimTidewalkerMaintainPhase2StackingMultiplier::GetValue(Action* actio return 1.0f; Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); - if (!tidewalker) + if (!tidewalker || tidewalker->GetHealthPct() > 25.0f) return 1.0f; - if (tidewalker->GetHealthPct() < 25.0f) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -580,41 +572,59 @@ float LadyVashjDelayCooldownsMultiplier::GetValue(Action* action) if (!AI_VALUE2(Unit*, "find target", "lady vashj")) return 1.0f; - if (bot->getClass() == CLASS_SHAMAN) - { - if (IsLadyVashjInPhase3(botAI)) - return 1.0f; + if (bot->getClass() == CLASS_SHAMAN && + !IsLadyVashjInPhase3(botAI) && + (dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!botAI->IsDps(bot) || !IsLadyVashjInPhase1(botAI)) + return 1.0f; + + 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) || + 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) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; - if (botAI->IsDps(bot) && IsLadyVashjInPhase1(botAI)) - { - 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) || - 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) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + return 1.0f; +} + +float LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (!IsMainTankInSameSubgroup(botAI, bot)) + 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; return 1.0f; } @@ -624,15 +634,15 @@ float LadyVashjMaintainPhase1RangedSpreadMultiplier::GetValue(Action* action) if (!botAI->IsRanged(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "lady vashj") && - IsLadyVashjInPhase1(botAI)) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || + !IsLadyVashjInPhase1(botAI)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -658,19 +668,18 @@ float LadyVashjStaticChargeStayAwayFromGroupMultiplier::GetValue(Action* action) // Bots should not loot the core with normal looting logic float LadyVashjDoNotLootTheTaintedCoreMultiplier::GetValue(Action* action) { - if (AI_VALUE2(Unit*, "find target", "lady vashj")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* action) { - if (!AI_VALUE2(Unit*, "find target", "lady vashj") || - !IsLadyVashjInPhase2(botAI)) + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI)) return 1.0f; if (dynamic_cast(action) || @@ -678,65 +687,43 @@ float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* acti dynamic_cast(action)) return 1.0f; - Group* group = bot->GetGroup(); - if (!group) - return 1.0f; + auto coreHandlers = GetCoreHandlers(botAI, bot); - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); - Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + bool isCoreHandler = false; + int myIndex = -1; + for (int i = 0; i < static_cast(coreHandlers.size()); ++i) + { + if (coreHandlers[i] && coreHandlers[i] == bot) + { + isCoreHandler = true; + myIndex = i; + } + } + if (!isCoreHandler) + return 1.0f; auto hasCore = [](Player* player) { return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); }; - if (hasCore(bot)) - { - if (!dynamic_cast(action)) - return 0.0f; - } - - if (bot == designatedLooter) - { - if (!hasCore(bot)) - return 1.0f; - } - else if (bot == firstCorePasser) - { - if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || - hasCore(fourthCorePasser)) - return 1.0f; - } - else if (bot == secondCorePasser) - { - if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) - return 1.0f; - } - else if (bot == thirdCorePasser) - { - if (hasCore(fourthCorePasser)) - return 1.0f; - } - else if (bot != fourthCorePasser) - return 1.0f; + // If the bot actually has the core, only allow core handling + if (hasCore(bot) && !dynamic_cast(action)) + return 0.0f; - if (AI_VALUE2(Unit*, "find target", "tainted elemental") && - (bot == firstCorePasser || bot == secondCorePasser)) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + // First and second passers block movement when the looter teleports to the elemental + Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (tainted && coreHandlers[0]->GetExactDist2d(tainted) < 5.0f && + (bot == coreHandlers[1] || bot == coreHandlers[2]) && + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; - if (AnyRecentCoreInInventory(group, botAI)) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + // If any prior handler (including self) recently had the core, block other movement + if (AnyRecentCoreInInventory(botAI, bot) && + dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -745,7 +732,8 @@ float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* acti // So the standard target selection system must be disabled float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *action) { - if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) return 1.0f; if (dynamic_cast(action)) @@ -755,24 +743,26 @@ float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *ac { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) return 0.0f; + if (bot->GetExactDist2d(vashj) < 60.0f && + dynamic_cast(action)) + return 0.0f; + if (!botAI->IsHeal(bot) && dynamic_cast(action)) return 0.0f; Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); - if (enchanted && bot->GetVictim() == enchanted) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (enchanted && bot->GetVictim() == enchanted && + dynamic_cast(action)) + return 0.0f; } if (IsLadyVashjInPhase3(botAI)) { - if (dynamic_cast(action)) + if (dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); @@ -780,16 +770,13 @@ float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *ac Unit* elite = AI_VALUE2(Unit*, "find target", "coilfang elite"); if (enchanted || strider || elite) { - if (dynamic_cast(action) || - dynamic_cast(action) || + if (dynamic_cast(action) || dynamic_cast(action)) return 0.0f; - if (enchanted && bot->GetVictim() == enchanted) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (enchanted && bot->GetVictim() == enchanted && + dynamic_cast(action)) + return 0.0f; } else if (dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h index 6630dc2060c..19ba735282e 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCMULTIPLIERS_H #define _PLAYERBOT_RAIDSSCMULTIPLIERS_H @@ -193,6 +198,14 @@ class LadyVashjDelayCooldownsMultiplier : public Multiplier virtual float GetValue(Action* action); }; +class LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier : public Multiplier +{ +public: + LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj main tank group shaman use grounding totem") {} + virtual float GetValue(Action* action); +}; + class LadyVashjMaintainPhase1RangedSpreadMultiplier : public Multiplier { public: diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h index e6dce16949d..1e2c44b49ec 100644 --- a/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h +++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCACTIONCONTEXT_H #define _PLAYERBOT_RAIDSSCACTIONCONTEXT_H @@ -161,9 +166,6 @@ class RaidSSCActionContext : public NamedObjectContext creators["lady vashj destroy tainted core"] = &RaidSSCActionContext::lady_vashj_destroy_tainted_core; - creators["lady vashj erase core passing trackers"] = - &RaidSSCActionContext::lady_vashj_erase_core_passing_trackers; - creators["lady vashj avoid toxic spores"] = &RaidSSCActionContext::lady_vashj_avoid_toxic_spores; @@ -324,9 +326,6 @@ class RaidSSCActionContext : public NamedObjectContext static Action* lady_vashj_destroy_tainted_core( PlayerbotAI* botAI) { return new LadyVashjDestroyTaintedCoreAction(botAI); } - static Action* lady_vashj_erase_core_passing_trackers( - PlayerbotAI* botAI) { return new LadyVashjEraseCorePassingTrackersAction(botAI); } - static Action* lady_vashj_avoid_toxic_spores( PlayerbotAI* botAI) { return new LadyVashjAvoidToxicSporesAction(botAI); } diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h index 13135bf3e51..737fd3a387a 100644 --- a/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h +++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCTRIGGERCONTEXT_H #define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H @@ -155,9 +160,6 @@ class RaidSSCTriggerContext : public NamedObjectContext creators["lady vashj tainted core is unusable"] = &RaidSSCTriggerContext::lady_vashj_tainted_core_is_unusable; - creators["lady vashj need to reset core passing trackers"] = - &RaidSSCTriggerContext::lady_vashj_need_to_reset_core_passing_trackers; - creators["lady vashj toxic sporebats are spewing poison clouds"] = &RaidSSCTriggerContext::lady_vashj_toxic_sporebats_are_spewing_poison_clouds; @@ -312,9 +314,6 @@ class RaidSSCTriggerContext : public NamedObjectContext static Trigger* lady_vashj_tainted_core_is_unusable( PlayerbotAI* botAI) { return new LadyVashjTaintedCoreIsUnusableTrigger(botAI); } - static Trigger* lady_vashj_need_to_reset_core_passing_trackers( - PlayerbotAI* botAI) { return new LadyVashjNeedToResetCorePassingTrackersTrigger(botAI); } - static Trigger* lady_vashj_toxic_sporebats_are_spewing_poison_clouds( PlayerbotAI* botAI) { return new LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger(botAI); } diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp index 139667dc668..624049c8196 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp @@ -1,3 +1,8 @@ +/* + * 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 "RaidSSCStrategy.h" #include "RaidSSCMultipliers.h" @@ -144,9 +149,6 @@ void RaidSSCStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("lady vashj tainted core is unusable", { NextAction("lady vashj destroy tainted core", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode("lady vashj need to reset core passing trackers", { - NextAction("lady vashj erase core passing trackers", ACTION_EMERGENCY + 10) })); - triggers.push_back(new TriggerNode("lady vashj adds spawn in phase 2 and phase 3", { NextAction("lady vashj assign phase 2 and phase 3 dps priority", ACTION_RAID + 1) })); @@ -198,6 +200,7 @@ void RaidSSCStrategy::InitMultipliers(std::vector& multipliers) // Lady Vashj multipliers.push_back(new LadyVashjDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier(botAI)); multipliers.push_back(new LadyVashjMaintainPhase1RangedSpreadMultiplier(botAI)); multipliers.push_back(new LadyVashjStaticChargeStayAwayFromGroupMultiplier(botAI)); multipliers.push_back(new LadyVashjDoNotLootTheTaintedCoreMultiplier(botAI)); diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h index 3c2c05f586e..a994600ba76 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h +++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCSTRATEGY_H_ #define _PLAYERBOT_RAIDSSCSTRATEGY_H_ diff --git a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp index e77e63642ed..c3e7532236a 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp @@ -1,3 +1,8 @@ +/* + * 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 "RaidSSCTriggers.h" #include "RaidSSCHelpers.h" #include "RaidSSCActions.h" @@ -26,35 +31,37 @@ bool UnderbogColossusSpawnedToxicPoolAfterDeathTrigger::IsActive() bool GreyheartTidecallerWaterElementalTotemSpawnedTrigger::IsActive() { return botAI->IsDps(bot) && - GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM); + AI_VALUE2(Unit*, "find target", "greyheart tidecaller"); } // Hydross the Unstable bool HydrossTheUnstableBotIsFrostTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "hydross the unstable") && - botAI->IsMainTank(bot); + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); } bool HydrossTheUnstableBotIsNatureTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "hydross the unstable") && - botAI->IsAssistTankOfIndex(bot, 0, true); + return botAI->IsAssistTankOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); } bool HydrossTheUnstableElementalsSpawnedTrigger::IsActive() { + if (botAI->IsHeal(bot)) + return false; + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); - if (hydross && hydross->GetHealthPct() < 10.0f) + if (!hydross || hydross->GetHealthPct() < 10.0f) return false; - if (!AI_VALUE2(Unit*, "find target", "pure spawn of hydross") && - !AI_VALUE2(Unit*, "find target", "tainted spawn of hydross")) + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true)) return false; - return !botAI->IsHeal(bot) && !botAI->IsMainTank(bot) && - !botAI->IsAssistTankOfIndex(bot, 0, true); + return AI_VALUE2(Unit*, "find target", "pure spawn of hydross") || + AI_VALUE2(Unit*, "find target", "tainted spawn of hydross"); } bool HydrossTheUnstableDangerFromWaterTombsTrigger::IsActive() @@ -71,19 +78,19 @@ bool HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger::IsActive() bool HydrossTheUnstableAggroResetsUponPhaseChangeTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + if (bot->getClass() == CLASS_HUNTER || + botAI->IsHeal(bot) || + botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, true)) return false; - return bot->getClass() != CLASS_HUNTER && - !botAI->IsHeal(bot) && - !botAI->IsMainTank(bot) && - !botAI->IsAssistTankOfIndex(bot, 0, true); + return AI_VALUE2(Unit*, "find target", "hydross the unstable"); } bool HydrossTheUnstableNeedToManageTimersTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "hydross the unstable") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); } // The Lurker Below @@ -102,11 +109,11 @@ bool TheLurkerBelowSpoutIsActiveTrigger::IsActive() bool TheLurkerBelowBossIsActiveForMainTankTrigger::IsActive() { - Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); - if (!lurker) + if (!botAI->IsMainTank(bot)) return false; - if (!botAI->IsMainTank(bot)) + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) return false; const time_t now = std::time(nullptr); @@ -135,35 +142,16 @@ bool TheLurkerBelowBossCastsGeyserTrigger::IsActive() // Trigger will be active only if there are at least 3 tanks in the raid bool TheLurkerBelowBossIsSubmergedTrigger::IsActive() { - Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); - if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) + if (!botAI->IsTank(bot)) return false; - Player* mainTank = nullptr; - Player* firstAssistTank = nullptr; - Player* secondAssistTank = nullptr; - - Group* group = bot->GetGroup(); - if (!group) + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) return false; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (!mainTank && memberAI->IsMainTank(member)) - mainTank = member; - else if (!firstAssistTank && memberAI->IsAssistTankOfIndex(member, 0, true)) - firstAssistTank = member; - else if (!secondAssistTank && memberAI->IsAssistTankOfIndex(member, 1, true)) - secondAssistTank = member; - } + Player* mainTank = GetGroupMainTank(botAI, bot); + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); if (!mainTank || !firstAssistTank || !secondAssistTank) return false; @@ -173,51 +161,55 @@ bool TheLurkerBelowBossIsSubmergedTrigger::IsActive() bool TheLurkerBelowNeedToPrepareTimerForSpoutTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "the lurker below") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "the lurker below"); } // Leotheras the Blind bool LeotherasTheBlindBossIsInactiveTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "greyheart spellbinder"); + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "greyheart spellbinder"); } bool LeotherasTheBlindBossTransformedIntoDemonFormTrigger::IsActive() { + if (bot->getClass() != CLASS_WARLOCK) + return false; + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; if (GetLeotherasDemonFormTank(bot) != bot) return false; - return GetActiveLeotherasDemon(botAI); + return GetActiveLeotherasDemon(bot); } bool LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger::IsActive() { - if (botAI->IsRanged(bot) || !botAI->IsTank(bot)) + if (!botAI->IsTank(bot)) return false; - if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; if (!GetLeotherasDemonFormTank(bot)) return false; - return GetPhase2LeotherasDemon(botAI); + return GetPhase2LeotherasDemon(bot); } bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (!botAI->IsRanged(bot)) return false; - if (!botAI->IsRanged(bot)) + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); @@ -231,14 +223,14 @@ bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive() bool LeotherasTheBlindBossChannelingWhirlwindTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (botAI->IsTank(bot)) return false; - if (botAI->IsTank(bot) && botAI->IsMelee(bot)) + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) return false; - Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); - if (!leotheras || leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; return leotheras->HasAura(SPELL_WHIRLWIND) || @@ -247,10 +239,13 @@ bool LeotherasTheBlindBossChannelingWhirlwindTrigger::IsActive() bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (botAI->IsRanged(bot)) return false; - if (botAI->IsRanged(bot)) + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); @@ -260,7 +255,7 @@ bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive() if (!GetLeotherasDemonFormTank(bot) && botAI->IsMainTank(bot)) return false; - return GetPhase2LeotherasDemon(botAI); + return GetPhase2LeotherasDemon(bot); } bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive() @@ -271,89 +266,68 @@ bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive() bool LeotherasTheBlindEnteredFinalPhaseTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (botAI->IsHeal(bot)) return false; - if (botAI->IsHeal(bot)) + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; - if (GetLeotherasDemonFormTank(bot) == bot) + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; - return GetPhase3LeotherasDemon(botAI) && - GetLeotherasHuman(botAI); + if (bot->getClass() == CLASS_WARLOCK && GetLeotherasDemonFormTank(bot) == bot) + return false; + + return GetPhase3LeotherasDemon(bot); } bool LeotherasTheBlindDemonFormTankNeedsAggro::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (bot->getClass() != CLASS_HUNTER) return false; - if (bot->getClass() != CLASS_HUNTER) + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; - return AI_VALUE2(Unit*, "find target", "leotheras the blind"); + return !bot->HasAura(SPELL_INSIDIOUS_WHISPER); } bool LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "leotheras the blind") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "leotheras the blind"); } // Fathom-Lord Karathress bool FathomLordKarathressBossEngagedByMainTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && - botAI->IsMainTank(bot); + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); } bool FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-guard caribdis") && - botAI->IsAssistTankOfIndex(bot, 0, false); + return botAI->IsAssistTankOfIndex(bot, 0, false) && + AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); } bool FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis") && - botAI->IsAssistTankOfIndex(bot, 1, false); + return botAI->IsAssistTankOfIndex(bot, 1, false) && + AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); } bool FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess") && - botAI->IsAssistTankOfIndex(bot, 2, false); + return botAI->IsAssistTankOfIndex(bot, 2, false) && + AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); } bool FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger::IsActive() { - Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); - if (!caribdis) - return false; - - if (!botAI->IsAssistHealOfIndex(bot, 0, true)) - return false; - - Player* firstAssistTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if (botAI->IsAssistTankOfIndex(member, 0, false)) - { - firstAssistTank = member; - break; - } - } - } - - return firstAssistTank; + return botAI->IsAssistHealOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); } bool FathomLordKarathressPullingBossesTrigger::IsActive() @@ -367,10 +341,10 @@ bool FathomLordKarathressPullingBossesTrigger::IsActive() bool FathomLordKarathressDeterminingKillOrderTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + if (botAI->IsHeal(bot)) return false; - if (botAI->IsHeal(bot)) + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) return false; if (botAI->IsDps(bot)) @@ -387,8 +361,8 @@ bool FathomLordKarathressDeterminingKillOrderTrigger::IsActive() bool FathomLordKarathressTanksNeedToEstablishAggroTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); } // Morogrim Tidewalker @@ -404,8 +378,8 @@ bool MorogrimTidewalkerPullingBossTrigger::IsActive() bool MorogrimTidewalkerBossEngagedByMainTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "morogrim tidewalker") && - botAI->IsMainTank(bot); + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); } bool MorogrimTidewalkerWaterGlobulesAreIncomingTrigger::IsActive() @@ -421,8 +395,11 @@ bool MorogrimTidewalkerWaterGlobulesAreIncomingTrigger::IsActive() bool LadyVashjBossEngagedByMainTankTrigger::IsActive() { + if (!botAI->IsMainTank(bot)) + return false; + return AI_VALUE2(Unit*, "find target", "lady vashj") && - !IsLadyVashjInPhase2(botAI) && botAI->IsMainTank(bot); + !IsLadyVashjInPhase2(botAI); } bool LadyVashjBossEngagedByRangedInPhase1Trigger::IsActive() @@ -439,10 +416,7 @@ bool LadyVashjCastsShockBlastOnHighestAggroTrigger::IsActive() IsLadyVashjInPhase2(botAI)) return false; - if (!IsMainTankInSameSubgroup(bot)) - return false; - - return true; + return IsMainTankInSameSubgroup(botAI, bot); } bool LadyVashjBotHasStaticChargeTrigger::IsActive() @@ -450,14 +424,15 @@ bool LadyVashjBotHasStaticChargeTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "lady vashj")) return false; - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasAura(SPELL_STATIC_CHARGE)) - return true; - } + Player* member = ref->GetSource(); + if (member && member->HasAura(SPELL_STATIC_CHARGE)) + return true; } return false; @@ -500,9 +475,10 @@ bool LadyVashjTaintedElementalCheatTrigger::IsActive() return false; bool taintedPresent = false; - Unit* taintedUnit = AI_VALUE2(Unit*, "find target", "tainted elemental"); - if (taintedUnit) + if (AI_VALUE2(Unit*, "find target", "tainted elemental")) + { taintedPresent = true; + } else { GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses"); @@ -513,13 +489,11 @@ bool LadyVashjTaintedElementalCheatTrigger::IsActive() if (!object) continue; - if (Creature* creature = object->ToCreature()) + if (Creature* creature = object->ToCreature(); + creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive()) { - if (creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive()) - { - taintedPresent = true; - break; - } + taintedPresent = true; + break; } } } @@ -527,12 +501,8 @@ bool LadyVashjTaintedElementalCheatTrigger::IsActive() if (!taintedPresent) return false; - Group* group = bot->GetGroup(); - if (!group) - return false; - - return (GetDesignatedCoreLooter(group, botAI) == bot && - !bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)); + return GetDesignatedCoreLooter(botAI, bot) == bot && + !bot->HasItemCount(ITEM_TAINTED_CORE, 1, false); } bool LadyVashjTaintedCoreWasLootedTrigger::IsActive() @@ -540,54 +510,24 @@ bool LadyVashjTaintedCoreWasLootedTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI)) return false; - Group* group = bot->GetGroup(); - if (!group) - return false; - - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); - Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + auto coreHandlers = GetCoreHandlers(botAI, bot); - auto hasCore = [](Player* player) -> bool - { - return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); - }; + bool isCoreHandler = false; + for (Player* handler : coreHandlers) + if (handler == bot) + isCoreHandler = true; - if (bot == designatedLooter) - { - if (!hasCore(bot)) - return false; - } - else if (bot == firstCorePasser) - { - if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || - hasCore(fourthCorePasser)) - return false; - } - else if (bot == secondCorePasser) - { - if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) - return false; - } - else if (bot == thirdCorePasser) - { - if (hasCore(fourthCorePasser)) - return false; - } - else if (bot != fourthCorePasser) + if (!isCoreHandler) return false; - if (AnyRecentCoreInInventory(group, botAI)) - return true; - // First and second passers move to positions as soon as the elemental appears - if (AI_VALUE2(Unit*, "find target", "tainted elemental") && - (bot == firstCorePasser || bot == secondCorePasser)) + Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (tainted && coreHandlers[0]->GetExactDist2d(tainted) < 5.0f && + (bot == coreHandlers[1] || bot == coreHandlers[2])) return true; - return false; + // Main logic: run if core is in play for this bot or a prior handler + return AnyRecentCoreInInventory(botAI, bot); } bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() @@ -599,18 +539,7 @@ bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() if (!IsLadyVashjInPhase2(botAI)) return bot->HasItemCount(ITEM_TAINTED_CORE, 1, false); - Group* group = bot->GetGroup(); - if (!group) - return false; - - Player* coreHandlers[] = - { - GetDesignatedCoreLooter(group, botAI), - GetFirstTaintedCorePasser(group, botAI), - GetSecondTaintedCorePasser(group, botAI), - GetThirdTaintedCorePasser(group, botAI), - GetFourthTaintedCorePasser(group, botAI) - }; + auto coreHandlers = GetCoreHandlers(botAI, bot); if (bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)) { @@ -625,24 +554,6 @@ bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() return false; } -bool LadyVashjNeedToResetCorePassingTrackersTrigger::IsActive() -{ - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - if (!vashj || IsLadyVashjInPhase2(botAI)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr) || - GetDesignatedCoreLooter(group, botAI) == bot || - GetFirstTaintedCorePasser(group, botAI) == bot || - GetSecondTaintedCorePasser(group, botAI) == bot || - GetThirdTaintedCorePasser(group, botAI) == bot || - GetFourthTaintedCorePasser(group, botAI) == bot; -} - bool LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger::IsActive() { return IsLadyVashjInPhase3(botAI); @@ -653,17 +564,18 @@ bool LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "lady vashj")) return false; - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->HasAura(SPELL_ENTANGLE)) - continue; + Player* member = ref->GetSource(); + if (!member || !member->HasAura(SPELL_ENTANGLE)) + continue; - if (botAI->IsMelee(member)) - return true; - } + if (botAI->IsMelee(member)) + return true; } return false; diff --git a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h index e106b58f3ee..4bf9e741608 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h +++ b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCTRIGGERS_H #define _PLAYERBOT_RAIDSSCTRIGGERS_H @@ -387,14 +392,6 @@ class LadyVashjTaintedCoreIsUnusableTrigger : public Trigger bool IsActive() override; }; -class LadyVashjNeedToResetCorePassingTrackersTrigger : public Trigger -{ -public: - LadyVashjNeedToResetCorePassingTrackersTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "lady vashj need to reset core passing trackers") {} - bool IsActive() override; -}; - class LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger : public Trigger { public: diff --git a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp index 7bda085be12..4ebeb6f885f 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp @@ -1,3 +1,8 @@ +/* + * 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 "RaidSSCHelpers.h" #include "AiFactory.h" #include "Creature.h" @@ -79,61 +84,54 @@ namespace SerpentShrineCavernHelpers std::unordered_map leotherasDemonFormDpsWaitTimer; std::unordered_map leotherasFinalPhaseDpsWaitTimer; - Unit* GetLeotherasHuman(PlayerbotAI* botAI) + Unit* GetLeotherasHuman(Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && - unit->IsInCombat() && !unit->HasAura(SPELL_METAMORPHOSIS)) - return unit; - } + constexpr float searchRadius = 100.0f; + Creature* leotheras = + bot->FindNearestCreature(NPC_LEOTHERAS_THE_BLIND, searchRadius, true); + + if (leotheras && leotheras->IsInCombat() && + !leotheras->HasAura(SPELL_METAMORPHOSIS)) + return leotheras; + return nullptr; } - Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI) + Unit* GetPhase2LeotherasDemon(Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && - unit->HasAura(SPELL_METAMORPHOSIS)) - return unit; - } + constexpr float searchRadius = 100.0f; + Creature* leotheras = + bot->FindNearestCreature(NPC_LEOTHERAS_THE_BLIND, searchRadius, true); + + if (leotheras && leotheras->HasAura(SPELL_METAMORPHOSIS)) + return leotheras; + return nullptr; } - Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI) + Unit* GetPhase3LeotherasDemon(Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == NPC_SHADOW_OF_LEOTHERAS) - return unit; - } - return nullptr; + constexpr float searchRadius = 100.0f; + return bot->FindNearestCreature(NPC_SHADOW_OF_LEOTHERAS, searchRadius, true); } - Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI) + Unit* GetActiveLeotherasDemon(Player* bot) { - Unit* phase2 = GetPhase2LeotherasDemon(botAI); - Unit* phase3 = GetPhase3LeotherasDemon(botAI); + Unit* phase2 = GetPhase2LeotherasDemon(bot); + Unit* phase3 = GetPhase3LeotherasDemon(bot); return phase2 ? phase2 : phase3; } + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot Player* GetLeotherasDemonFormTank(Player* bot) { Group* group = bot->GetGroup(); if (!group) return nullptr; - // (1) First loop: Return the first assistant Warlock (real player or bot) + Player* fallbackWarlock = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); @@ -142,21 +140,12 @@ namespace SerpentShrineCavernHelpers if (group->IsAssistant(member->GetGUID())) return member; - } - // (2) Fall back to first found bot Warlock - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - member->getClass() != CLASS_WARLOCK) - continue; - - return member; + if (!fallbackWarlock && GET_PLAYERBOT_AI(member)) + fallbackWarlock = member; } - // (3) Return nullptr if none found - return nullptr; + return fallbackWarlock; } // Fathom-Lord Karathress @@ -182,16 +171,15 @@ namespace SerpentShrineCavernHelpers // Lady Vashj - const Position VASHJ_PLATFORM_CENTER_POSITION = { 29.634f, -923.541f, 42.985f }; + const Position VASHJ_PLATFORM_CENTER_POSITION = { 29.634f, -923.541f, 42.902f }; - std::unordered_map vashjRangedPositions; std::unordered_map hasReachedVashjRangedPosition; std::unordered_map nearestTriggerGuid; std::unordered_map intendedLineup; std::unordered_map lastImbueAttempt; - std::unordered_map lastCoreInInventoryTime; + std::unordered_map lastCoreInInventoryTime; - bool IsMainTankInSameSubgroup(Player* bot) + bool IsMainTankInSameSubgroup(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); if (!group || !group->isRaidGroup()) @@ -210,11 +198,8 @@ namespace SerpentShrineCavernHelpers if (group->GetMemberGroup(member->GetGUID()) != botSubGroup) continue; - if (PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member)) - { - if (memberAI->IsMainTank(member)) - return true; - } + if (botAI->IsMainTank(member)) + return true; } return false; @@ -277,38 +262,9 @@ namespace SerpentShrineCavernHelpers return false; } - bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds) - { - Unit* vashj = - botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); - if (!vashj) - return false; - - if (group) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) - return true; - } - } - - const uint32 instanceId = vashj->GetMap()->GetInstanceId(); - const time_t now = std::time(nullptr); - - auto it = lastCoreInInventoryTime.find(instanceId); - if (it != lastCoreInInventoryTime.end()) - { - if ((now - it->second) <= static_cast(graceSeconds)) - return true; - } - - return false; - } - - Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI) + Player* GetDesignatedCoreLooter(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; @@ -317,10 +273,15 @@ namespace SerpentShrineCavernHelpers if (!leaderGuid.IsEmpty()) leader = ObjectAccessor::FindPlayer(leaderGuid); + // If cheats are disabled, the group leader will be the designated looter if (!botAI->HasCheat(BotCheatMask::raid)) return leader; - Player* fallback = leader; + // Priority: (1) assistant melee DPS, (2) other melee DPS, (3) any ranged DPS + Player* meleeDpsAssistant = nullptr; + Player* meleeDps = nullptr; + Player* rangedDps = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); @@ -331,22 +292,36 @@ namespace SerpentShrineCavernHelpers if (!memberAI) continue; - if (memberAI->IsMelee(member) && memberAI->IsDps(member)) - return member; + if (!meleeDpsAssistant && memberAI->IsMelee(member) && + memberAI->IsDps(member) && group->IsAssistant(member->GetGUID())) + { + meleeDpsAssistant = member; + break; + } + + if (!meleeDps && memberAI->IsMelee(member) && memberAI->IsDps(member)) + meleeDps = member; - if (!fallback && memberAI->IsRangedDps(member)) - fallback = member; + if (!rangedDps && memberAI->IsRangedDps(member)) + rangedDps = member; } - return fallback ? fallback : leader; + if (meleeDpsAssistant) + return meleeDpsAssistant; + if (meleeDps) + return meleeDps; + if (rangedDps) + return rangedDps; + return leader; } - Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetFirstTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -355,32 +330,29 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistHealOfIndex(member, 0, true)) + if (memberAI && memberAI->IsAssistHealOfIndex(member, 0, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter) + return member; } return nullptr; } - Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetSecondTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -390,34 +362,31 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistHealOfIndex(member, 1, true)) + if (memberAI && memberAI->IsAssistHealOfIndex(member, 1, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter || - member == firstCorePasser) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter && + member != firstCorePasser) + return member; } return nullptr; } - Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetThirdTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); + Player* secondCorePasser = GetSecondTaintedCorePasser(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -427,35 +396,32 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistHealOfIndex(member, 2, true)) + if (memberAI && memberAI->IsAssistHealOfIndex(member, 2, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter || - member == firstCorePasser || member == secondCorePasser) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter && + member != firstCorePasser && member != secondCorePasser) + return member; } return nullptr; } - Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetFourthTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); + Player* secondCorePasser = GetSecondTaintedCorePasser(botAI, bot); + Player* thirdCorePasser = GetThirdTaintedCorePasser(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -466,27 +432,75 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistRangedDpsOfIndex(member, 0, true)) + if (memberAI && memberAI->IsAssistRangedDpsOfIndex(member, 0, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter || - member == firstCorePasser || member == secondCorePasser || - member == thirdCorePasser) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter && + member != firstCorePasser && member != secondCorePasser && + member != thirdCorePasser) + return member; } return nullptr; } + std::array GetCoreHandlers(PlayerbotAI* botAI, Player* bot) + { + return + { + GetDesignatedCoreLooter(botAI, bot), + GetFirstTaintedCorePasser(botAI, bot), + GetSecondTaintedCorePasser(botAI, bot), + GetThirdTaintedCorePasser(botAI, bot), + GetFourthTaintedCorePasser(botAI, bot) + }; + } + + // Checks if any bot from earlier in the passing sequence has the Tainted Core or + // had it within the prior 3 seconds so the chain is not broken when the Core is in transit + bool AnyRecentCoreInInventory(PlayerbotAI* botAI, Player* bot) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + auto coreHandlers = GetCoreHandlers(botAI, bot); + + int8 myIndex = -1; + for (int8 i = 0; i < 5; ++i) + if (coreHandlers[i] && coreHandlers[i] == bot) + myIndex = i; + + if (myIndex == -1) + return false; + + const time_t now = std::time(nullptr); + constexpr uint8 lookbackSeconds = 3; + + for (int8 i = 0; i <= myIndex; ++i) + { + Player* handler = coreHandlers[i]; + if (!handler) + continue; + + if (handler->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return true; + + auto it = lastCoreInInventoryTime.find(handler->GetGUID()); + if (it != lastCoreInInventoryTime.end() && + (now - it->second) <= static_cast(lookbackSeconds)) + return true; + } + + return false; + } + const std::vector SHIELD_GENERATOR_DB_GUIDS = { 47482, // NW @@ -510,10 +524,7 @@ namespace SerpentShrineCavernHelpers continue; GameObject* go = bounds.first->second; - if (!go) - continue; - - if (go->GetGoState() != GO_STATE_READY) + if (!go || go->GetGoState() != GO_STATE_READY) continue; GeneratorInfo info; @@ -529,7 +540,7 @@ namespace SerpentShrineCavernHelpers // Returns the nearest active Shield Generator to the bot // Active generators are powered by NPC_WORLD_INVISIBLE_TRIGGER creatures, - // which depawn after use + // which despawn after use Unit* GetNearestActiveShieldGeneratorTriggerByEntry(Unit* reference) { if (!reference) diff --git a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h index a725e28fcd4..72ee52c8c1f 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h +++ b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h @@ -1,3 +1,8 @@ +/* + * 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_RAIDSSCHELPERS_H_ #define _PLAYERBOT_RAIDSSCHELPERS_H_ @@ -64,9 +69,6 @@ namespace SerpentShrineCavernHelpers // Warlock SPELL_CURSE_OF_EXHAUSTION = 18223, - - // Item - SPELL_HEAVY_NETHERWEAVE_NET = 31368, }; enum SerpentShrineCavernNPCs @@ -105,9 +107,6 @@ namespace SerpentShrineCavernHelpers { // Lady Vashj ITEM_TAINTED_CORE = 31088, - - // Tailoring - ITEM_HEAVY_NETHERWEAVE_NET = 24269, }; constexpr uint32 SSC_MAP_ID = 548; @@ -134,10 +133,10 @@ namespace SerpentShrineCavernHelpers extern std::unordered_map leotherasHumanFormDpsWaitTimer; extern std::unordered_map leotherasDemonFormDpsWaitTimer; extern std::unordered_map leotherasFinalPhaseDpsWaitTimer; - Unit* GetLeotherasHuman(PlayerbotAI* botAI); - Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI); - Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI); - Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI); + Unit* GetLeotherasHuman(Player* bot); + Unit* GetPhase2LeotherasDemon(Player* bot); + Unit* GetPhase3LeotherasDemon(Player* bot); + Unit* GetActiveLeotherasDemon(Player* bot); Player* GetLeotherasDemonFormTank(Player* bot); // Fathom-Lord Karathress @@ -158,25 +157,26 @@ namespace SerpentShrineCavernHelpers extern std::unordered_map tidewalkerRangedStep; // Lady Vashj - constexpr float VASHJ_PLATFORM_Z = 42.985f; + constexpr float VASHJ_PLATFORM_CENTER_Z = 42.902f; + constexpr float VASHJ_PLATFORM_EDGE_Z = 41.097f; extern const Position VASHJ_PLATFORM_CENTER_POSITION; - extern std::unordered_map vashjRangedPositions; extern std::unordered_map hasReachedVashjRangedPosition; extern std::unordered_map nearestTriggerGuid; extern std::unordered_map intendedLineup; extern std::unordered_map lastImbueAttempt; - extern std::unordered_map lastCoreInInventoryTime; - bool IsMainTankInSameSubgroup(Player* bot); + extern std::unordered_map lastCoreInInventoryTime; + bool IsMainTankInSameSubgroup(PlayerbotAI* botAI, Player* bot); bool IsLadyVashjInPhase1(PlayerbotAI* botAI); bool IsLadyVashjInPhase2(PlayerbotAI* botAI); bool IsLadyVashjInPhase3(PlayerbotAI* botAI); bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI); - bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds = 3); - Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI); - Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI); - Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI); - Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI); - Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetDesignatedCoreLooter(PlayerbotAI* botAI, Player* bot); + Player* GetFirstTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + Player* GetSecondTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + Player* GetThirdTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + Player* GetFourthTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + std::array GetCoreHandlers(PlayerbotAI* botAI, Player* bot); + bool AnyRecentCoreInInventory(PlayerbotAI* botAI, Player* bot); struct GeneratorInfo { ObjectGuid guid; float x, y, z; }; extern const std::vector SHIELD_GENERATOR_DB_GUIDS; std::vector GetAllGeneratorInfosByDbGuids( From 5e7613f71943745073e00937591e911194040337 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:21:55 -0700 Subject: [PATCH 03/87] Change reinterpret cast to dynamic cast. (#2182) # Pull Request In a few instances the code used reinterpret cast. This is potentially risky if the object is incorrect. This is a safer approach. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- src/Bot/PlayerbotMgr.cpp | 4 ++-- src/Mgr/Guild/GuildTaskMgr.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index ba0e3643d5f..9859877f94b 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -1796,7 +1796,7 @@ PlayerbotAI* PlayerbotsMgr::GetPlayerbotAI(Player* player) if (itr != _playerbotsAIMap.end()) { if (itr->second->IsBotAI()) - return reinterpret_cast(itr->second); + return dynamic_cast(itr->second); } return nullptr; @@ -1812,7 +1812,7 @@ PlayerbotMgr* PlayerbotsMgr::GetPlayerbotMgr(Player* player) if (itr != _playerbotsMgrMap.end()) { if (!itr->second->IsBotAI()) - return reinterpret_cast(itr->second); + return dynamic_cast(itr->second); } return nullptr; diff --git a/src/Mgr/Guild/GuildTaskMgr.cpp b/src/Mgr/Guild/GuildTaskMgr.cpp index 96162c044a6..d73662a7911 100644 --- a/src/Mgr/Guild/GuildTaskMgr.cpp +++ b/src/Mgr/Guild/GuildTaskMgr.cpp @@ -1070,7 +1070,7 @@ void GuildTaskMgr::CheckKillTaskInternal(Player* player, Unit* victim) if (!victim->IsCreature()) return; - Creature* creature = reinterpret_cast(victim); + Creature* creature = dynamic_cast(victim); if (!creature) return; From a695ac77fa3658081a86e1542e25571fe7e52609 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 13 Mar 2026 22:22:11 +0100 Subject: [PATCH 04/87] Small fixes to naxxramas strategy (#2201) # Pull Request Small fixes to naxxramas strategy --- ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: Copilot CLI to code review --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h | 16 ++++++++-------- .../Action/RaidNaxxActions_Anubrekhan.cpp | 12 ++++-------- ...eman.cpp => RaidNaxxActions_FourHorsemen.cpp} | 6 +++--- .../Action/RaidNaxxActions_Sapphiron.cpp | 1 - .../Action/RaidNaxxActions_Thaddius.cpp | 4 ++-- .../Naxxramas/Multiplier/RaidNaxxMultipliers.cpp | 2 +- .../Naxxramas/Multiplier/RaidNaxxMultipliers.h | 8 ++++---- src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h | 13 +++++-------- src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h | 8 ++++---- .../Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp | 12 ++++++------ .../Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp | 4 ++-- src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h | 12 ++++++------ src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h | 9 +++++---- src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h | 1 + 14 files changed, 51 insertions(+), 57 deletions(-) rename src/Ai/Raid/Naxxramas/Action/{RaidNaxxActions_FourHorseman.cpp => RaidNaxxActions_FourHorsemen.cpp} (87%) diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h index d5bfe0bcfcd..f2712d74dd5 100644 --- a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h @@ -37,10 +37,10 @@ class GrobbulusRotateAction : public RotateAroundTheCenterPointAction uint32 GetCurrWaypoint() override; }; -class GrobblulusMoveCenterAction : public MoveInsideAction +class GrobbulusMoveCenterAction : public MoveInsideAction { public: - GrobblulusMoveCenterAction(PlayerbotAI* ai) : MoveInsideAction(ai, 3281.23f, -3310.38f, 5.0f) {} + GrobbulusMoveCenterAction(PlayerbotAI* ai) : MoveInsideAction(ai, 3281.23f, -3310.38f, 5.0f) {} }; class GrobbulusMoveAwayAction : public MovementAction @@ -173,26 +173,26 @@ class RazuviousTargetAction : public AttackAction RazuviousBossHelper helper; }; -class HorsemanAttractAlternativelyAction : public AttackAction +class FourHorsemenAttractAlternativelyAction : public AttackAction { public: - HorsemanAttractAlternativelyAction(PlayerbotAI* ai) : AttackAction(ai, "horseman attract alternatively"), helper(ai) + FourHorsemenAttractAlternativelyAction(PlayerbotAI* ai) : AttackAction(ai, "four horsemen attract alternatively"), helper(ai) { } bool Execute(Event event) override; protected: - FourhorsemanBossHelper helper; + FourHorsemenBossHelper helper; }; -class HorsemanAttactInOrderAction : public AttackAction +class FourHorsemenAttackInOrderAction : public AttackAction { public: - HorsemanAttactInOrderAction(PlayerbotAI* ai) : AttackAction(ai, "horseman attact in order"), helper(ai) {} + FourHorsemenAttackInOrderAction(PlayerbotAI* ai) : AttackAction(ai, "four horsemen attack in order"), helper(ai) {} bool Execute(Event event) override; protected: - FourhorsemanBossHelper helper; + FourHorsemenBossHelper helper; }; // class SapphironGroundMainTankPositionAction : public MovementAction diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp index 7ff77eadc11..4391ba76cf8 100644 --- a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp @@ -1,7 +1,6 @@ -#include "RaidNaxxActions.h" - #include "ObjectGuid.h" #include "Playerbots.h" +#include "RaidNaxxActions.h" bool AnubrekhanChooseTargetAction::Execute(Event /*event*/) { @@ -66,13 +65,10 @@ bool AnubrekhanPositionAction::Execute(Event /*event*/) { uint32 nearest = FindNearestWaypoint(); uint32 next_point; - if (inPhase) - next_point = (nearest + 1) % intervals; - else - next_point = nearest; + next_point = (nearest + 1) % intervals; - return MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, bot->GetPositionZ(), false, false, - false, false, MovementPriority::MOVEMENT_COMBAT); + return MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, + bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); } else return MoveInside(533, 3272.49f, -3476.27f, bot->GetPositionZ(), 3.0f, MovementPriority::MOVEMENT_COMBAT); diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorsemen.cpp similarity index 87% rename from src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp rename to src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorsemen.cpp index 5673638879e..291b225e734 100644 --- a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorsemen.cpp @@ -2,7 +2,7 @@ #include "Playerbots.h" -bool HorsemanAttractAlternativelyAction::Execute(Event /*event*/) +bool FourHorsemenAttractAlternativelyAction::Execute(Event /*event*/) { if (!helper.UpdateBossAI()) return false; @@ -13,13 +13,13 @@ bool HorsemanAttractAlternativelyAction::Execute(Event /*event*/) return true; Unit* attackTarget = helper.CurrentAttackTarget(); - if (context->GetValue("current target")->Get() != attackTarget) + if (attackTarget && context->GetValue("current target")->Get() != attackTarget) return Attack(attackTarget); return false; } -bool HorsemanAttactInOrderAction::Execute(Event /*event*/) +bool FourHorsemenAttackInOrderAction::Execute(Event /*event*/) { if (!helper.UpdateBossAI()) return false; diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp index af906da8b85..5fb6d868680 100644 --- a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp @@ -70,7 +70,6 @@ bool SapphironFlightPositionAction::MoveToNearestIcebolt() if (!group) return false; - Group::MemberSlotList const& slots = group->GetMemberSlots(); Player* playerWithIcebolt = nullptr; float minDistance; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp index 0935f0545fb..145604da0cb 100644 --- a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp @@ -13,7 +13,7 @@ bool ThaddiusAttackNearestPetAction::isUseful() return false; Unit* target = helper.GetNearestPet(); - if (!bot->IsWithinDistInMap(target, 50.0f)) + if (!target || !bot->IsWithinDistInMap(target, 50.0f)) return false; return true; @@ -22,7 +22,7 @@ bool ThaddiusAttackNearestPetAction::isUseful() bool ThaddiusAttackNearestPetAction::Execute(Event /*event*/) { Unit* target = helper.GetNearestPet(); - if (!bot->IsWithinLOSInMap(target)) + if (!target || !bot->IsWithinLOSInMap(target)) return MoveTo(target, 0, MovementPriority::MOVEMENT_COMBAT); if (AI_VALUE(Unit*, "current target") != target) diff --git a/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp index d34b48e6d54..39e7668fa9f 100644 --- a/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp +++ b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp @@ -245,7 +245,7 @@ float AnubrekhanGenericMultiplier::GetValue(Action* action) return 1.0f; } -float FourhorsemanGenericMultiplier::GetValue(Action* action) +float FourHorsemenGenericMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit*, "find target", "sir zeliek"); if (!boss) diff --git a/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h index af702f6056c..f51420a2a38 100644 --- a/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h +++ b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h @@ -1,6 +1,6 @@ -#ifndef _PLAYERRBOT_RAIDNAXXMULTIPLIERS_H -#define _PLAYERRBOT_RAIDNAXXMULTIPLIERS_H +#ifndef _PLAYERBOT_RAIDNAXXMULTIPLIERS_H +#define _PLAYERBOT_RAIDNAXXMULTIPLIERS_H #include "Multiplier.h" #include "RaidNaxxBossHelper.h" @@ -84,10 +84,10 @@ class AnubrekhanGenericMultiplier : public Multiplier virtual float GetValue(Action* action); }; -class FourhorsemanGenericMultiplier : public Multiplier +class FourHorsemenGenericMultiplier : public Multiplier { public: - FourhorsemanGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "fourhorseman generic") {} + FourHorsemenGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "four horsemen generic") {} public: virtual float GetValue(Action* action); diff --git a/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h b/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h index 45941d679c4..4aaf9462e77 100644 --- a/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h +++ b/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h @@ -31,8 +31,8 @@ class RaidNaxxActionContext : public NamedObjectContext creators["razuvious use obedience crystal"] = &RaidNaxxActionContext::razuvious_use_obedience_crystal; creators["razuvious target"] = &RaidNaxxActionContext::razuvious_target; - creators["horseman attract alternatively"] = &RaidNaxxActionContext::horseman_attract_alternatively; - creators["horseman attack in order"] = &RaidNaxxActionContext::horseman_attack_in_order; + creators["four horsemen attract alternatively"] = &RaidNaxxActionContext::four_horsemen_attract_alternatively; + creators["four horsemen attack in order"] = &RaidNaxxActionContext::four_horsemen_attack_in_order; creators["sapphiron ground position"] = &RaidNaxxActionContext::sapphiron_ground_position; creators["sapphiron flight position"] = &RaidNaxxActionContext::sapphiron_flight_position; @@ -56,7 +56,7 @@ class RaidNaxxActionContext : public NamedObjectContext private: static Action* go_behind_the_boss(PlayerbotAI* ai) { return new GrobbulusGoBehindAction(ai); } static Action* rotate_grobbulus(PlayerbotAI* ai) { return new GrobbulusRotateAction(ai); } - static Action* grobbulus_move_center(PlayerbotAI* ai) { return new GrobblulusMoveCenterAction(ai); } + static Action* grobbulus_move_center(PlayerbotAI* ai) { return new GrobbulusMoveCenterAction(ai); } static Action* grobbulus_move_away(PlayerbotAI* ai) { return new GrobbulusMoveAwayAction(ai); } //static Action* heigan_dance_melee(PlayerbotAI* ai) { return new HeiganDanceMeleeAction(ai); } //static Action* heigan_dance_ranged(PlayerbotAI* ai) { return new HeiganDanceRangedAction(ai); } @@ -70,11 +70,8 @@ class RaidNaxxActionContext : public NamedObjectContext { return new RazuviousUseObedienceCrystalAction(ai); } - static Action* horseman_attract_alternatively(PlayerbotAI* ai) - { - return new HorsemanAttractAlternativelyAction(ai); - } - static Action* horseman_attack_in_order(PlayerbotAI* ai) { return new HorsemanAttactInOrderAction(ai); } + static Action* four_horsemen_attract_alternatively(PlayerbotAI* ai) { return new FourHorsemenAttractAlternativelyAction(ai); } + static Action* four_horsemen_attack_in_order(PlayerbotAI* ai) { return new FourHorsemenAttackInOrderAction(ai); } // static Action* sapphiron_ground_main_tank_position(PlayerbotAI* ai) { return new // SapphironGroundMainTankPositionAction(ai); } static Action* sapphiron_ground_position(PlayerbotAI* ai) { return new SapphironGroundPositionAction(ai); } diff --git a/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h b/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h index d980b4768d6..4d1557d566b 100644 --- a/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h +++ b/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h @@ -30,8 +30,8 @@ class RaidNaxxTriggerContext : public NamedObjectContext creators["razuvious tank"] = &RaidNaxxTriggerContext::razuvious_tank; creators["razuvious nontank"] = &RaidNaxxTriggerContext::razuvious_nontank; - creators["horseman attractors"] = &RaidNaxxTriggerContext::horseman_attractors; - creators["horseman except attractors"] = &RaidNaxxTriggerContext::horseman_except_attractors; + creators["four horsemen attractors"] = &RaidNaxxTriggerContext::four_horsemen_attractors; + creators["four horsemen except attractors"] = &RaidNaxxTriggerContext::four_horsemen_except_attractors; creators["sapphiron ground"] = &RaidNaxxTriggerContext::sapphiron_ground; creators["sapphiron flight"] = &RaidNaxxTriggerContext::sapphiron_flight; @@ -66,8 +66,8 @@ class RaidNaxxTriggerContext : public NamedObjectContext static Trigger* razuvious_tank(PlayerbotAI* ai) { return new RazuviousTankTrigger(ai); } static Trigger* razuvious_nontank(PlayerbotAI* ai) { return new RazuviousNontankTrigger(ai); } - static Trigger* horseman_attractors(PlayerbotAI* ai) { return new HorsemanAttractorsTrigger(ai); } - static Trigger* horseman_except_attractors(PlayerbotAI* ai) { return new HorsemanExceptAttractorsTrigger(ai); } + static Trigger* four_horsemen_attractors(PlayerbotAI* ai) { return new FourHorsemenAttractorsTrigger(ai); } + static Trigger* four_horsemen_except_attractors(PlayerbotAI* ai) { return new FourHorsemenExceptAttractorsTrigger(ai); } static Trigger* sapphiron_ground(PlayerbotAI* ai) { return new SapphironGroundTrigger(ai); } static Trigger* sapphiron_flight(PlayerbotAI* ai) { return new SapphironFlightTrigger(ai); } diff --git a/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp index e0c3421d1b0..51f59f75586 100644 --- a/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp +++ b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp @@ -97,13 +97,13 @@ void RaidNaxxStrategy::InitTriggers(std::vector& triggers) { NextAction("razuvious target", ACTION_RAID + 1) } )); - // four horseman - triggers.push_back(new TriggerNode("horseman attractors", - { NextAction("horseman attract alternatively", ACTION_RAID + 1) } + // four horsemen + triggers.push_back(new TriggerNode("four horsemen attractors", + { NextAction("four horsemen attract alternatively", ACTION_RAID + 1) } )); - triggers.push_back(new TriggerNode("horseman except attractors", - { NextAction("horseman attack in order", ACTION_RAID + 1) } + triggers.push_back(new TriggerNode("four horsemen except attractors", + { NextAction("four horsemen attack in order", ACTION_RAID + 1) } )); // sapphiron @@ -150,7 +150,7 @@ void RaidNaxxStrategy::InitMultipliers(std::vector& multipliers) multipliers.push_back(new InstructorRazuviousGenericMultiplier(botAI)); multipliers.push_back(new KelthuzadGenericMultiplier(botAI)); multipliers.push_back(new AnubrekhanGenericMultiplier(botAI)); - multipliers.push_back(new FourhorsemanGenericMultiplier(botAI)); + multipliers.push_back(new FourHorsemenGenericMultiplier(botAI)); // multipliers.push_back(new GothikGenericMultiplier(botAI)); multipliers.push_back(new GluthGenericMultiplier(botAI)); } diff --git a/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp index f4fb38e1d12..3f0fc98b3a7 100644 --- a/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp +++ b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp @@ -114,7 +114,7 @@ bool RazuviousNontankTrigger::IsActive() return helper.UpdateBossAI() && !(bot->getClass() == CLASS_PRIEST); } -bool HorsemanAttractorsTrigger::IsActive() +bool FourHorsemenAttractorsTrigger::IsActive() { if (!helper.UpdateBossAI()) return false; @@ -122,7 +122,7 @@ bool HorsemanAttractorsTrigger::IsActive() return helper.IsAttracter(bot); } -bool HorsemanExceptAttractorsTrigger::IsActive() +bool FourHorsemenExceptAttractorsTrigger::IsActive() { if (!helper.UpdateBossAI()) return false; diff --git a/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h index 6f7727cf22a..c48cadd798c 100644 --- a/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h +++ b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h @@ -186,24 +186,24 @@ class ThaddiusPhaseThaddiusTrigger : public Trigger ThaddiusBossHelper helper; }; -class HorsemanAttractorsTrigger : public Trigger +class FourHorsemenAttractorsTrigger : public Trigger { public: - HorsemanAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "fourhorsemen attractors"), helper(ai) {} + FourHorsemenAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "four horsemen attractors"), helper(ai) {} bool IsActive() override; private: - FourhorsemanBossHelper helper; + FourHorsemenBossHelper helper; }; -class HorsemanExceptAttractorsTrigger : public Trigger +class FourHorsemenExceptAttractorsTrigger : public Trigger { public: - HorsemanExceptAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "fourhorsemen except attractors"), helper(ai) {} + FourHorsemenExceptAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "four horsemen except attractors"), helper(ai) {} bool IsActive() override; private: - FourhorsemanBossHelper helper; + FourHorsemenBossHelper helper; }; class SapphironGroundTrigger : public Trigger diff --git a/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h b/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h index 9b87bb5830e..a13a5b893dc 100644 --- a/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h +++ b/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h @@ -202,7 +202,7 @@ class SapphironBossHelper : public AiObject } bool FindPosToAvoidChill(std::vector& dest) { - Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::Chill25}); + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::Chill10, NaxxSpellIds::Chill25}); if (!aura) { // Fallback to name for custom spell data. @@ -363,13 +363,13 @@ class LoathebBossHelper : public AiObject Unit* _unit = nullptr; }; -class FourhorsemanBossHelper : public AiObject +class FourHorsemenBossHelper : public AiObject { public: const float posZ = 241.27f; const std::pair attractPos[2] = {{2502.03f, -2910.90f}, {2484.61f, -2947.07f}}; // left (sir zeliek), right (lady blaumeux) - FourhorsemanBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + FourHorsemenBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} bool UpdateBossAI() { if (!bot->IsInCombat()) @@ -497,7 +497,8 @@ class ThaddiusBossHelper : public AiObject if (feugen && feugen->IsAlive()) unit = feugen; - if (stalagg && stalagg->IsAlive() && (!feugen || bot->GetDistance(stalagg) < bot->GetDistance(feugen))) + if (stalagg && stalagg->IsAlive() && + (!feugen || !feugen->IsAlive() || bot->GetDistance(stalagg) < bot->GetDistance(feugen))) unit = stalagg; return unit; diff --git a/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h b/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h index 53299e7e9ad..94c013eb7c3 100644 --- a/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h +++ b/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h @@ -58,6 +58,7 @@ namespace NaxxSpellIds // Sapphiron static constexpr uint32 Icebolt10 = 28522; static constexpr uint32 Icebolt25 = 28526; + static constexpr uint32 Chill10 = 28547; static constexpr uint32 Chill25 = 55699; /* // Fight From d6f396ab5077de89e342bacd7b3b5408d98ead96 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:50:20 -0700 Subject: [PATCH 05/87] CoreUpdate (#2207) Core PR update. https://github.com/mod-playerbots/azerothcore-wotlk/pull/178 Core set packet as const, and so had to recast. --- src/Script/PlayerbotsSecureLogin.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Script/PlayerbotsSecureLogin.cpp b/src/Script/PlayerbotsSecureLogin.cpp index fc54a82b7dd..efede3ee51e 100644 --- a/src/Script/PlayerbotsSecureLogin.cpp +++ b/src/Script/PlayerbotsSecureLogin.cpp @@ -52,15 +52,16 @@ class PlayerbotsSecureLoginServerScript : public ServerScript PlayerbotsSecureLoginServerScript() : ServerScript("PlayerbotsSecureLoginServerScript", { SERVERHOOK_CAN_PACKET_RECEIVE }) {} - bool CanPacketReceive(WorldSession* /*session*/, WorldPacket& packet) override + bool CanPacketReceive(WorldSession* /*session*/, WorldPacket const& packet) override { if (packet.GetOpcode() != CMSG_PLAYER_LOGIN) return true; - auto const oldPos = packet.rpos(); + WorldPacket& pkt = const_cast(packet); + auto const oldPos = pkt.rpos(); ObjectGuid loginGuid; - packet >> loginGuid; - packet.rpos(oldPos); + pkt >> loginGuid; + pkt.rpos(oldPos); if (!loginGuid) return true; From 545b21ec0c60d6ef01f6e81f2921104a49ffc624 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Tue, 17 Mar 2026 06:51:50 +0100 Subject: [PATCH 06/87] PR template fix (#2216) Maintenance PR to remove tasklist visible on github --- PULL_REQUEST_TEMPLATE.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index d9ef2cedba7..825717f83da 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -43,21 +43,21 @@ any impact on performance, you may skip these question. If necessary, a maintain ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) + - - [ ] No, not at all + - - [ ] Minimal impact (**explain below**) + - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [ ] Yes (**explain why**) + - - [ ] No + - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [ ] Yes (**explain below**) + - - [ ] No + - - [ ] Yes (**explain below**) @@ -68,8 +68,8 @@ the message is in a translatable format, and list in the table the message_key a Search for GetBotTextOrDefault in the codebase for examples. --> Does this change add bot messages to translate? -- [ ] No -- [ ] Yes (**list messages in the table**) + - - [ ] No + - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | @@ -82,8 +82,8 @@ AI assistance is allowed, but all submitted code must be fully understood, revie We expect contributors to be honest about what they do and do not understand. --> Was AI assistance used while working on this change? -- [ ] No -- [ ] Yes (**explain below**) + - - [ ] No + - - [ ] Yes (**explain below**) From a473432b8f03ede010f72e1ff4e2a250b7090ced Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:04:54 -0700 Subject: [PATCH 07/87] Core update (#2221) ## Pull Request Description Update based on @TakenBacon feedback. Thank you for the correction to the PR. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Script/PlayerbotsSecureLogin.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Script/PlayerbotsSecureLogin.cpp b/src/Script/PlayerbotsSecureLogin.cpp index efede3ee51e..09810756661 100644 --- a/src/Script/PlayerbotsSecureLogin.cpp +++ b/src/Script/PlayerbotsSecureLogin.cpp @@ -57,11 +57,9 @@ class PlayerbotsSecureLoginServerScript : public ServerScript if (packet.GetOpcode() != CMSG_PLAYER_LOGIN) return true; - WorldPacket& pkt = const_cast(packet); - auto const oldPos = pkt.rpos(); + WorldPacket pkt(packet); ObjectGuid loginGuid; pkt >> loginGuid; - pkt.rpos(oldPos); if (!loginGuid) return true; From 2ce89939860740d1731589ec7b5a43bacfe3be5b Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:37:02 -0700 Subject: [PATCH 08/87] Correct Loot rolling behavior (#2190) # Pull Request This fixes the loot rolling behavior issue created by #2068 . Introduce the ability for enchanter bots to disenchant items they dont need, and roll need on recipes they also need. Make it so ITEM_USAGE_AH ensures the item is not BOP. Try to reduce the call for item_usage in CalculateRollVote by passing usage if available. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? -- Add a new check that downgrades greed rolls to desired levels, or bools for the other two options. - Describe the **cheapest implementation** that produces an acceptable result? -- As implemented. - Describe the **runtime cost** when this logic executes across many bots? -- Same as before. Item usage is the heaviest part, and that hasnt changed to accommodate this. --- ## How to Test the Changes - multiple bots in a group with group loot on, do a dungeon or something. One bot should be an enchanter. ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [x] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [X] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [X] Yes (**explain why**) - - - Corrects the looting behavior to original design. If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [X] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- conf/playerbots.conf.dist | 19 ++++++- src/Ai/Base/Actions/LootRollAction.cpp | 78 +++++++++++--------------- src/Ai/Base/Actions/LootRollAction.h | 2 +- src/Ai/Base/Value/ItemUsageValue.cpp | 3 +- src/PlayerbotAIConfig.cpp | 5 +- src/PlayerbotAIConfig.h | 5 +- 6 files changed, 60 insertions(+), 52 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 0f957262103..52e80e6803a 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -298,9 +298,24 @@ AiPlayerbot.TwoRoundsGearInit = 0 # Default: 0 (disabled) AiPlayerbot.FreeMethodLoot = 0 -# Bots' loot roll level (0 = pass, 1 = greed, 2 = need) +# Bots' Roll level bots will use for items they Need (0 = pass, 1 = greed, 2 = need) # Default: 1 (greed) -AiPlayerbot.LootRollLevel = 1 +AiPlayerbot.LootNeedRollLevel = 1 + +# Enable bots to roll GREED on items (global toggle) +# If disabled, bots will PASS instead of GREED on all items +# Default: 0 (disabled - bots only NEED or PASS) +AiPlayerbot.LootGreedRollLevel = 0 + +# Enable bots to roll on recipes. Will NEED on learnable profession recipes they don't already know +# Bots will roll GREED on BoE recipes they can't learn if LootRollGreed is enabled. +# Default: 0 (disabled) +AiPlayerbot.LootRollRecipe = 0 + +# Bots with enchanting will roll DISENCHANT instead of GREED on disenchantable items +# If disabled, bots will GREED on disenchantable items instead +# Default: 0 (disabled) +AiPlayerbot.LootRollDisenchant = 0 # # diff --git a/src/Ai/Base/Actions/LootRollAction.cpp b/src/Ai/Base/Actions/LootRollAction.cpp index a3bf13041bc..749f47b9d21 100644 --- a/src/Ai/Base/Actions/LootRollAction.cpp +++ b/src/Ai/Base/Actions/LootRollAction.cpp @@ -22,10 +22,10 @@ bool LootRollAction::Execute(Event /*event*/) std::vector rolls = group->GetRolls(); for (Roll*& roll : rolls) { - if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET) - { + auto voteItr = roll->playerVote.find(bot->GetGUID()); + if (voteItr == roll->playerVote.end() || voteItr->second != NOT_EMITED_YET) continue; - } + ObjectGuid guid = roll->itemGUID; uint32 itemId = roll->itemid; int32 randomProperty = 0; @@ -41,27 +41,22 @@ bool LootRollAction::Execute(Event /*event*/) std::string itemUsageParam; if (randomProperty != 0) - { itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } else - { itemUsageParam = std::to_string(itemId); - } + ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); // Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality. if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC) { if (CanBotUseToken(proto, bot)) - { vote = NEED; // Eligible for "Need" - } else - { vote = GREED; // Not eligible, so "Greed" - } } + else if (usage == ITEM_USAGE_DISENCHANT) + vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED; else { switch (proto->Class) @@ -69,40 +64,34 @@ bool LootRollAction::Execute(Event /*event*/) case ITEM_CLASS_WEAPON: case ITEM_CLASS_ARMOR: if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { vote = NEED; - } else if (usage != ITEM_USAGE_NONE) - { vote = GREED; - } + break; + case ITEM_CLASS_RECIPE: + if (!sPlayerbotAIConfig.lootRollRecipe) + vote = PASS; + else if (usage == ITEM_USAGE_SKILL) + vote = NEED; // Bot can learn this recipe + else if (proto->Bonding != BIND_WHEN_PICKED_UP) + vote = GREED; // BoE recipe bot can't learn - GREED for AH/trade break; default: if (StoreLootAction::IsLootAllowed(itemId, botAI)) - vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior + vote = CalculateRollVote(proto, usage); break; } } - if (sPlayerbotAIConfig.lootRollLevel == 0) - { - vote = PASS; - } - else if (sPlayerbotAIConfig.lootRollLevel == 1) + if (vote == NEED) { - // Level 1 = "greed" mode: bots greed on useful items but never need - // Only downgrade NEED to GREED, preserve GREED votes as-is - if (vote == NEED) - { - if (RollUniqueCheck(proto, bot)) - { - vote = PASS; - } - else - { - vote = GREED; - } - } + if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot)) + vote = PASS; + else if (sPlayerbotAIConfig.lootNeedRollLevel == 1) + vote = GREED; } + else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel) + vote = PASS; + switch (group->GetLootMethod()) { case MASTER_LOOT: @@ -120,11 +109,14 @@ bool LootRollAction::Execute(Event /*event*/) return false; } -RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) +RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage) { - std::ostringstream out; - out << proto->ItemId; - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str()); + if (usage == ITEM_USAGE_NONE) + { + std::ostringstream out; + out << proto->ItemId; + usage = AI_VALUE2(ItemUsage, "item usage", out.str()); + } RollVote needVote = PASS; switch (usage) @@ -137,11 +129,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) break; case ITEM_USAGE_SKILL: case ITEM_USAGE_USE: - case ITEM_USAGE_DISENCHANT: case ITEM_USAGE_AH: case ITEM_USAGE_VENDOR: needVote = GREED; break; + case ITEM_USAGE_DISENCHANT: + needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED; + break; default: break; } @@ -195,9 +189,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot) // Check if the bot's class is allowed to use the token if (proto->AllowableClass & botClassMask) - { return true; // Bot's class is eligible to use this token - } return false; // Bot's class cannot use this token } @@ -213,13 +205,9 @@ bool RollUniqueCheck(ItemTemplate const* proto, Player* bot) // Determine if the unique item is already equipped bool isEquipped = (totalItemCount > bagItemCount); if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE)) - { return true; // Unique Item is already equipped - } else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1)) - { return true; // Unique item already in bag, don't roll for it - } return false; // Item is not equipped or in bags, roll for it } diff --git a/src/Ai/Base/Actions/LootRollAction.h b/src/Ai/Base/Actions/LootRollAction.h index 13d8958605b..63d2c4b9d05 100644 --- a/src/Ai/Base/Actions/LootRollAction.h +++ b/src/Ai/Base/Actions/LootRollAction.h @@ -22,7 +22,7 @@ class LootRollAction : public QueryItemUsageAction bool Execute(Event event) override; protected: - RollVote CalculateRollVote(ItemTemplate const* proto); + RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE); }; bool CanBotUseToken(ItemTemplate const* proto, Player* bot); diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 7e5aae87c46..b651af95620 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -153,9 +153,8 @@ ItemUsage ItemUsageValue::Calculate() // Need to add something like free bagspace or item value. if (proto->SellPrice > 0) { - if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) + if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP) return ITEM_USAGE_AH; - else return ITEM_USAGE_VENDOR; } diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 45551ab1a5f..fb0ffad4d68 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -620,7 +620,10 @@ bool PlayerbotAIConfig::Initialize() // SPP automation freeMethodLoot = sConfigMgr->GetOption("AiPlayerbot.FreeMethodLoot", false); - lootRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootRollLevel", 1); + lootNeedRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootNeedRollLevel", 1); + lootRollRecipe = sConfigMgr->GetOption("AiPlayerbot.LootRollRecipe", false); + lootRollDisenchant = sConfigMgr->GetOption("AiPlayerbot.LootRollDisenchant", false); + lootGreedRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootGreedRollLevel", false); autoPickReward = sConfigMgr->GetOption("AiPlayerbot.AutoPickReward", "yes"); autoEquipUpgradeLoot = sConfigMgr->GetOption("AiPlayerbot.AutoEquipUpgradeLoot", true); equipUpgradeThreshold = sConfigMgr->GetOption("AiPlayerbot.EquipUpgradeThreshold", 1.1f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index efde98e10fd..7b9b2fe8112 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -346,7 +346,10 @@ class PlayerbotAIConfig uint32 botActiveAloneSmartScaleWhenMaxLevel; bool freeMethodLoot; - int32 lootRollLevel; + int32 lootNeedRollLevel; + bool lootGreedRollLevel; + bool lootRollRecipe; + bool lootRollDisenchant; std::string autoPickReward; bool autoEquipUpgradeLoot; float equipUpgradeThreshold; From 473b2ab5c6f42c5450361f119a08a9642e6a68d4 Mon Sep 17 00:00:00 2001 From: XYUU Date: Fri, 20 Mar 2026 20:37:44 +0100 Subject: [PATCH 09/87] Fix: WLK shaman totem quest vs relic totems: avoid keeping 4 totem items when relic exists #2119 (#2197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Detects shaman relics (relic type, totem subclass) in bags/equipment. * Skips adding the four classic totem items (5175–5178) when a relic exists. * Cleans up any existing totem items from bags/equipment/bank when a relic exists, while keeping Ankh handling intact. ## Test plan Verified manually (local environment). --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar Co-authored-by: github-actions --- src/Bot/Factory/PlayerbotFactory.cpp | 32 ++++++++++++++++++++++------ src/Mgr/Item/ItemVisitors.h | 23 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 9e1b30a7342..9bf987fef04 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -3342,18 +3342,36 @@ void PlayerbotFactory::InitReagents() items.push_back({44615, 40}); // Devout Candle break; case CLASS_SHAMAN: - if (level >= 4) - items.push_back({5175, 1}); // Earth Totem - if (level >= 10) - items.push_back({5176, 1}); // Flame Totem - if (level >= 20) - items.push_back({5177, 1}); // Water Totem + { + HasRelicBySubclassVisitor relicVisitor(ITEM_SUBCLASS_ARMOR_TOTEM); + IterateItems(&relicVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP)); + bool hasRelic = relicVisitor.found; + + if (!hasRelic) + { + if (level >= 4) + items.push_back({5175, 1}); // Earth Totem + if (level >= 10) + items.push_back({5176, 1}); // Flame Totem + if (level >= 20) + items.push_back({5177, 1}); // Water Totem + } + else + { + ItemIds totemIds = {5175, 5176, 5177, 5178}; + FindItemByIdsVisitor totemVisitor(totemIds); + IterateItems(&totemVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP | ITERATE_ITEMS_IN_BANK)); + for (Item* item : totemVisitor.GetResult()) + bot->DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + } if (level >= 30) { - items.push_back({5178, 1}); // Air Totem + if (!hasRelic) + items.push_back({5178, 1}); // Air Totem items.push_back({17030, 20}); // Ankh } break; + } case CLASS_WARLOCK: items.push_back({6265, 5}); // Soul Shard break; diff --git a/src/Mgr/Item/ItemVisitors.h b/src/Mgr/Item/ItemVisitors.h index e42b14f89e7..930aa1f4a3d 100644 --- a/src/Mgr/Item/ItemVisitors.h +++ b/src/Mgr/Item/ItemVisitors.h @@ -66,6 +66,29 @@ class FindUsableItemVisitor : public FindItemVisitor Player* bot; }; +class HasRelicBySubclassVisitor : public IterateItemsVisitor +{ +public: + HasRelicBySubclassVisitor(uint32 subClass) : subClass(subClass) {} + + bool Visit(Item* item) override + { + ItemTemplate const* proto = item->GetTemplate(); + if (proto && proto->InventoryType == INVTYPE_RELIC && proto->SubClass == subClass) + { + found = true; + return false; + } + + return true; + } + + bool found = false; + +private: + uint32 subClass; +}; + class FindItemsByQualityVisitor : public IterateItemsVisitor { public: From 35a0282ca677bcb58c36e480fd31fa544764ece7 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 20 Mar 2026 14:38:06 -0500 Subject: [PATCH 10/87] Add Sense Undead for Paladins (#2200) # Pull Request This PR adds the sense undead ability for Paladins, which they will keep active at all times. This is mildly useful because the associated minor glyph provides a 1% damage increase against undead while the ability is active. Sense undead is also added to InitClassSpells(). I understand that it is a trainer spell so would normally be covered by InitAvailableSpells(), but those playing with mod-individual-progression will not receive the spell through InitAvailableSpells() because it is removed from trainers by the mod (in TBC, a quest was required to obtain the spell). Finally, the minor glyph of sense undead is now added to the config as a default glyph for all PvE specs. It is not added for PvP specs because Forsaken do not count as undead so the glyph is useless in PvP. I also made some other tweaks to Paladin default minor glyphs that are not worth spending any time talking about. Edit: I also did some minor reformatting of code and replaced some numbers with existing constants. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? The implementation just checks if a Paladin has the sense undead aura, and if not, the Paladin will activate sense undead. It is simple and cheap. --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [ ] No - - [x] Yes (**describe and justify impact**) Infinitesimally Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Paladin bots will by default have sense undead enabled. There is no disadvantage to this. If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 12 ++--- .../Class/Paladin/Action/PaladinActions.cpp | 5 +- src/Ai/Class/Paladin/Action/PaladinActions.h | 40 +++++++-------- .../Class/Paladin/PaladinAiObjectContext.cpp | 4 ++ .../GenericPaladinNonCombatStrategy.cpp | 17 ++++--- .../Class/Paladin/Trigger/PaladinTriggers.cpp | 5 ++ .../Class/Paladin/Trigger/PaladinTriggers.h | 50 +++++++++---------- src/Bot/Factory/PlayerbotFactory.cpp | 18 +------ 8 files changed, 71 insertions(+), 80 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 52e80e6803a..c1c06b8b717 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1406,28 +1406,28 @@ AiPlayerbot.PremadeSpecLink.1.5.80 = 0502300123-3-250031220223012521332113321 # AiPlayerbot.PremadeSpecName.2.0 = holy pve -AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43369,43365,41109 +AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43368,43365,41109 AiPlayerbot.PremadeSpecLink.2.0.60 = 50350151020013053100515221 AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312 AiPlayerbot.PremadeSpecName.2.1 = prot pve -AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43369,43365,45745 +AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43368,43369,45745 AiPlayerbot.PremadeSpecLink.2.1.60 = -05005135203102311333112321 AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003 AiPlayerbot.PremadeSpecName.2.2 = ret pve -AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43369,43365,43869 +AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43368,43369,43869 AiPlayerbot.PremadeSpecLink.2.2.60 = --05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331 AiPlayerbot.PremadeSpecName.2.3 = holy pvp -AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43366,43365,45747 +AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43369,43365,45747 AiPlayerbot.PremadeSpecLink.2.3.60 = 50332150300013050133215221 AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122 AiPlayerbot.PremadeSpecName.2.4 = prot pvp -AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43369,41101,43368,43365,45745 +AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43367,41101,43369,43365,45745 AiPlayerbot.PremadeSpecLink.2.4.60 = -15320130223122311323311321 AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502 AiPlayerbot.PremadeSpecName.2.5 = ret pvp -AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43369,41102,43368,43365,45747 +AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43367,41102,43369,43365,45747 AiPlayerbot.PremadeSpecLink.2.5.60 = --05230250203331222133201321 AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321 diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index d0bfbabbf3c..c1521bb1e1a 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -472,9 +472,8 @@ Unit* CastRighteousDefenseAction::GetTarget() { Unit* current_target = AI_VALUE(Unit*, "current target"); if (!current_target) - { - return NULL; - } + return nullptr; + return current_target->GetVictim(); } diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.h b/src/Ai/Class/Paladin/Action/PaladinActions.h index 3bacc88460b..c58c3209d65 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/src/Ai/Class/Paladin/Action/PaladinActions.h @@ -91,9 +91,8 @@ class CastBlessingOfMightAction : public CastBuffSpellAction class CastBlessingOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) : BuffOnPartyAction(botAI, name), name(name) - { - } + CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) + : BuffOnPartyAction(botAI, name), name(name) {} Value* GetTargetValue() override; @@ -154,9 +153,7 @@ class CastBlessingOfSanctuaryAction : public CastBuffSpellAction class CastBlessingOfSanctuaryOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") - { - } + CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {} std::string const getName() override { return "blessing of sanctuary on party"; } Value* GetTargetValue() override; @@ -173,18 +170,14 @@ class CastHolyShockOnPartyAction : public HealPartyMemberAction { public: CastHolyShockOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) - { - } + : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) {} }; class CastHolyLightOnPartyAction : public HealPartyMemberAction { public: CastHolyLightOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) - { - } + : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) {} }; class CastFlashOfLightAction : public CastHealingSpellAction @@ -197,9 +190,7 @@ class CastFlashOfLightOnPartyAction : public HealPartyMemberAction { public: CastFlashOfLightOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) - { - } + : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) {} }; class CastLayOnHandsAction : public CastHealingSpellAction @@ -357,9 +348,7 @@ class CastHammerOfJusticeOnEnemyHealerAction : public CastSpellOnEnemyHealerActi { public: CastHammerOfJusticeOnEnemyHealerAction(PlayerbotAI* botAI) - : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") - { - } + : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") {} }; class CastHammerOfJusticeSnareAction : public CastSnareSpellAction @@ -368,6 +357,12 @@ class CastHammerOfJusticeSnareAction : public CastSnareSpellAction CastHammerOfJusticeSnareAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "hammer of justice") {} }; +class CastSenseUndeadAction : public CastBuffSpellAction +{ +public: + CastSenseUndeadAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "sense undead") {} +}; + class CastTurnUndeadAction : public CastBuffSpellAction { public: @@ -381,25 +376,25 @@ PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection"); class CastDivinePleaAction : public CastBuffSpellAction { public: - CastDivinePleaAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "divine plea") {} + CastDivinePleaAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "divine plea") {} }; class ShieldOfRighteousnessAction : public CastMeleeSpellAction { public: - ShieldOfRighteousnessAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "shield of righteousness") {} + ShieldOfRighteousnessAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shield of righteousness") {} }; class CastBeaconOfLightOnMainTankAction : public BuffOnMainTankAction { public: - CastBeaconOfLightOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "beacon of light", true) {} + CastBeaconOfLightOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "beacon of light", true) {} }; class CastSacredShieldOnMainTankAction : public BuffOnMainTankAction { public: - CastSacredShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "sacred shield", false) {} + CastSacredShieldOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "sacred shield", false) {} }; class CastAvengingWrathAction : public CastBuffSpellAction @@ -428,4 +423,5 @@ class CastCancelDivineSacrificeAction : public Action bool Execute(Event event) override; bool isUseful() override; }; + #endif diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 45f676ec9f5..ed8f4931b38 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -132,6 +132,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext &PaladinTriggerFactoryInternal::hammer_of_justice_on_enemy_target; creators["hammer of justice on snare target"] = &PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target; + creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead; creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor; creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead; creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield; @@ -151,6 +152,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext } private: + static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); } static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); } static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); } static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); } @@ -288,6 +290,7 @@ class PaladinAiObjectContextInternal : public NamedObjectContext creators["hammer of justice on snare target"] = &PaladinAiObjectContextInternal::hammer_of_justice_on_snare_target; creators["divine favor"] = &PaladinAiObjectContextInternal::divine_favor; + creators["sense undead"] = &PaladinAiObjectContextInternal::sense_undead; creators["turn undead"] = &PaladinAiObjectContextInternal::turn_undead; creators["blessing of protection on party"] = &PaladinAiObjectContextInternal::blessing_of_protection_on_party; creators["righteous defense"] = &PaladinAiObjectContextInternal::righteous_defense; @@ -312,6 +315,7 @@ class PaladinAiObjectContextInternal : public NamedObjectContext { return new CastBlessingOfProtectionProtectAction(botAI); } + static Action* sense_undead(PlayerbotAI* botAI) { return new CastSenseUndeadAction(botAI); } static Action* turn_undead(PlayerbotAI* botAI) { return new CastTurnUndeadAction(botAI); } static Action* divine_favor(PlayerbotAI* botAI) { return new CastDivineFavorAction(botAI); } static Action* righteous_fury(PlayerbotAI* botAI) { return new CastRighteousFuryAction(botAI); } diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp index 7f9919e06b9..670d7e629a3 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp @@ -19,14 +19,15 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector& tr NonCombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) })); - triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", 25.0f) })); - triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", 27.0f) })); - triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", 28.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 5.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 6.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 7.0f) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 8.0f) })); + triggers.push_back(new TriggerNode("not sensing undead", { NextAction("sense undead", ACTION_IDLE + 1.0f) })); int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot()); - if (specTab == 0 || specTab == 1) // Holy or Protection - triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); - if (specTab == 2) // Retribution - triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + if (specTab == PALADIN_TAB_HOLY || specTab == PALADIN_TAB_PROTECTION) + triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) })); + if (specTab == PALADIN_TAB_RETRIBUTION) + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) })); } diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index 6c333bc7eeb..71328c4dcf7 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -30,3 +30,8 @@ bool BlessingTrigger::IsActive() return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", "blessing of kings", "blessing of sanctuary", nullptr); } + +bool NotSensingUndeadTrigger::IsActive() +{ + return !botAI->HasAura("sense undead", bot); +} diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index 7352dbc8125..f33b66890ad 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -77,9 +77,7 @@ class BlessingOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingTrigger : public BuffTrigger @@ -93,7 +91,8 @@ class BlessingTrigger : public BuffTrigger class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger { public: - HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "hammer of justice") {} + HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) + : InterruptSpellTrigger(botAI, "hammer of justice") {} }; class HammerOfJusticeSnareTrigger : public SnareTargetTrigger @@ -144,9 +143,7 @@ class CleanseCurePartyMemberDiseaseTrigger : public PartyMemberNeedCureTrigger { public: CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) {} }; class CleanseCurePoisonTrigger : public NeedCureTrigger @@ -159,9 +156,7 @@ class CleanseCurePartyMemberPoisonTrigger : public PartyMemberNeedCureTrigger { public: CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) {} }; class CleanseCureMagicTrigger : public NeedCureTrigger @@ -173,15 +168,15 @@ class CleanseCureMagicTrigger : public NeedCureTrigger class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger { public: - CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) - { - } + CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) {} }; class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger { public: - HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} + HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) + : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} }; class DivineFavorTrigger : public BuffTrigger @@ -190,6 +185,14 @@ class DivineFavorTrigger : public BuffTrigger DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} }; +class NotSensingUndeadTrigger : public BuffTrigger +{ +public: + NotSensingUndeadTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "not sensing undead") {} + + bool IsActive() override; +}; + class TurnUndeadTrigger : public HasCcTargetTrigger { public: @@ -201,7 +204,8 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield"); class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger { public: - BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "beacon of light", true) {} + BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) + : BuffOnMainTankTrigger(ai, "beacon of light", true) {} }; class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger @@ -213,34 +217,29 @@ class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger { public: - BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} + BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} }; class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} }; class AvengingWrathTrigger : public BoostTrigger @@ -248,4 +247,5 @@ class AvengingWrathTrigger : public BoostTrigger public: AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} }; + #endif diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 9bf987fef04..c0e6d80dbb6 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -2553,17 +2553,15 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(7386, false); // Sunder Armor } if (level >= 30) - { bot->learnSpell(2458, false); // Berserker Stance - } break; case CLASS_PALADIN: bot->learnSpell(21084, true); bot->learnSpell(635, true); if (level >= 12) - { bot->learnSpell(7328, false); // Redemption - } + if (level >= 20) + bot->learnSpell(5502, false); // Sense Undead break; case CLASS_ROGUE: bot->learnSpell(1752, true); @@ -2605,17 +2603,11 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(686, true); bot->learnSpell(688, false); // summon imp if (level >= 10) - { bot->learnSpell(697, false); // summon voidwalker - } if (level >= 20) - { bot->learnSpell(712, false); // summon succubus - } if (level >= 30) - { bot->learnSpell(691, false); // summon felhunter - } break; case CLASS_DRUID: bot->learnSpell(5176, true); @@ -2632,17 +2624,11 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(331, true); // bot->learnSpell(66747, true); // Totem of the Earthen Ring if (level >= 4) - { bot->learnSpell(8071, false); // stoneskin totem - } if (level >= 10) - { bot->learnSpell(3599, false); // searing totem - } if (level >= 20) - { bot->learnSpell(5394, false); // healing stream totem - } break; default: break; From 4c0cb30f0b092275d4b4ad1f93f662d953133241 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:38:27 -0700 Subject: [PATCH 11/87] New readme (#2202) # Pull Request Add references to wiki with me detailed installation information, and troubleshooting page based on frequently observed issues in support. Also included are https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting --------- Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- README.md | 50 +++++++++++--------------------------------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index c1c179eef02..1b5205168da 100644 --- a/README.md +++ b/README.md @@ -32,25 +32,11 @@ We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can ## Installation -Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. +Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. -**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below. +> **Important:** All `mod-playerbots` installations require a custom fork of AzerothCore: [mod-playerbots/azerothcore-wotlk (Playerbot branch)](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). The standard AzerothCore repository will **not** work. -### Cloning the Repositories - -To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following: - -```bash -git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot -cd azerothcore-wotlk/modules -git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master -``` - -For more information, refer to the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. - -### Docker Installation - -Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module: +### Quick Start ```bash git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot @@ -58,32 +44,18 @@ cd azerothcore-wotlk/modules git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master ``` -Afterwards, create a `docker-compose.override.yml` file in the `azerothcore-wotlk` directory. This override file allows for mounting the modules directory to the `ac-worldserver` service which is required for it to run. Put the following inside and save: +Then build the server following the platform-specific instructions in our **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)**. -```yml -services: - ac-worldserver: - volumes: - - ./modules:/azerothcore/modules:ro -``` - -Additionally, this override file can be used to set custom configuration settings for `ac-worldserver` and any modules you install as environment variables: - -```yml -services: - ac-worldserver: - environment: - AC_RATE_XP_KILL: "1" - AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "1" - volumes: - - ./modules:/azerothcore/modules:ro -``` +> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues. -For example, to double the experience gain rate per kill, take the setting `Rate.XP.Kill = 1` from [woldserver.conf](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/src/server/apps/worldserver/worldserver.conf.dist), convert it to an environment variable, and change it to the desired setting in the override file to get `AC_RATE_XP_KILL: "2"`. If you wanted to disable random bots from logging in automatically, take the `AiPlayerbot.RandomBotAutologin = 1` setting from [playerbots.conf](https://github.com/mod-playerbots/mod-playerbots/blob/master/conf/playerbots.conf.dist) and do the same to get `AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "0"`. For more information on how to configure Azerothcore, Playerbots, and other module settings as environment variables in Docker Compose, see the "Configuring AzerothCore in Containers" section in the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) guide. +### Detailed Guides -Before building, consider setting the database password. One way to do this is to create a `.env` file in the root `azerothcore-wotlk` directory using the [template](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/conf/dist/env.docker). This file also allows you to set the user and group Docker uses for the services in case you run into any permissions issues, which are the most common cause for Docker installation problems. +| Guide | Description | +|---|---| +| **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)** | Full step-by-step instructions for clean installs, migrating from existing AzerothCore, Docker setup, adding modules, and updating | +| **[Troubleshooting](https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting)** | Solutions to the most common build errors, database issues, configuration mistakes, crashes, and platform-specific problems | -Use `docker compose up -d --build` to build and run the server. For more information, including how to create an account and taking backups, refer to the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) page. +For additional references, see the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. ## Documentation From 4877dcc5731aa302ae149bd42d38552bfc49b1c3 Mon Sep 17 00:00:00 2001 From: Aldori Date: Fri, 20 Mar 2026 15:38:57 -0400 Subject: [PATCH 12/87] fix: ByteBufferException error (opcode: 149) (#2206) Fixes #2204 ## Pull Request Description Fixes an opcode 149 ByteBufferException when Questie-335 (or other addons that send addon messages) is used in a party with Playerbots. The issue was caused by addon-language packets reaching parsing logic they should not have reached. This change adjusts the early return for `LANG_ADDON` packets before further handling. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Moved the early return for `LANG_ADDON` packets in the outgoing packet handler. - Describe the **processing cost** when this logic executes across many bots. - Negligible. It's a simple conditional check with an early return. ## How to Test the Changes 1. Install and enable Questie-335. 2. Invite at least 1 Playerbot to a party. 3. Accept a quest, abandon a quest, or progress a quest objective such as kill credit or looting a quest item. 4. Verify the worldserver no longer logs opcode 149 ByteBufferException errors. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers This is a small fix intended only to prevent addon language packets from reaching incompatible packet parsing logic. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/PlayerbotAI.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index c0e679f3eaa..6c89400b1fb 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1119,6 +1119,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE) return; + if (lang == LANG_ADDON) + return; + if (p.GetOpcode() == SMSG_GM_MESSAGECHAT) { p >> textLen; @@ -1168,8 +1171,6 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) return; - if (lang == LANG_ADDON) - return; if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || From 5c63aacd6050b7e1aa276290429cc6b89b595fb5 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:39:24 -0600 Subject: [PATCH 13/87] Drop server initialization time message. Show number of bots set to login. (#2209) ## Pull Request Description The server initialization time on login is neither relevant to the typical user, nor is it accurate. It simply takes `maxRandomBots`, does some arbitrary multiplications and divisions on it, and declare that as the time it takes the server to load. It does not take into account any other of your server configurations nor your server capabilities. Here we exchange that message with one more relevant to the user, telling them the number of logged in bot (or set to be logged in with DisabledWithoutRealPlayer enabled). But honestly, even removing that whole snippet is a better idea than keeping the misleading message. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. Alternatively the whole snippet can be removed. ## How to Test the Changes Login and see the welcome messages. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Script/Playerbots.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp index af28027c93f..7db91eb40ae 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -112,13 +112,10 @@ class PlayerbotsPlayerScript : public PlayerScript if (sPlayerbotAIConfig.enabled || sPlayerbotAIConfig.randomBotAutologin) { - std::string roundedTime = - std::to_string(std::ceil((sPlayerbotAIConfig.maxRandomBots * 0.11 / 60) * 10) / 10.0); - roundedTime = roundedTime.substr(0, roundedTime.find('.') + 2); + std::string maxAllowedBotCount = std::to_string(sRandomPlayerbotMgr.GetMaxAllowedBotCount()); ChatHandler(player->GetSession()).SendSysMessage( - "|cff00ff00Playerbots:|r bot initialization at server startup takes about '" - + roundedTime + "' minutes."); + "|cff00ff00Playerbots:|r The server is configured with " + maxAllowedBotCount + " bots."); } } } From 957eca0263232001c02476ea2b61576a7dceed9f Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:39:53 -0700 Subject: [PATCH 14/87] Feat. Enable multi node flying, and refactor into travel manager (#2156) # Pull Request Feature - Enable multi node flying for bots - Bots currently only do node to node flying. This PR makes it so they can connect multiple noted. -- This is enabled by sending a vector containing the node sequence instead of a single destination node -- To minimize the run-time cost of searching for available nodes and connection, a cache of all possible connections is prepared at start up using a BFS search algorithm. Refactor - Move all world destination logic (cities, banks, inns) to existing Travel manager - Eliminate flightmastercache and integrate to new manager - replace SQLs calls with in-memory data search by core - Add in new map that stores creature areas by template. Clean up - Move other rpg files to related folder. (Next steps) The selection for where bots fly to should be smarter than it is. Instead of trying to determine where a bot can go, it should first decide where it should go, and then identify the correct way to get there. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x[ No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) The call itself is fairly infrequent, and although now there are a greater number of paths available for the bots, I dont think it would be significant. ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Gemini first suggested the use of a BFS algorithm. This was rewritten by me to actually work as intended. Verification by additional logging not present in final code. Claude code converted the SQL filtering to the atrocious if statements found in PrepareDestinationCache, but after verifying them it works. If there are better ways to do this Im open to it. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- src/Ai/World/Rpg/Action/NewRpgAction.cpp | 5 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 82 +-- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 2 +- .../Rpg/Action}/RpgAction.cpp | 0 .../Actions => World/Rpg/Action}/RpgAction.h | 0 .../Rpg/Action}/RpgSubActions.cpp | 0 .../Rpg/Action}/RpgSubActions.h | 0 src/Ai/World/Rpg/NewRpgInfo.cpp | 9 +- src/Ai/World/Rpg/NewRpgInfo.h | 5 +- .../{Base/Actions => World/Rpg}/RpgValues.h | 0 .../Rpg}/Strategy/RpgStrategy.cpp | 0 .../Rpg}/Strategy/RpgStrategy.h | 0 src/Bot/PlayerbotAI.cpp | 2 +- src/Bot/PlayerbotAI.h | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 489 +---------------- src/Bot/RandomPlayerbotMgr.h | 17 - src/Db/FlightMasterCache.cpp | 39 -- src/Db/FlightMasterCache.h | 36 -- src/Mgr/Travel/TravelMgr.cpp | 500 ++++++++++++++++++ src/Mgr/Travel/TravelMgr.h | 43 ++ src/Mgr/Travel/TravelNode.cpp | 125 +++++ src/Mgr/Travel/TravelNode.h | 14 + src/PlayerbotAIConfig.cpp | 2 + 23 files changed, 721 insertions(+), 651 deletions(-) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.cpp (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.h (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.cpp (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.h (100%) rename src/Ai/{Base/Actions => World/Rpg}/RpgValues.h (100%) rename src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.cpp (100%) rename src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.h (100%) delete mode 100644 src/Db/FlightMasterCache.cpp delete mode 100644 src/Db/FlightMasterCache.h diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 6820c64600d..58846b949aa 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -231,7 +231,6 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/) return false; auto& data = *dataPtr; uint32 questId = data.questId; - const Quest* quest = data.quest; uint8 questStatus = bot->GetQuestStatus(questId); switch (questStatus) { @@ -438,7 +437,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) return MoveFarTo(flightMaster); - std::vector nodes = {data.fromNode, data.toNode}; + std::vector nodes = data.path; botAI->RemoveShapeshift(); if (bot->IsMounted()) @@ -447,7 +446,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0)) { LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), - flightMaster->GetEntry(), nodes[0], nodes[1]); + flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]); botAI->rpgInfo.ChangeToIdle(); } return true; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 75392418b30..b5156d6c153 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -3,7 +3,6 @@ #include "BroadcastHelper.h" #include "ChatHelper.h" #include "Creature.h" -#include "FlightMasterCache.h" #include "G3D/Vector2.h" #include "GameObject.h" #include "GossipDef.h" @@ -856,7 +855,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) { - const std::vector& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()]; + const std::vector& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel()); float hiRange = 500.0f; float loRange = 2500.0f; if (bot->GetLevel() < 5) @@ -914,9 +913,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) { - const std::vector& locs = IsAlliance(bot->getRace()) - ? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()] - : sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()]; + const std::vector locs = sTravelMgr.GetTravelHubs(bot); bool inCity = false; @@ -957,70 +954,19 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) return dest; } -bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode) +bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path) { - Creature* nearestFlightMaster = FlightMasterCache::Instance().GetNearestFlightMaster(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot); + if (!flightMaster) return false; - fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), - nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), - bot->GetTeamId()); - - if (!fromNode) - return false; - - std::vector availableToNodes; - for (uint32 i = 1; i < sTaxiNodesStore.GetNumRows(); ++i) - { - if (fromNode == i) - continue; - - TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(i); - - // check map - if (!node || node->map_id != bot->GetMapId() || - (!node->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0])) // dk flight - continue; - - // check taxi node known - if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(i)) - continue; - - // check distance by level - if (!botAI->CheckLocationDistanceByLevel(bot, WorldLocation(node->map_id, node->x, node->y, node->z), false)) - continue; - - // check path - uint32 path, cost; - sObjectMgr->GetTaxiPath(fromNode, i, path, cost); - if (!path) - continue; - - // check area level - uint32 nodeZoneId = bot->GetMap()->GetZoneId(bot->GetPhaseMask(), node->x, node->y, node->z); - bool capital = false; - if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(nodeZoneId)) - { - capital = zone->flags & AREA_FLAG_CAPITAL; - } - - auto itr = sRandomPlayerbotMgr.zone2LevelBracket.find(nodeZoneId); - if (!capital && itr == sRandomPlayerbotMgr.zone2LevelBracket.end()) - continue; - - if (!capital && (bot->GetLevel() < itr->second.low || bot->GetLevel() > itr->second.high)) - continue; - - availableToNodes.push_back(i); - } - if (availableToNodes.empty()) + std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot); + if (availablePaths.empty()) return false; - flightMaster = nearestFlightMaster->GetGUID(); - toNode = availableToNodes[urand(0, availableToNodes.size() - 1)]; + path = availablePaths[urand(0, availablePaths.size() - 1)]; LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", - bot->GetName(), flightMaster.GetEntry(), fromNode, toNode, availableToNodes.size()); + bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size()); return true; } @@ -1121,10 +1067,10 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta case RPG_TRAVEL_FLIGHT: { ObjectGuid flightMaster; - uint32 fromNode, toNode; - if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode)) + std::vector path; + if (SelectRandomFlightTaxiNode(flightMaster, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode); + botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path); return true; } return false; @@ -1197,8 +1143,8 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) case RPG_TRAVEL_FLIGHT: { ObjectGuid flightMaster; - uint32 fromNode, toNode; - return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode); + std::vector path; + return SelectRandomFlightTaxiNode(flightMaster, path); } default: return false; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 910e5f49428..9cd939eb772 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -54,7 +54,7 @@ class NewRpgBaseAction : public MovementAction bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); - bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode); + bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path); bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); diff --git a/src/Ai/Base/Actions/RpgAction.cpp b/src/Ai/World/Rpg/Action/RpgAction.cpp similarity index 100% rename from src/Ai/Base/Actions/RpgAction.cpp rename to src/Ai/World/Rpg/Action/RpgAction.cpp diff --git a/src/Ai/Base/Actions/RpgAction.h b/src/Ai/World/Rpg/Action/RpgAction.h similarity index 100% rename from src/Ai/Base/Actions/RpgAction.h rename to src/Ai/World/Rpg/Action/RpgAction.h diff --git a/src/Ai/Base/Actions/RpgSubActions.cpp b/src/Ai/World/Rpg/Action/RpgSubActions.cpp similarity index 100% rename from src/Ai/Base/Actions/RpgSubActions.cpp rename to src/Ai/World/Rpg/Action/RpgSubActions.cpp diff --git a/src/Ai/Base/Actions/RpgSubActions.h b/src/Ai/World/Rpg/Action/RpgSubActions.h similarity index 100% rename from src/Ai/Base/Actions/RpgSubActions.h rename to src/Ai/World/Rpg/Action/RpgSubActions.h diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 20db8b049b5..4e04ab08674 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -37,13 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) data = do_quest; } -void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode) +void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path) { startT = getMSTime(); TravelFlight flight; flight.fromFlightMaster = fromFlightMaster; - flight.fromNode = fromNode; - flight.toNode = toNode; + flight.path = std::move(path); flight.inFlight = false; data = flight; } @@ -150,8 +149,8 @@ std::string NewRpgInfo::ToString() { out << "TRAVEL_FLIGHT"; out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); - out << "\nfromNode: " << arg.fromNode; - out << "\ntoNode: " << arg.toNode; + out << "\nfromNode: " << arg.path[0]; + out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; } else diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 5b6ae3cb95e..c2349c14b2f 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -50,8 +50,7 @@ struct NewRpgInfo struct TravelFlight { ObjectGuid fromFlightMaster{}; - uint32 fromNode{0}; - uint32 toNode{0}; + std::vector path; bool inFlight{false}; }; // RPG_REST @@ -91,7 +90,7 @@ struct NewRpgInfo void ChangeToWanderNpc(); void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); - void ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode); + void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); void ChangeToRest(); void ChangeToIdle(); bool CanChangeTo(NewRpgStatus status); diff --git a/src/Ai/Base/Actions/RpgValues.h b/src/Ai/World/Rpg/RpgValues.h similarity index 100% rename from src/Ai/Base/Actions/RpgValues.h rename to src/Ai/World/Rpg/RpgValues.h diff --git a/src/Ai/Base/Strategy/RpgStrategy.cpp b/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp similarity index 100% rename from src/Ai/Base/Strategy/RpgStrategy.cpp rename to src/Ai/World/Rpg/Strategy/RpgStrategy.cpp diff --git a/src/Ai/Base/Strategy/RpgStrategy.h b/src/Ai/World/Rpg/Strategy/RpgStrategy.h similarity index 100% rename from src/Ai/Base/Strategy/RpgStrategy.h rename to src/Ai/World/Rpg/Strategy/RpgStrategy.h diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 6c89400b1fb..800247c6fcb 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -6487,7 +6487,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st return ChatChannelSource::SRC_UNDEFINED; } -bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp) +bool PlayerbotAI::StarterLevelDistanceCheck(Player* player, const WorldLocation& loc, bool fromStartUp) { if (player->GetLevel() > 16) return true; diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index b3a51b15c05..5d64bf15946 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -556,7 +556,7 @@ class PlayerbotAI : public PlayerbotAIBase bool IsSafe(WorldObject* obj); ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName); - bool CheckLocationDistanceByLevel(Player* player, const WorldLocation &loc, bool fromStartUp = false); + bool StarterLevelDistanceCheck(Player* player, const WorldLocation &loc, bool fromStartUp = false); bool HasCheat(BotCheatMask mask) { diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index bc1ffaad1ca..3fea369a52b 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -23,7 +23,6 @@ #include "DatabaseEnv.h" #include "Define.h" #include "FleeManager.h" -#include "FlightMasterCache.h" #include "GridNotifiers.h" #include "LFGMgr.h" #include "MapMgr.h" @@ -47,9 +46,7 @@ #include "World.h" #include "Cell.h" #include "GridNotifiers.h" -// Required for Cell because of poor AC implementation #include "CellImpl.h" -// Required for GridNotifiers because of poor AC implementation #include "GridNotifiersImpl.h" struct GuidClassRaceInfo @@ -59,48 +56,6 @@ struct GuidClassRaceInfo uint32 rRace; }; -enum class CityId : uint8 { - STORMWIND, IRONFORGE, DARNASSUS, EXODAR, - ORGRIMMAR, UNDERCITY, THUNDER_BLUFF, SILVERMOON_CITY, - SHATTRATH_CITY, DALARAN -}; - -enum class FactionId : uint8 { ALLIANCE, HORDE, NEUTRAL }; - -// Map of banker entry → city + faction -static const std::unordered_map> bankerToCity = { - {2455, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2456, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2457, {CityId::STORMWIND, FactionId::ALLIANCE}}, - {2460, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {2461, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {5099, {CityId::IRONFORGE, FactionId::ALLIANCE}}, - {4155, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4208, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4209, {CityId::DARNASSUS, FactionId::ALLIANCE}}, - {17773, {CityId::EXODAR, FactionId::ALLIANCE}}, {18350, {CityId::EXODAR, FactionId::ALLIANCE}}, {16710, {CityId::EXODAR, FactionId::ALLIANCE}}, - {3320, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3309, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3318, {CityId::ORGRIMMAR, FactionId::HORDE}}, - {4549, {CityId::UNDERCITY, FactionId::HORDE}}, {2459, {CityId::UNDERCITY, FactionId::HORDE}}, {2458, {CityId::UNDERCITY, FactionId::HORDE}}, {4550, {CityId::UNDERCITY, FactionId::HORDE}}, - {2996, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8356, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8357, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, - {17631, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17632, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17633, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {16615, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16616, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16617, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {19246, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {19034, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {30604, {CityId::DALARAN, FactionId::NEUTRAL}}, {30605, {CityId::DALARAN, FactionId::NEUTRAL}}, {30607, {CityId::DALARAN, FactionId::NEUTRAL}}, - {28675, {CityId::DALARAN, FactionId::NEUTRAL}}, {28676, {CityId::DALARAN, FactionId::NEUTRAL}}, {28677, {CityId::DALARAN, FactionId::NEUTRAL}} -}; - -// Map of city → available banker entries -static const std::unordered_map> cityToBankers = { - {CityId::STORMWIND, {2455, 2456, 2457}}, - {CityId::IRONFORGE, {2460, 2461, 5099}}, - {CityId::DARNASSUS, {4155, 4208, 4209}}, - {CityId::EXODAR, {17773, 18350, 16710}}, - {CityId::ORGRIMMAR, {3320, 3309, 3318}}, - {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, - {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, - {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, - {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, - {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} -}; - -// Quick lookup map: banker entry → location -static std::unordered_map bankerEntryToLocation; - void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); } void activatePrintStatsThread() @@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& z = 0.05f + ground; - if (!botAI->CheckLocationDistanceByLevel(bot, loc, true)) + if (!botAI->StarterLevelDistanceCheck(bot, loc, true)) continue; const LocaleConstant& locale = sWorld->GetDefaultDbcLocale(); @@ -1762,329 +1717,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& // tlocs.size()); } -void RandomPlayerbotMgr::PrepareZone2LevelBracket() -{ - // Classic WoW - Low - level zones - zone2LevelBracket[1] = {5, 12}; // Dun Morogh - zone2LevelBracket[12] = {5, 12}; // Elwynn Forest - zone2LevelBracket[14] = {5, 12}; // Durotar - zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades - zone2LevelBracket[141] = {5, 12}; // Teldrassil - zone2LevelBracket[215] = {5, 12}; // Mulgore - zone2LevelBracket[3430] = {5, 12}; // Eversong Woods - zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle - - // Classic WoW - Mid - level zones - zone2LevelBracket[17] = {10, 25}; // Barrens - zone2LevelBracket[38] = {10, 20}; // Loch Modan - zone2LevelBracket[40] = {10, 21}; // Westfall - zone2LevelBracket[130] = {10, 23}; // Silverpine Forest - zone2LevelBracket[148] = {10, 21}; // Darkshore - zone2LevelBracket[3433] = {10, 22}; // Ghostlands - zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle - - // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Duskwood - zone2LevelBracket[11] = {21, 30}; // Wetlands - zone2LevelBracket[44] = {16, 28}; // Redridge Mountains - zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills - zone2LevelBracket[331] = {18, 33}; // Ashenvale - zone2LevelBracket[400] = {24, 36}; // Thousand Needles - zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains - - // Classic WoW - Higher - level zones - zone2LevelBracket[3] = {36, 46}; // Badlands - zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows - zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh - zone2LevelBracket[16] = {45, 52}; // Azshara - zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale - zone2LevelBracket[45] = {30, 42}; // Arathi Highlands - zone2LevelBracket[47] = {42, 51}; // Hinterlands - zone2LevelBracket[51] = {45, 51}; // Searing Gorge - zone2LevelBracket[357] = {40, 52}; // Feralas - zone2LevelBracket[405] = {30, 41}; // Desolace - zone2LevelBracket[440] = {41, 52}; // Tanaris - - // Classic WoW - Top - level zones - zone2LevelBracket[4] = {52, 57}; // Blasted Lands - zone2LevelBracket[28] = {50, 60}; // Western Plaguelands - zone2LevelBracket[46] = {51, 60}; // Burning Steppes - zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands - zone2LevelBracket[361] = {47, 57}; // Felwood - zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater - zone2LevelBracket[618] = {54, 61}; // Winterspring - zone2LevelBracket[1377] = {54, 63}; // Silithus - - // The Burning Crusade - Zones - zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula - zone2LevelBracket[3518] = {64, 70}; // Nagrand - zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest - zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley - zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh - zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains - zone2LevelBracket[3523] = {67, 73}; // Netherstorm - zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas - - // Wrath of the Lich King - Zones - zone2LevelBracket[65] = {71, 77}; // Dragonblight - zone2LevelBracket[66] = {74, 80}; // Zul'Drak - zone2LevelBracket[67] = {77, 80}; // Storm Peaks - zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier - zone2LevelBracket[394] = {72, 78}; // Grizzly Hills - zone2LevelBracket[495] = {68, 74}; // Howling Fjord - zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest - zone2LevelBracket[3537] = {68, 75}; // Borean Tundra - zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin - zone2LevelBracket[4197] = {79, 80}; // Wintergrasp - - // Override with values from config - for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) - { - zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; - } -} - -void RandomPlayerbotMgr::PrepareTeleportCache() -{ - uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("playerbots", "Preparing random teleport caches for {} levels...", maxLevel); - - QueryResult results = WorldDatabase.Query( - "SELECT " - "g.map, " - "position_x, " - "position_y, " - "position_z, " - "t.minlevel, " - "t.maxlevel " - "FROM " - "(SELECT " - "map, " - "MIN( c.guid ) guid " - "FROM " - "creature c " - "INNER JOIN creature_template t ON c.id1 = t.entry " - "WHERE " - "t.npcflag = 0 " - "AND t.lootid != 0 " - "AND t.maxlevel - t.minlevel < 3 " - "AND map IN ({}) " - "AND t.entry not in (32820, 24196, 30627, 30617) " - "AND c.spawntimesecs < 1000 " - "AND t.faction not in (11, 71, 79, 85, 188, 1575) " - "AND (t.unit_flags & 256) = 0 " - "AND (t.unit_flags & 4096) = 0 " - "AND t.rank = 0 " - // "AND (t.flags_extra & 32768) = 0 " - "GROUP BY " - "map, " - "ROUND(position_x / 50), " - "ROUND(position_y / 50), " - "ROUND(position_z / 50) " - "HAVING " - "count(*) >= 2) " - "AS g " - "INNER JOIN creature c ON g.guid = c.guid " - "INNER JOIN creature_template t on c.id1 = t.entry " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - uint32 collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - uint32 min_level = fields[4].Get(); - uint32 max_level = fields[5].Get(); - uint32 level = (min_level + max_level + 1) / 2; - WorldLocation loc(mapId, x, y, z, 0); - collected_locs++; - for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; - l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) - { - if (l < 1 || l > maxLevel) - { - continue; - } - locsPerLevelCache[(uint8)l].push_back(loc); - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} locations for level collected.", collected_locs); - - if (sPlayerbotAIConfig.enableNewRpgStrategy) - { - PrepareZone2LevelBracket(); - LOG_INFO("playerbots", "Preparing innkeepers / flightmasters locations for level..."); - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.faction, " - "t.entry, " - "t.npcflag, " - "c.guid " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 73728 " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 faction = fields[5].Get(); - uint32 tEntry = fields[6].Get(); - uint32 tNpcflag = fields[7].Get(); - uint32 guid = fields[8].Get(); - - if (tEntry == 3838 || tEntry == 29480) - continue; - - const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction); - - WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); - collected_locs++; - Map* map = sMapMgr->FindMap(loc.GetMapId(), 0); - if (!map) - continue; - bool forHorde = !(entry->hostileMask & 4); - bool forAlliance = !(entry->hostileMask & 2); - if (tNpcflag & UNIT_NPC_FLAG_FLIGHTMASTER) - { - WorldPosition pos(mapId, x, y, z, orient); - if (forHorde) - FlightMasterCache::Instance().AddHordeFlightMaster(guid, pos); - - if (forAlliance) - FlightMasterCache::Instance().AddAllianceFlightMaster(guid, pos); - } - const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); - uint32 zoneId = area->zone ? area->zone : area->ID; - if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end()) - continue; - LevelBracket bracket = zone2LevelBracket[zoneId]; - for (int i = bracket.low; i <= bracket.high; i++) - { - if (forHorde) - hordeStarterPerLevelCache[i].push_back(loc); - if (forAlliance) - allianceStarterPerLevelCache[i].push_back(loc); - } - - } while (results->NextRow()); - } - - // add all initial position - for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) - { - for (uint32 j = 1; j < MAX_CLASSES; j++) - { - PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); - - if (!info) - continue; - - WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); - - for (int32 l = 1; l <= 5; l++) - { - if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) - allianceStarterPerLevelCache[(uint8)l].push_back(pos); - else - hordeStarterPerLevelCache[(uint8)l].push_back(pos); - } - break; - } - } - LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs); - } - - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.minlevel, " - "t.entry " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 131072 " - "AND t.npcflag != 135298 " - "AND t.minlevel != 55 " - "AND t.minlevel != 65 " - "AND t.faction not in (35, 474, 69, 57) " - "AND t.entry not in (30606, 30608, 29282) " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 level = fields[5].Get(); - uint32 entry = fields[6].Get(); - BankerLocation bLoc; - bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); - bLoc.entry = entry; - collected_locs++; - for (int32 l = 1; l <= maxLevel; l++) - { - // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) - if (l <=60 && level > 45) - { - continue; - } - // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) - if ((l >=61 && l <=70) && (level < 60 || level > 70)) - { - continue; - } - // Bots 71+ go to Dalaran bankers (all have minlevel 75) - if ((l >=71) && level != 75) - { - continue; - } - bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); - bankerEntryToLocation[bLoc.entry] = bLoc.loc; - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} banker locations for level collected.", collected_locs); -} - void RandomPlayerbotMgr::PrepareAddclassCache() { // Using accounts marked as type 2 (AddClass) @@ -2125,11 +1757,6 @@ void RandomPlayerbotMgr::Init() if (sPlayerbotAIConfig.addClassCommand) sRandomPlayerbotMgr.PrepareAddclassCache(); - if (sPlayerbotAIConfig.enabled) - { - sRandomPlayerbotMgr.PrepareTeleportCache(); - } - if (sPlayerbotAIConfig.randomBotJoinBG) sRandomPlayerbotMgr.LoadBattleMastersCache(); @@ -2141,103 +1768,17 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; - if (level >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + std::vector locs = sTravelMgr.GetCityLocations(bot); + if (!locs.empty()) { - std::vector fallbackLocs; - for (auto& bLoc : bankerLocsPerLevelCache[level]) - fallbackLocs.push_back(bLoc.loc); - - if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Collect valid cities based on bot faction. - std::unordered_set validBankerCities; - for (auto& loc : bankerLocsPerLevelCache[level]) - { - auto cityIt = bankerToCity.find(loc.entry); - if (cityIt == bankerToCity.end()) continue; - - CityId cityId = cityIt->second.first; - FactionId cityFactionId = cityIt->second.second; - - if ((IsAlliance(bot->getRace()) && cityFactionId == FactionId::ALLIANCE) || - (!IsAlliance(bot->getRace()) && cityFactionId == FactionId::HORDE) || - (cityFactionId == FactionId::NEUTRAL)) - { - validBankerCities.insert(cityId); - } - } - - // Fallback if no valid cities - if (validBankerCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Apply weights to valid cities - std::vector weightedCities; - for (CityId city : validBankerCities) - { - int weight = 0; - switch (city) - { - case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; - case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; - case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; - case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; - case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; - case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; - case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; - case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; - case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; - case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; - default: weight = 0; break; - } - if (weight <= 0) continue; - - for (int i = 0; i < weight; ++i) - { - weightedCities.push_back(city); - } - } - - // Fallback if no valid cities - if (weightedCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Pick a weighted city randomly, then a random banker in that city - // then teleport to that banker - CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; - auto const& bankers = cityToBankers.at(selectedCity); - uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; - auto locIt = bankerEntryToLocation.find(selectedBankerEntry); - if (locIt != bankerEntryToLocation.end()) - { - std::vector teleportTarget = { locIt->second }; - RandomTeleport(bot, teleportTarget, true); - return; - } - - // Fallback if something went wrong - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, true); + return; } - else + locs = sTravelMgr.GetTeleportLocations(bot); + if (!locs.empty()) { - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, false); + return; } } @@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; + std::vector locs = sTravelMgr.GetTeleportLocations(bot); LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(), - bot->GetLevel(), locs->size()); + bot->GetLevel(), locs.size()); - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs); } void RandomPlayerbotMgr::RandomTeleport(Player* bot) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index 94c0a015134..db74f2cbea6 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -164,25 +164,8 @@ class RandomPlayerbotMgr : public PlayerbotHolder static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } void PrepareAddclassCache(); - void PrepareZone2LevelBracket(); - void PrepareTeleportCache(); void Init(); std::map> addclassCache; - std::map> locsPerLevelCache; - std::map> allianceStarterPerLevelCache; - std::map> hordeStarterPerLevelCache; - - struct LevelBracket { - uint32 low; - uint32 high; - bool InsideBracket(uint32 val) { return val >= low && val <= high; } - }; - std::map zone2LevelBracket; - struct BankerLocation { - WorldLocation loc; - uint32 entry; - }; - std::map> bankerLocsPerLevelCache; // Account type management void AssignAccountTypes(); diff --git a/src/Db/FlightMasterCache.cpp b/src/Db/FlightMasterCache.cpp deleted file mode 100644 index effe249936c..00000000000 --- a/src/Db/FlightMasterCache.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "FlightMasterCache.h" - -void FlightMasterCache::AddHordeFlightMaster(uint32 entry, WorldPosition pos) -{ - hordeFlightMasterCache[entry] = pos; -} - -void FlightMasterCache::AddAllianceFlightMaster(uint32 entry, WorldPosition pos) -{ - allianceFlightMasterCache[entry] = pos; -} - -Creature* FlightMasterCache::GetNearestFlightMaster(Player* bot) -{ - std::map& flightMasterCache = - (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; - - Creature* nearestFlightMaster = nullptr; - float nearestDistance = std::numeric_limits::max(); - - for (auto const& [entry, pos] : flightMasterCache) - { - if (pos.GetMapId() == bot->GetMapId()) - { - float distance = bot->GetExactDist2dSq(pos); - if (distance < nearestDistance) - { - Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); - if (flightMaster) - { - nearestDistance = distance; - nearestFlightMaster = flightMaster; - } - } - } - } - - return nearestFlightMaster; -} diff --git a/src/Db/FlightMasterCache.h b/src/Db/FlightMasterCache.h deleted file mode 100644 index 7f8b95310de..00000000000 --- a/src/Db/FlightMasterCache.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef _PLAYERBOT_FLIGHTMASTER_H -#define _PLAYERBOT_FLIGHTMASTER_H - -#include "Creature.h" -#include "Player.h" -#include "TravelMgr.h" - -class FlightMasterCache -{ -public: - static FlightMasterCache& Instance() - { - static FlightMasterCache instance; - - return instance; - } - - Creature* GetNearestFlightMaster(Player* bot); - void AddHordeFlightMaster(uint32 entry, WorldPosition pos); - void AddAllianceFlightMaster(uint32 entry, WorldPosition pos); - -private: - FlightMasterCache() = default; - ~FlightMasterCache() = default; - - FlightMasterCache(const FlightMasterCache&) = delete; - FlightMasterCache& operator=(const FlightMasterCache&) = delete; - - FlightMasterCache(FlightMasterCache&&) = delete; - FlightMasterCache& operator=(FlightMasterCache&&) = delete; - - std::map allianceFlightMasterCache; - std::map hordeFlightMasterCache; -}; - -#endif diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 703cca0ccc4..4ddba6d46fb 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -8,6 +8,10 @@ #include #include +#include "Creature.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "TravelNode.h" #include "Talentspec.h" #include "ChatHelper.h" #include "MMapFactory.h" @@ -22,6 +26,71 @@ #include "Corpse.h" #include "CellImpl.h" +// Navigation data + +enum class CityId : uint8 +{ + STORMWIND, + IRONFORGE, + DARNASSUS, + EXODAR, + ORGRIMMAR, + UNDERCITY, + THUNDER_BLUFF, + SILVERMOON_CITY, + SHATTRATH_CITY, + DALARAN +}; + +static const std::unordered_map> bankerToCity = { + {2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}}, + {2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}}, + {4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}}, + {17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}}, + {3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}}, + {4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}}, + {2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, + {17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, + {16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, + {19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, + {19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, + {30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}}, + {28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}} +}; + +static const std::unordered_map> cityToBankers = { + {CityId::STORMWIND, {2455, 2456, 2457}}, + {CityId::IRONFORGE, {2460, 2461, 5099}}, + {CityId::DARNASSUS, {4155, 4208, 4209}}, + {CityId::EXODAR, {17773, 18350, 16710}}, + {CityId::ORGRIMMAR, {3320, 3309, 3318}}, + {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, + {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, + {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, + {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, + {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} +}; + +static int GetCityWeight(CityId city) +{ + int weight = 0; + switch (city) + { + case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; + case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; + case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; + case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; + case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; + case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; + case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; + case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; + case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; + case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; + default: weight = 0; break; + } + return weight; +} + WorldPosition::WorldPosition(std::string const str) { std::vector tokens = split(str, '|'); @@ -4287,3 +4356,434 @@ void TravelMgr::printObj(WorldObject* obj, std::string const type) } } } + +void TravelMgr::Init() +{ + if (sPlayerbotAIConfig.enabled) + { + PrepareZone2LevelBracket(); + PrepareDestinationCache(); + } + sTravelNodeMap.InitTaxiGraph(); + LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); +} + +Creature* TravelMgr::GetNearestFlightMaster(Player* bot) +{ + std::map& flightMasterCache = + (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + + Creature* nearestFlightMaster = nullptr; + float nearestDistance = std::numeric_limits::max(); + + for (auto const& [entry, pos] : flightMasterCache) + { + if (pos.GetMapId() != bot->GetMapId()) + continue; + + float distance = bot->GetExactDist2dSq(pos); + if (distance > nearestDistance) + continue; + + Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); + if (flightMaster) + { + nearestDistance = distance; + nearestFlightMaster = flightMaster; + } + } + + return nearestFlightMaster; +} + +ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot) +{ + Creature* nearestFlightMaster = GetNearestFlightMaster(bot); + if (!nearestFlightMaster) + return ObjectGuid::Empty; + + return nearestFlightMaster->GetGUID(); +} + +std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot) +{ + std::vector> validDestinations; + + Creature* nearestFlightMaster = GetNearestFlightMaster(bot); + if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + return validDestinations; + + uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), + nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), + bot->GetTeamId()); + if (!fromNode) + return validDestinations; + std::vector candidateLocations; + if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + candidateLocations = GetCityLocations(bot); + + std::vector hubLocations = GetTravelHubs(bot); + candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end()); + + for (auto const& loc : candidateLocations) + { + uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(), + loc.GetPositionZ(), loc.GetMapId(), + bot->GetTeamId()); + if (!candidateNode) + continue; + + std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode); + if (!path.empty()) + validDestinations.push_back(path); + } + return validDestinations; +} + +const std::vector TravelMgr::GetTeleportLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + uint8 isAlliance = bot->GetTeamId() == TEAM_ALLIANCE; + if (sPlayerbotAIConfig.enableNewRpgStrategy) + return isAlliance ? allianceHubsPerLevelCache[level] : hordeHubsPerLevelCache[level]; + + return locsPerLevelCache[level]; +} + +const std::vector TravelMgr::GetTravelHubs(Player* bot) +{ + std::vector locs = bot->GetTeamId() == TEAM_ALLIANCE + ? allianceHubsPerLevelCache[bot->GetLevel()] + : hordeHubsPerLevelCache[bot->GetLevel()]; + return locs; +} + +std::vector TravelMgr::GetCityLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + + std::vector fallbackLocations; + for (auto& bLoc : bankerLocsPerLevelCache[level]) + fallbackLocations.push_back(bLoc.loc); + + if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) + return fallbackLocations; + + TeamId botTeamId = bot->GetTeamId(); + std::unordered_set validBankerCities; + for (auto& loc : bankerLocsPerLevelCache[level]) + { + auto cityIt = bankerToCity.find(loc.entry); + if (cityIt == bankerToCity.end()) + continue; + + TeamId cityTeamId = cityIt->second.second; + + if (cityTeamId == botTeamId || + (cityTeamId == TEAM_NEUTRAL) + ) + validBankerCities.insert(cityIt->second.first); + } + // Fallback if no valid cities + if (validBankerCities.empty()) + return fallbackLocations; + + // Apply weights to valid cities + std::vector weightedCities; + for (CityId city : validBankerCities) + { + int weight = GetCityWeight(city); + if (weight <= 0) + continue; + + for (int i = 0; i < weight; ++i) + weightedCities.push_back(city); + } + + // Fallback if no valid cities + if (weightedCities.empty()) + return fallbackLocations; + + // Pick a weighted city randomly, then a random banker in that city + CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + + auto const& bankers = cityToBankers.at(selectedCity); + uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; + auto locIt = bankerEntryToLocation.find(selectedBankerEntry); + if (locIt != bankerEntryToLocation.end()) + return { locIt->second }; + // Fallback if something went wrong + return fallbackLocations; +} + +void TravelMgr::PrepareZone2LevelBracket() +{ + // Classic WoW - Low - level zones + zone2LevelBracket[1] = {5, 12}; // Dun Morogh + zone2LevelBracket[12] = {5, 12}; // Elwynn Forest + zone2LevelBracket[14] = {5, 12}; // Durotar + zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades + zone2LevelBracket[141] = {5, 12}; // Teldrassil + zone2LevelBracket[215] = {5, 12}; // Mulgore + zone2LevelBracket[3430] = {5, 12}; // Eversong Woods + zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle + + // Classic WoW - Mid - level zones + zone2LevelBracket[17] = {10, 25}; // Barrens + zone2LevelBracket[38] = {10, 20}; // Loch Modan + zone2LevelBracket[40] = {10, 21}; // Westfall + zone2LevelBracket[130] = {10, 23}; // Silverpine Forest + zone2LevelBracket[148] = {10, 21}; // Darkshore + zone2LevelBracket[3433] = {10, 22}; // Ghostlands + zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle + + // Classic WoW - High - level zones + zone2LevelBracket[10] = {19, 33}; // Deadwind Pass + zone2LevelBracket[11] = {21, 30}; // Wetlands + zone2LevelBracket[44] = {16, 28}; // Redridge Mountains + zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills + zone2LevelBracket[331] = {18, 33}; // Ashenvale + zone2LevelBracket[400] = {24, 36}; // Thousand Needles + zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains + + // Classic WoW - Higher - level zones + zone2LevelBracket[3] = {36, 46}; // Badlands + zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows + zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh + zone2LevelBracket[16] = {45, 52}; // Azshara + zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale + zone2LevelBracket[45] = {30, 42}; // Arathi Highlands + zone2LevelBracket[47] = {42, 51}; // Hinterlands + zone2LevelBracket[51] = {45, 51}; // Searing Gorge + zone2LevelBracket[357] = {40, 52}; // Feralas + zone2LevelBracket[405] = {30, 41}; // Desolace + zone2LevelBracket[440] = {41, 52}; // Tanaris + + // Classic WoW - Top - level zones + zone2LevelBracket[4] = {52, 57}; // Blasted Lands + zone2LevelBracket[28] = {50, 60}; // Western Plaguelands + zone2LevelBracket[46] = {51, 60}; // Burning Steppes + zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands + zone2LevelBracket[361] = {47, 57}; // Felwood + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater + zone2LevelBracket[618] = {54, 61}; // Winterspring + zone2LevelBracket[1377] = {54, 63}; // Silithus + + // The Burning Crusade - Zones + zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula + zone2LevelBracket[3518] = {64, 70}; // Nagrand + zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest + zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley + zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh + zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains + zone2LevelBracket[3523] = {67, 73}; // Netherstorm + zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas + + // Wrath of the Lich King - Zones + zone2LevelBracket[65] = {71, 77}; // Dragonblight + zone2LevelBracket[66] = {74, 80}; // Zul'Drak + zone2LevelBracket[67] = {77, 80}; // Storm Peaks + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier + zone2LevelBracket[394] = {72, 78}; // Grizzly Hills + zone2LevelBracket[495] = {68, 74}; // Howling Fjord + zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest + zone2LevelBracket[3537] = {68, 75}; // Borean Tundra + zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin + zone2LevelBracket[4197] = {79, 80}; // Wintergrasp + + // Override with values from config + for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) + zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; +} + +void TravelMgr::PrepareDestinationCache() +{ + uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); + uint32 flightMastersCount = 0; + uint32 innkeepersCount = 0; + uint32 bankerCount = 0; + + LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); + // Temporary map to group creatures by entry and area + std::map, std::vector> tempLocsCache; + std::map>> tempCreatureCache; + for (auto const& [guid, creatureData] : sObjectMgr->GetAllCreatureData()) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureData.id1); + if (!creatureTemplate) + continue; + + uint16 mapId = creatureData.mapid; + if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId) + == sPlayerbotAIConfig.randomBotMaps.end()) + continue; + + float x = creatureData.posX; + float y = creatureData.posY; + float z = creatureData.posZ; + float orient = creatureData.orientation; + uint32 templateEntry = creatureData.id1; + + Map* map = sMapMgr->FindMap(mapId, 0); + if (!map) + continue; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); + if (!area) + continue; + + uint32 areaId = area->zone ? area->zone : area->ID; + + // CREATURES + if (creatureTemplate->npcflag == 0 && + creatureTemplate->lootid != 0 && + creatureTemplate->maxlevel - creatureTemplate->minlevel < 3 && + creatureTemplate->Entry != 32820 && creatureTemplate->Entry != 24196 && + creatureTemplate->Entry != 30627 && creatureTemplate->Entry != 30617 && + creatureData.spawntimesecs < 1000 && + creatureTemplate->faction != 11 && creatureTemplate->faction != 71 && + creatureTemplate->faction != 79 && creatureTemplate->faction != 85 && + creatureTemplate->faction != 188 && creatureTemplate->faction != 1575 && + (creatureTemplate->unit_flags & 256) == 0 && + (creatureTemplate->unit_flags & 4096) == 0 && + creatureTemplate->rank == 0) + { + uint32 roundX = (x / 50.0f) * 10.0f; + uint32 roundY = (y / 50.0f) * 10.0f; + uint32 roundZ = (z / 50.0f) * 10.0f; + tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); + tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); + } + // FLIGHT MASTERS + else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || + creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && + creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) + { + FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); + bool forHorde = !(factionEntry->hostileMask & 4); + bool forAlliance = !(factionEntry->hostileMask & 2); + + if (creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER) + { + WorldPosition pos(mapId, x, y, z, orient); + if (forHorde) + hordeFlightMasterCache[guid] = pos; + + if (forAlliance) + allianceFlightMasterCache[guid] = pos; + flightMastersCount++; + } + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) + { + if (zone2LevelBracket.find(areaId) == zone2LevelBracket.end()) + continue; + + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (int i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + innkeepersCount++; + } + } + } + // === BANKERS === + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_BANKER && + creatureTemplate->npcflag != 135298 && + creatureTemplate->minlevel != 55 && + creatureTemplate->minlevel != 65 && + creatureTemplate->faction != 35 && creatureTemplate->faction != 474 && + creatureTemplate->faction != 69 && creatureTemplate->faction != 57 && + creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && + creatureTemplate->Entry != 29282) + { + BankerLocation bLoc; + bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); + bLoc.entry = templateEntry; + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = 1; l <= maxLevel; l++) + { + // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) + if (l <=60 && level > 45) + continue; + + // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) + if ((l >=61 && l <=70) && (level < 60 || level > 70)) + continue; + + // Bots 71+ go to Dalaran bankers (all have minlevel 75) + if ((l >=71) && level != 75) + continue; + + bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); + bankerEntryToLocation[bLoc.entry] = bLoc.loc; + } + bankerCount++; + } + } + + // Process temporary caches + for (auto const& [gridTuple, creatureDataList] : tempLocsCache) + { + if (creatureDataList.size() > 2) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; + l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) + { + if (l < 1 || l > maxLevel) + continue; + + locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple))); + } + } + } + for (auto const& [entry, areaMap] : tempCreatureCache) + { + for (auto const& [area, locList] : areaMap) + { + if (locList.size() > 3) + continue; + + float totalX = 0, totalY = 0, totalZ = 0; + for (auto const& loc : locList) + { + totalX += loc.GetPositionX(); + totalY += loc.GetPositionY(); + totalZ += loc.GetPositionZ(); + } + float avgX = totalX / locList.size(); + float avgY = totalY / locList.size(); + float avgZ = totalZ / locList.size(); + creatureSpawnsByTemplate[entry].push_back(WorldLocation(locList[0].GetMapId(), avgX, avgY, avgZ, 0)); + } + } + // Add travel hubs based on player start locations + for (uint32 i = 1; i < MAX_RACES; i++) + { + for (uint32 j = 1; j < MAX_CLASSES; j++) + { + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); + + if (!info) + continue; + + WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); + + for (int32 l = 1; l <= 5; l++) + { + if ((1 << (i - 1)) & RACEMASK_ALLIANCE) + allianceHubsPerLevelCache[(uint8)l].push_back(pos); + else + hordeHubsPerLevelCache[(uint8)l].push_back(pos); + } + break; + } + } + LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); +} diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 1f5f848cdc4..f300ae6361f 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_TRAVELMGR_H #include +#include #include #include "AiObject.h" @@ -15,6 +16,7 @@ #include "GridDefines.h" #include "PlayerbotAIConfig.h" +class Creature; class GuidPosition; class ObjectGuid; class Quest; @@ -854,6 +856,16 @@ class TravelMgr void Clear(); void LoadQuestTravelTable(); + // Navigation + void Init(); + Creature* GetNearestFlightMaster(Player* bot); + ObjectGuid GetNearestFlightMasterGuid(Player* bot); + std::vector> GetOptimalFlightDestinations(Player* bot); + const std::vector GetTeleportLocations(Player* bot); + const std::vector GetTravelHubs(Player* bot); + std::vector GetCityLocations(Player* bot); + const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } + template void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g) { @@ -943,6 +955,37 @@ class TravelMgr TravelMgr(TravelMgr&&) = delete; TravelMgr& operator=(TravelMgr&&) = delete; + + // Navigation initialization + void PrepareZone2LevelBracket(); + void PrepareDestinationCache(); + + // Internal types + struct LevelBracket + { + uint32 low; + uint32 high; + bool InsideBracket(uint32 val) const { return val >= low && val <= high; } + }; + + struct BankerLocation + { + WorldLocation loc; + uint32 entry; + }; + + // Navigation caches + std::map allianceFlightMasterCache; + std::map hordeFlightMasterCache; + std::map> allianceHubsPerLevelCache; + std::map> hordeHubsPerLevelCache; + std::map> bankerLocsPerLevelCache; + std::unordered_map bankerEntryToLocation; + std::map> locsPerLevelCache; + std::unordered_map> creatureSpawnsByTemplate; + std::map zone2LevelBracket; }; +#define sTravelMgr TravelMgr::instance() + #endif diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 3e304677fe3..3b4996e974e 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "BudgetValues.h" #include "PathGenerator.h" @@ -2447,3 +2448,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId) return WorldPosition(mapId, 0, 0, 0, 0); } + +// ============================================================ +// TravelNodeMap taxi graph (BFS-based flight path lookup) +// ============================================================ + +void TravelNodeMap::InitTaxiGraph() +{ + BuildTaxiGraph(); + ComputeAllPaths(); +} + +std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) +{ + if (fromNode == toNode) + return {}; + + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode); + + if (!startNode || !endNode || startNode->map_id != endNode->map_id) + return {}; + + auto cacheItr = taxiPathCache.find(fromNode); + if (cacheItr == taxiPathCache.end()) + return {}; + + auto toNodeItr = cacheItr->second.find(toNode); + if (toNodeItr == cacheItr->second.end()) + return {}; + + return toNodeItr->second; +} + +void TravelNodeMap::BuildTaxiGraph() +{ + taxiGraph.clear(); + std::unordered_map> tempGraph; + for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i) + { + TaxiPathEntry const* path = sTaxiPathStore.LookupEntry(i); + if (!path) + continue; + + if (path->to == 0 || path->to == uint32(-1)) + continue; + + tempGraph[path->from].insert(path->to); + tempGraph[path->to].insert(path->from); + } + for (auto const& [node, neighbors] : tempGraph) + taxiGraph[node] = std::vector(neighbors.begin(), neighbors.end()); +} + +void TravelNodeMap::ComputeAllPaths() +{ + std::set allNodes; + for (auto const& [source, neighbors] : taxiGraph) + allNodes.insert(source); + + for (uint32 source : allNodes) + { + auto parentMap = BFS(source); + + for (uint32 target : allNodes) + { + if (source == target) + continue; + + auto path = BuildPath(source, target, parentMap); + if (!path.empty()) + taxiPathCache[source][target] = path; + } + } +} + +std::unordered_map TravelNodeMap::BFS(uint32 fromNode) +{ + std::queue workQueue; + std::unordered_set visited; + std::unordered_map parentMap; + + workQueue.push(fromNode); + visited.insert(fromNode); + parentMap[fromNode] = 0; + + while (!workQueue.empty()) + { + uint32 current = workQueue.front(); + workQueue.pop(); + + for (uint32 next : taxiGraph.at(current)) + { + if (visited.count(next)) + continue; + + visited.insert(next); + parentMap[next] = current; + workQueue.push(next); + } + } + return parentMap; +} + +std::vector TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap) +{ + if (!parentMap.count(toNode)) + return {}; // unreachable + + std::vector path; + uint32 current = toNode; + while (current != fromNode) + { + path.push_back(current); + auto it = parentMap.find(current); + if (it == parentMap.end() || it->second == 0) + break; + current = it->second; + } + + path.push_back(fromNode); + std::reverse(path.begin(), path.end()); + return path; +} diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 4dc2357216d..9e05e249072 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -580,6 +580,10 @@ class TravelNodeMap void calcMapOffset(); WorldPosition getMapOffset(uint32 mapId); + // Taxi graph (BFS-based path lookup between taxi nodes) + void InitTaxiGraph(); + std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); + std::shared_timed_mutex m_nMapMtx; std::unordered_map> teleportNodes; @@ -593,6 +597,16 @@ class TravelNodeMap TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap& operator=(TravelNodeMap&&) = delete; + // Taxi graph internals + void BuildTaxiGraph(); + void ComputeAllPaths(); + std::unordered_map BFS(uint32 startNode); + std::vector BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap); + + std::unordered_map> taxiGraph; + std::map>> taxiPathCache; + std::vector m_nodes; std::vector> mapOffsets; diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index fb0ffad4d68..6a8d60129e2 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -15,6 +15,7 @@ #include "RandomPlayerbotFactory.h" #include "RandomPlayerbotMgr.h" #include "Talentspec.h" +#include "TravelMgr.h" template void LoadList(std::string const value, T& list) @@ -691,6 +692,7 @@ bool PlayerbotAIConfig::Initialize() { PlayerbotDungeonRepository::instance().LoadDungeonSuggestions(); } + sTravelMgr.Init(); excludedHunterPetFamilies.clear(); LoadList>(sConfigMgr->GetOption("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies); From cba6af27add9722ea954d6084ecf2ae3212206a4 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 20 Mar 2026 14:40:59 -0500 Subject: [PATCH 15/87] Fix Assassination Rogue Finishers and add Cold Blood (#2215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description **Note for reviewers**: The Rogue files are very confusing, so for background, there is DpsRogueStrategy, which is for all Rogues and represented by the “dps” strategy in game, and there is also AssassinationRogueStrategy, which is for Assassination and Subtlety specs and represented by the “melee” strategy in game. So Combat has only the dps strategy, while Assassination and Subtlety have the dps and melee strategies. - The main focus of this PR is to fix an issue with Assassination Rogues that caused them to use Eviscerate instead of Envenom about 1/3 of the time they should have been using Envenom, which was significantly reducing their DPS. See the bottom of this post for an explanation for why this was happening and why the fix works. Well, LMK if you think it's wrong, but this is how I am understanding things, and my back-of-the-envelope math (also below) supports it. - After this PR, Assassination Rogues will use Eviscerate only if they are unable to use Envenom (don't have the ability learned or no Deadly Poison on the target) or if they don’t have Rank 3 in Master Poisoner. - Additionally, Assassination Rogues previously would use Envenom/Eviscerate at 3 or more combo points. This is suboptimal so I created a new “combo points 4 available” trigger that will fire at 4 or 5 combo points only. They will still use the finisher at 3 combo points if the mob is almost dead (via the existing “target with combo points almost dead” trigger). - I then added Cold Blood, which Rogues previously would not use at all. Now there is a ColdBloodAction(), and Cold Blood is used when a Rogue has at least 4 combo points, right before using Envenom (or Eviscerate). I implemented it as a standard BuffTrigger so they’ll just use the ability off cooldown. - While looking at the combo point triggers, I thought it was confusing that the “combo points available” trigger actually meant 5 combo points (presumably because the default parameter for combo points in ComboPointsAvailableTrigger() is 5). I changed the string to “combo points 5 available” so it’s less confusing going forward. This necessitated some changes in the Druid files too. - Next, I cleaned up DpsRogueStrategy a bit. Not a lot to say, just some duplicative or useless logic was removed. There shouldn’t be any impact on gameplay from the changes. - In the process of making the edits in the Druid files, I noticed that the trigger for Tiger’s Fury in OffhealDruidCatStrategy was “low energy,” which does not exist (there is a “light energy available,” but the EnergyAvailable triggers are for when energy is AT LEAST the designated level, not AT MOST the designated level). So I replaced the trigger with the already-existing “tiger’s fury” trigger, which I think is just a generic BuffTrigger so I don’t actually know why it exists (i.e., Druid will use the spell off cooldown). But this particular change is just a quick fix and not intended to be thoughtful (that would be outside the scope of this PR). ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. There should be no relevant impact on performance. This PR adds one new action triggered by the standard BuffTrigger. Otherwise, these are just fixes to existing logic. ## How to Test the Changes The easiest way to test is to fight a boss that doesn't tend to result in downtime (since downtime can lead to the loss of deadly poison stacks, in which case Eviscerate will (and should be) used by Assassination Rogues). You can use a damage meter such as Skada to track ability use. You should see: - Assassination Rogues don't use Eviscerate at all, or very few times. - Assassination Rogues use Cold Blood. - Offheal Cat Druids use Tiger's Fury. - Otherwise, Rogue and Cat Druid behavior should remain the same. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [ ] No - [x] Yes (**explain why**) Default behavior for Assassination Rogues was broken, as explained above. - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [ ] No - [x] Yes (**explain below**) I had Claude help me diagnose the initial issue and help me understand the queue system. And I had it implement the changes that were just busywork (like combo point triggers). ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers The reason for why Assassination Rogues were using Eviscerate so frequently is due to the fact that Envenom and Eviscerate were part of the same TriggerNode. When actions are part of the same TriggerNode, they're processed together, and the actions are queued by priority. When the higher-priority action is executed, the lower-priority action is not cleared--it remains in the queue for expireActionTime from the config, which is 5 seconds by default. Then, as soon as the lower-priority action can be executed (without regard for triggers because it is already triggered, just sitting in the queue), it will execute. This pattern of code works fine ingame if (1) you are actually trying to queue actions, like what I did with Cold Blood -> Envenom, (2) there are other guards like IsUseful() and IsPossible() that keep unwanted actions from executing, or (3) the trigger is just constantly firing so the higher-priority action is always evaluated. But TriggerNode isn't really the right way to implement action priority--that's through ActionNode. AssassinationRogueStrategy had Envenom and Eviscerate in the same TriggerNode, and then the corresponding ActionNode had Rupture as a fallback. Now, I changed it so Eviscerate is instead a fallback in the Envenom ActionNode (and Rupture is removed entirely because Assassination Rogues just shouldn't be using it, except maybe on very high-armor targets that are immune to poison, but that is very niche). ~ I did some back-of-the-envelope math to check this pattern. Say we're in a situation where Deadly Poison is up so ideally the Rogue should use Envenom 100% of the time. Through the old system, what would happen when the trigger fired? - Rogue uses Envenom since it's the higher-priority action. - Due to the Ruthlessness talent, Rogue has a 60% chance of having 1 combo point after the finisher, 40% chance of 0 combo points. If it has 1 combo point, it uses Eviscerate immediately. - If it has 0 combo points, it uses Mutilate. Mutilate grants 2 combo points, unless it crits, in which case it grants 3 due to Seal Fate. If Mutilate doesn't crit, the Rogue has 2 combo points, and it uses Eviscerate. If Mutilate does crit, the Rogue has 3 combo points, and it uses Envenom. - So let's assume Mutilate has a 55% crit chance (very reasonable for a Rogue in entry-level raid gear with raid buffs due to Opportunity giving +20% crit chance to Mutilate). Mutilate hits twice, and if either hit crits, Seal Fate Procs. The chance of at least one crit with two hits at a 55% crit chance is ~80%. That means if Ruthlessness doesn't give a combo point, there is an 80% chance that Envenom will be used and a 20% chance that Eviscerate will be used. - Combine the above, and the result of one trigger firing is you get 1 guaranteed Envenom + 0.6 Eviscerates (Ruthlessness proc path) + 0.32 Envenoms (No Ruthlessness proc but Seal Fate proc path) + 0.08 Eviscerates (No Ruthlessness proc and no Seal Fate proc path) = 1.32 Envenoms to each 0.68 Eviscerates, or a 1.94:1 ratio of Envenoms to Eviscerates. That is basically identical to what I saw in practice of roughly a 2:1 ratio of Envenoms to Eviscerates. - I understand the above is simplistic and it assumes that the Rogue gets a combo point within 5 seconds following using Envenom (very likely) and that there are not two opportunities to use Envenom or Eviscerate in the 5-second queue period after using Envenom (it can happen but is uncommon). That's all at the margins and isn't going to impact the math very much. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Base/TriggerContext.h | 6 ++-- .../Druid/Strategy/CatDpsDruidStrategy.cpp | 2 +- .../Strategy/OffhealDruidCatStrategy.cpp | 4 +-- src/Ai/Class/Rogue/Action/RogueActions.cpp | 2 +- src/Ai/Class/Rogue/Action/RogueActions.h | 6 ++++ src/Ai/Class/Rogue/RogueAiObjectContext.cpp | 2 ++ .../Strategy/AssassinationRogueStrategy.cpp | 11 ++++---- .../Class/Rogue/Strategy/DpsRogueStrategy.cpp | 28 ++----------------- 8 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index ceb7c001c1e..d4f8ac3b7d2 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -103,7 +103,8 @@ class TriggerContext : public NamedObjectContext creators["enemy within melee"] = &TriggerContext::enemy_within_melee; creators["party member to heal out of spell range"] = &TriggerContext::party_member_to_heal_out_of_spell_range; - creators["combo points available"] = &TriggerContext::ComboPointsAvailable; + creators["combo points 5 available"] = &TriggerContext::ComboPoints5Available; + creators["combo points 4 available"] = &TriggerContext::ComboPoints4Available; creators["combo points 3 available"] = &TriggerContext::ComboPoints3Available; creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead; creators["combo points not full"] = &TriggerContext::ComboPointsNotFull; @@ -338,7 +339,8 @@ class TriggerContext : public NamedObjectContext { return new PartyMemberToHealOutOfSpellRangeTrigger(botAI); } - static Trigger* ComboPointsAvailable(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI); } + static Trigger* ComboPoints5Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 5); } + static Trigger* ComboPoints4Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 4); } static Trigger* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); } static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai) { diff --git a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp index fda1b5f94fc..b1a4685b13b 100644 --- a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp @@ -228,7 +228,7 @@ void CatDpsDruidStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rip", ACTION_HIGH + 6) } diff --git a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp b/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp index c472ce8d863..fb7893651c0 100644 --- a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp @@ -176,7 +176,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rip", ACTION_HIGH + 6) } @@ -257,7 +257,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "low energy", + "tiger's fury", { NextAction("tiger's fury", ACTION_NORMAL + 1) } diff --git a/src/Ai/Class/Rogue/Action/RogueActions.cpp b/src/Ai/Class/Rogue/Action/RogueActions.cpp index b554a82537d..46beaf86c1a 100644 --- a/src/Ai/Class/Rogue/Action/RogueActions.cpp +++ b/src/Ai/Class/Rogue/Action/RogueActions.cpp @@ -61,7 +61,7 @@ bool CastEnvenomAction::isUseful() bool CastEnvenomAction::isPossible() { // alternate to eviscerate if talents unlearned - return botAI->HasAura(58410, bot) /* Master Poisoner */; + return botAI->HasAura(58410, bot) /* Master Poisoner Rank 3 */; } bool CastTricksOfTheTradeOnMainTankAction::isUseful() diff --git a/src/Ai/Class/Rogue/Action/RogueActions.h b/src/Ai/Class/Rogue/Action/RogueActions.h index dd0ad4735e8..3ae1f8142f5 100644 --- a/src/Ai/Class/Rogue/Action/RogueActions.h +++ b/src/Ai/Class/Rogue/Action/RogueActions.h @@ -78,6 +78,12 @@ class CastFeintAction : public CastBuffSpellAction CastFeintAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feint") {} }; +class CastColdBloodAction : public CastBuffSpellAction +{ +public: + CastColdBloodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold blood") {} +}; + class CastDismantleAction : public CastSpellAction { public: diff --git a/src/Ai/Class/Rogue/RogueAiObjectContext.cpp b/src/Ai/Class/Rogue/RogueAiObjectContext.cpp index 8586d93d149..4b4ebab1ef0 100644 --- a/src/Ai/Class/Rogue/RogueAiObjectContext.cpp +++ b/src/Ai/Class/Rogue/RogueAiObjectContext.cpp @@ -143,6 +143,7 @@ class RogueAiObjectContextInternal : public NamedObjectContext creators["use instant poison on off hand"] = &RogueAiObjectContextInternal::use_instant_poison_off_hand; creators["fan of knives"] = &RogueAiObjectContextInternal::fan_of_knives; creators["killing spree"] = &RogueAiObjectContextInternal::killing_spree; + creators["cold blood"] = &RogueAiObjectContextInternal::cold_blood; } private: @@ -184,6 +185,7 @@ class RogueAiObjectContextInternal : public NamedObjectContext static Action* use_instant_poison_off_hand(PlayerbotAI* ai) { return new UseInstantPoisonOffHandAction(ai); } static Action* fan_of_knives(PlayerbotAI* ai) { return new FanOfKnivesAction(ai); } static Action* killing_spree(PlayerbotAI* ai) { return new CastKillingSpreeAction(ai); } + static Action* cold_blood(PlayerbotAI* ai) { return new CastColdBloodAction(ai); } }; SharedNamedObjectContextList RogueAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp b/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp index b8563893cea..a104fae0775 100644 --- a/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp +++ b/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp @@ -29,7 +29,7 @@ class AssassinationRogueStrategyActionNodeFactory : public NamedObjectFactory& trigger triggers.push_back( new TriggerNode( - "combo points 3 available", + "combo points 4 available", { - NextAction("envenom", ACTION_HIGH + 5), - NextAction("eviscerate", ACTION_HIGH + 3) + NextAction("cold blood", ACTION_HIGH + 6), + NextAction("envenom", ACTION_HIGH + 5) } ) ); @@ -120,8 +120,7 @@ void AssassinationRogueStrategy::InitTriggers(std::vector& trigger new TriggerNode( "target with combo points almost dead", { - NextAction("envenom", ACTION_HIGH + 4), - NextAction("eviscerate", ACTION_HIGH + 2) + NextAction("envenom", ACTION_HIGH + 4) } ) ); diff --git a/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp b/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp index 22c6a6f8367..06aeda57c3d 100644 --- a/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp +++ b/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp @@ -12,36 +12,14 @@ class DpsRogueStrategyActionNodeFactory : public NamedObjectFactory public: DpsRogueStrategyActionNodeFactory() { - creators["mutilate"] = &mutilate; creators["sinister strike"] = &sinister_strike; creators["kick"] = &kick; creators["kidney shot"] = &kidney_shot; creators["backstab"] = &backstab; - creators["melee"] = &melee; creators["rupture"] = &rupture; } private: - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ {}, - /*A*/ { - NextAction("mutilate") }, - /*C*/ {} - ); - } - static ActionNode* mutilate([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mutilate", - /*P*/ {}, - /*A*/ { - NextAction("sinister strike") }, - /*C*/ {} - ); - } static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -77,7 +55,7 @@ class DpsRogueStrategyActionNodeFactory : public NamedObjectFactory "backstab", /*P*/ {}, /*A*/ { - NextAction("mutilate") }, + NextAction("sinister strike") }, /*C*/ {} ); } @@ -140,7 +118,7 @@ void DpsRogueStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rupture", ACTION_HIGH + 1), NextAction("eviscerate", ACTION_HIGH) @@ -335,7 +313,7 @@ void StealthedRogueStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("eviscerate", ACTION_HIGH) } From c6a07ad012cb7dd45468d7ce9cb6abfc94115409 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 20 Mar 2026 20:41:22 +0100 Subject: [PATCH 16/87] Every Man for Himself racial support (#2198) # Pull Request Added Every Man for Himself racial support Partially resolves: https://github.com/mod-playerbots/mod-playerbots/issues/2002 --- ## How to Test the Changes - when human bot is in combat apply aura via command `.aura 20066` - bot should use "Every Man for Himself" to remove aura ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Human bots now using "Every Man for Himself" by default where in combat If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [x] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Copilot CLI to review changes --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Test result: obraz --- src/Ai/Base/ActionContext.h | 2 ++ src/Ai/Base/Actions/GenericSpellActions.cpp | 24 +++++++++++++++++++++ src/Ai/Base/Actions/GenericSpellActions.h | 10 +++++++++ src/Ai/Base/Strategy/RacialsStrategy.cpp | 3 +++ src/Ai/Base/Trigger/GenericTriggers.cpp | 9 ++++++++ src/Ai/Base/Trigger/GenericTriggers.h | 8 +++++++ src/Ai/Base/TriggerContext.h | 2 ++ 7 files changed, 58 insertions(+) diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 6431ee87ca9..55081b443ca 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -163,6 +163,7 @@ class ActionContext : public NamedObjectContext creators["war stomp"] = &ActionContext::war_stomp; creators["blood fury"] = &ActionContext::blood_fury; creators["berserking"] = &ActionContext::berserking; + creators["every man for himself"] = &ActionContext::every_man_for_himself; creators["use trinket"] = &ActionContext::use_trinket; creators["auto talents"] = &ActionContext::auto_talents; creators["auto share quest"] = &ActionContext::auto_share_quest; @@ -357,6 +358,7 @@ class ActionContext : public NamedObjectContext static Action* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); } static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); } static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); } + static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); } static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 148bc6d3c2c..392d1850076 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -311,6 +311,30 @@ bool CastVehicleSpellAction::Execute(Event /*event*/) return botAI->CastVehicleSpell(spellId, GetTarget()); } +bool CastEveryManForHimselfAction::isPossible() +{ + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + if (!spellId) + return false; + + if (!bot->HasSpell(spellId)) + return false; + + if (bot->HasSpellCooldown(spellId)) + return false; + + return true; +} + +bool CastEveryManForHimselfAction::isUseful() +{ + return bot->HasAuraType(SPELL_AURA_MOD_STUN) || + bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_ROOT) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM); +} + bool UseTrinketAction::Execute(Event /*event*/) { Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1); diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index 9aa83f62d60..fdc0dcdcf13 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -284,6 +284,16 @@ class CastBerserkingAction : public CastBuffSpellAction CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {} }; +class CastEveryManForHimselfAction : public CastSpellAction +{ +public: + CastEveryManForHimselfAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "every man for himself") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; + bool isUseful() override; +}; + class UseTrinketAction : public Action { public: diff --git a/src/Ai/Base/Strategy/RacialsStrategy.cpp b/src/Ai/Base/Strategy/RacialsStrategy.cpp index dc6d5bc4820..753302c354e 100644 --- a/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -34,6 +34,9 @@ void RacialsStrategy::InitTriggers(std::vector& triggers) NextAction("berserking", ACTION_NORMAL + 5), NextAction("use trinket", ACTION_NORMAL + 4) })); + triggers.push_back(new TriggerNode( + "loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) })); + } RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI) diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 209766be73c..8cee6bef0ed 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -464,6 +464,15 @@ bool AttackerCountTrigger::IsActive() { return AI_VALUE(uint8, "attacker count") bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); } +bool LossOfControlTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_STUN) || + bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_ROOT) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM); +} + bool HasAuraStackTrigger::IsActive() { Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index 7b884fbf08b..68fc4b61f5c 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -746,6 +746,14 @@ class PossibleAddsTrigger : public Trigger bool IsActive() override; }; +class LossOfControlTrigger : public Trigger +{ +public: + LossOfControlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "loss of control", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index d4f8ac3b7d2..ca662f3d45c 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -59,6 +59,7 @@ class TriggerContext : public NamedObjectContext creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth; creators["generic boost"] = &TriggerContext::generic_boost; + creators["loss of control"] = &TriggerContext::loss_of_control; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -363,6 +364,7 @@ class TriggerContext : public NamedObjectContext return new PartyMemberAlmostFullHealthTrigger(botAI); } static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } + static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI); From d5762b7e0fcbb81c1afd27c8df1584c14373aa39 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 20 Mar 2026 14:41:47 -0500 Subject: [PATCH 17/87] Remove Vertical Speed Limit from Knockback Packet (#2223) ## Pull Request Description This PR removes the break from SMSG_MOVE_KNOCK_BACK for knockbacks with vertical speed of >35.0f. This break is the reason for many vertical knockbacks having no effect on bots, including Shade of Aran's Flame Wreath, High Astromancer Solarian's Wrath of the Astromancer, and Archimonde's Air Burst. There is a comment that indicates that the limit was originally added due to bots getting stuck from high-speed vertical knockbacks. I have not observed this at all and have been playing with this break removed for several months. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. I honestly cannot say if there is impact on processing cost because I have no understanding of packets. I would be surprised if there are any performance issues since knockback packets are ordinarily getting sent all the time, it's just a small number of moves that get skipped due to this break. ## How to Test the Changes 1. .go creature name High Astromancer Solarian 2. Start combat and wait until a bot gets hit with Wrath of the Astromancer 3. Wait for the aura to expire and watch the bot fly to Mars and fall back down ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [ ] No, not at all - [x] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) I do not know for sure, but as noted above, I would be surprised if there was any notable performance impact. - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [ ] Performance impact is understood, tested, and acceptable. <- I can't say for sure, but I've not had any issues. I would appreciate getting thoughts from somebody knowledgeable about packet use, however. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/PlayerbotAI.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 800247c6fcb..36631cead34 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1250,17 +1250,10 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed; if (horizontalSpeed <= 0.1f) - { horizontalSpeed = 0.11f; - } verticalSpeed = -verticalSpeed; - // high vertical may result in stuck as bot can not handle gravity - if (verticalSpeed > 35.0f) - break; - // stop casting - InterruptSpell(); - // stop movement + InterruptSpell(); bot->StopMoving(); bot->GetMotionMaster()->Clear(); From f160420d70414a4d580610dbb0667c5879096034 Mon Sep 17 00:00:00 2001 From: oskov Date: Fri, 20 Mar 2026 21:42:07 +0200 Subject: [PATCH 18/87] Fix/talent tree ordered map (#2222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2050 InitTalents builds a map of talentRow → [TalentEntry*] and iterates it to teach talents row by row. WoW's talent system requires each row to be filled before unlocking the next, so iteration must happen in ascending row order. Commit b474dc4 ("Performance optim") changed the container from std::map to std::unordered_map, which has no guaranteed key ordering. As a result, bots would frequently attempt to learn talents in a row whose prerequisites hadn't been met yet, silently skipping them. I belive it's the reason of #2050 issue. The fix is a one-character type change: restoring std::map, which guarantees ascending key (row) order. How to Test the Changes 1. Make fresh installation 2. Create new character 3. Observe talents tree of fresh rnd bots Was AI assistance used while working on this change? - [X] Yes — GitHub Copilot CLI was used to identify the root cause (unordered_map introduced in b474dc4 breaking talent row ordering), stage the one-line fix, and draft this PR description. The code change was reviewed and fully understood before submission. Root cause commit: b474dc44bb6323430a84fc17c1ec046f9919a101 ("Performance optim") — changed std::map to std::unordered_map in InitTalents, breaking the row-ordering guarantee that WoW's talent prerequisite system depends on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Bot/Factory/PlayerbotFactory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index c0e6d80dbb6..506ad9154fd 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -762,7 +762,7 @@ void PlayerbotFactory::InitPetTalents() // pet_family->petTalentType); return; } - std::unordered_map> spells; + std::map> spells; bool diveTypePet = (1LL << ci->family) & diveMask; for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) @@ -2653,7 +2653,7 @@ void PlayerbotFactory::InitSpecialSpells() void PlayerbotFactory::InitTalents(uint32 specNo) { uint32 classMask = bot->getClassMask(); - std::unordered_map> spells; + std::map> spells; for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) { TalentEntry const* talentInfo = sTalentStore.LookupEntry(i); From 98395a1090fb1ec6df6b976a1dd7ff4cc78de5ca Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 20 Mar 2026 20:42:22 +0100 Subject: [PATCH 19/87] Added cancellation druid form actions (#2194) # Pull Request Added new (for now manual) actions to cancel druid forms. Resolve: https://github.com/mod-playerbots/mod-playerbots/issues/1788 --- ## Feature Evaluation Please answer the following: - order bot enter some form like `do travel form` - order bot cancel form like `do cancel travel form` --- ## How to Test the Changes - order bot enter some form like `do travel form` - order bot cancel form like `do cancel travel form` ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- --- src/Ai/Base/ChatTriggerContext.h | 14 ++++ .../Strategy/ChatCommandHandlerStrategy.cpp | 7 ++ .../Druid/Action/DruidShapeshiftActions.cpp | 6 +- .../Druid/Action/DruidShapeshiftActions.h | 68 ++++++++++++++++++- src/Ai/Class/Druid/DruidAiObjectContext.cpp | 12 ++++ 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 54ae236b4c8..b305b19eae7 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -104,6 +104,13 @@ class ChatTriggerContext : public NamedObjectContext creators["target"] = &ChatTriggerContext::target; creators["formation"] = &ChatTriggerContext::formation; creators["stance"] = &ChatTriggerContext::stance; + creators["cancel tree form"] = &ChatTriggerContext::cancel_tree_form; + creators["cancel travel form"] = &ChatTriggerContext::cancel_travel_form; + creators["cancel bear form"] = &ChatTriggerContext::cancel_bear_form; + creators["cancel dire bear form"] = &ChatTriggerContext::cancel_dire_bear_form; + creators["cancel cat form"] = &ChatTriggerContext::cancel_cat_form; + creators["cancel moonkin form"] = &ChatTriggerContext::cancel_moonkin_form; + creators["cancel aquatic form"] = &ChatTriggerContext::cancel_aquatic_form; creators["sendmail"] = &ChatTriggerContext::sendmail; creators["mail"] = &ChatTriggerContext::mail; creators["outfit"] = &ChatTriggerContext::outfit; @@ -159,6 +166,13 @@ class ChatTriggerContext : public NamedObjectContext static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); } static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); } static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); } + static Trigger* cancel_tree_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel tree form"); } + static Trigger* cancel_travel_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel travel form"); } + static Trigger* cancel_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel bear form"); } + static Trigger* cancel_dire_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel dire bear form"); } + static Trigger* cancel_cat_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel cat form"); } + static Trigger* cancel_moonkin_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel moonkin form"); } + static Trigger* cancel_aquatic_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel aquatic form"); } static Trigger* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); } static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); } static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); } diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index 7bc1d53955b..64334b799ff 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -160,6 +160,13 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("save mana"); supported.push_back("formation"); supported.push_back("stance"); + supported.push_back("cancel tree form"); + supported.push_back("cancel travel form"); + supported.push_back("cancel bear form"); + supported.push_back("cancel dire bear form"); + supported.push_back("cancel cat form"); + supported.push_back("cancel moonkin form"); + supported.push_back("cancel aquatic form"); supported.push_back("sendmail"); supported.push_back("mail"); supported.push_back("outfit"); diff --git a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp index a87f22a3b55..f30d4ec032e 100644 --- a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp +++ b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp @@ -44,13 +44,13 @@ bool CastCasterFormAction::isUseful() AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; } -bool CastCancelTreeFormAction::Execute(Event /*event*/) +bool CastCancelDruidAction::Execute(Event /*event*/) { - botAI->RemoveAura("tree of life"); + botAI->RemoveAura(auraName); return true; } -bool CastCancelTreeFormAction::isUseful() { return botAI->HasAura(33891, bot); } +bool CastCancelDruidAction::isUseful() { return botAI->HasAura(auraId, bot); } bool CastTreeFormAction::isUseful() { diff --git a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h index 9d75f268208..d485171d24c 100644 --- a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h +++ b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h @@ -71,14 +71,78 @@ class CastCasterFormAction : public CastBuffSpellAction bool isPossible() override { return true; } }; -class CastCancelTreeFormAction : public CastBuffSpellAction +class CastCancelDruidAction : public CastBuffSpellAction { public: - CastCancelTreeFormAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cancel tree form") {} + CastCancelDruidAction(PlayerbotAI* botAI, std::string const& actionName, std::string const& auraName, uint32 auraId) + : CastBuffSpellAction(botAI, actionName), auraName(auraName), auraId(auraId) + { + } bool Execute(Event event) override; bool isUseful() override; bool isPossible() override { return true; } + +private: + std::string auraName; + uint32 auraId; +}; + +class CastCancelTreeFormAction : public CastCancelDruidAction +{ +public: + CastCancelTreeFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel tree form", "tree of life", 33891) + { + } +}; + +class CastCancelTravelFormAction : public CastCancelDruidAction +{ +public: + CastCancelTravelFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel travel form", "travel form", 783) + { + } +}; + +class CastCancelBearFormAction : public CastCancelDruidAction +{ +public: + CastCancelBearFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel bear form", "bear form", 5487) {} +}; + +class CastCancelDireBearFormAction : public CastCancelDruidAction +{ +public: + CastCancelDireBearFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel dire bear form", "dire bear form", 9634) + { + } +}; + +class CastCancelCatFormAction : public CastCancelDruidAction +{ +public: + CastCancelCatFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel cat form", "cat form", 768) {} +}; + +class CastCancelMoonkinFormAction : public CastCancelDruidAction +{ +public: + CastCancelMoonkinFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel moonkin form", "moonkin form", 24858) + { + } +}; + +class CastCancelAquaticFormAction : public CastCancelDruidAction +{ +public: + CastCancelAquaticFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel aquatic form", "aquatic form", 1066) + { + } }; #endif diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 29d9d4fdc0b..cc12f009fdd 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -170,6 +170,12 @@ class DruidAiObjectContextInternal : public NamedObjectContext creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form; creators["caster form"] = &DruidAiObjectContextInternal::caster_form; creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form; + creators["cancel travel form"] = &DruidAiObjectContextInternal::cancel_travel_form; + creators["cancel bear form"] = &DruidAiObjectContextInternal::cancel_bear_form; + creators["cancel dire bear form"] = &DruidAiObjectContextInternal::cancel_dire_bear_form; + creators["cancel cat form"] = &DruidAiObjectContextInternal::cancel_cat_form; + creators["cancel moonkin form"] = &DruidAiObjectContextInternal::cancel_moonkin_form; + creators["cancel aquatic form"] = &DruidAiObjectContextInternal::cancel_aquatic_form; creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear; creators["maul"] = &DruidAiObjectContextInternal::maul; creators["bash"] = &DruidAiObjectContextInternal::bash; @@ -258,6 +264,12 @@ class DruidAiObjectContextInternal : public NamedObjectContext static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); } static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); } static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); } + static Action* cancel_travel_form(PlayerbotAI* botAI) { return new CastCancelTravelFormAction(botAI); } + static Action* cancel_bear_form(PlayerbotAI* botAI) { return new CastCancelBearFormAction(botAI); } + static Action* cancel_dire_bear_form(PlayerbotAI* botAI) { return new CastCancelDireBearFormAction(botAI); } + static Action* cancel_cat_form(PlayerbotAI* botAI) { return new CastCancelCatFormAction(botAI); } + static Action* cancel_moonkin_form(PlayerbotAI* botAI) { return new CastCancelMoonkinFormAction(botAI); } + static Action* cancel_aquatic_form(PlayerbotAI* botAI) { return new CastCancelAquaticFormAction(botAI); } static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); } static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); } static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } From 2b273f6a2c8ffea04fc41d3bb58788af1a67d1d9 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:23:07 -0700 Subject: [PATCH 20/87] Fix merge error in test staging (#2226) Fix merge error we missed due to core sync issues. ## Pull Request Description ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Mgr/Travel/TravelMgr.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 4ddba6d46fb..d6942deab4f 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -4764,7 +4764,7 @@ void TravelMgr::PrepareDestinationCache() } } // Add travel hubs based on player start locations - for (uint32 i = 1; i < MAX_RACES; i++) + for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) { for (uint32 j = 1; j < MAX_CLASSES; j++) { @@ -4777,7 +4777,7 @@ void TravelMgr::PrepareDestinationCache() for (int32 l = 1; l <= 5; l++) { - if ((1 << (i - 1)) & RACEMASK_ALLIANCE) + if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) allianceHubsPerLevelCache[(uint8)l].push_back(pos); else hordeHubsPerLevelCache[(uint8)l].push_back(pos); From 32af1b95dea5b1be2840c8fd94c226239347bbe9 Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:19:22 -0400 Subject: [PATCH 21/87] Paladin Seal of wisdom fallback fix for Ret/Prot Paladins (#2147) # Pull Request This PR removes the fallback for Seal of Wisdom action from generic Paladin strategy and moves it to Holy Paladin only. This is necessary because a paladin who does not have Seal of Wisdom yet who goes low mana and triggers the seal of wisdom action will end up falling back to Seal of Righteousness, even though a better seal may be available, such as Seal of Command. The fallback is added to Holy Paladin so that low level holy paladins still use Righteousness until they get Wisdom --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? Ret paladin without Seal of Wisdom shouldn't change seals on low mana. - Describe the **cheapest implementation** that produces an acceptable result? Cheapest implementation is to remove seal of wisdom fallback for non-holy paladins. - Describe the **runtime cost** when this logic executes across many bots? No difference in cost compared to existing logic. --- ## How to Test the Changes Use a ret paladin bot who has Seal of Command but who does not have Seal of Wisdom. A paladin under level 38 will do. Order them to attack something, like a test dummy, until they eventually run low on mana. Before this change: The paladin will switch to Seal of Righteousness when they get low mana. After this change: The paladin leaves Seal of Command on when they get low mana. ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- .../Paladin/Strategy/DpsPaladinStrategy.cpp | 33 ----------------- .../GenericPaladinStrategyActionNodeFactory.h | 36 +++++++++++++------ .../Paladin/Strategy/HealPaladinStrategy.cpp | 2 +- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp index 185fb72d766..fc6ae92a0e1 100644 --- a/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp @@ -15,9 +15,6 @@ class DpsPaladinStrategyActionNodeFactory : public NamedObjectFactory& triggers) new TriggerNode( "seal", { - NextAction("seal of wisdom", ACTION_HIGH) + NextAction("seal of wisdom", ACTION_HIGH), } ) ); From 9f875a7c81124baca6392d83240c637e17b6140e Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:41:07 -0700 Subject: [PATCH 22/87] CoreUpdate - ThreatMgr (#2228) ## Pull Request Description Modification to threat system required for current core update PR. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Claude. Module search for changes made. It also identified a section of dead code in EnemyPlayerValue due to incorrect ref that was fixed. ## Final Checklist - - [X] Stability is not compromised. - - [X] Performance impact is understood, tested, and acceptable. - - [X] Added logic complexity is justified and explained. - - [X] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Actions/MovementActions.cpp | 4 +- src/Ai/Base/Actions/TellTargetAction.cpp | 17 +++--- src/Ai/Base/Trigger/GenericTriggers.cpp | 4 +- src/Ai/Base/Value/AttackersValue.cpp | 51 +++++++----------- src/Ai/Base/Value/CcTargetValue.cpp | 2 +- src/Ai/Base/Value/CurrentCcTargetValue.cpp | 2 +- src/Ai/Base/Value/DpsTargetValue.cpp | 62 ++++++++-------------- src/Ai/Base/Value/EnemyPlayerValue.cpp | 36 +++++-------- src/Ai/Base/Value/LeastHpTargetValue.cpp | 2 +- src/Ai/Base/Value/TankTargetValue.cpp | 33 ++++-------- src/Ai/Base/Value/TargetValue.cpp | 22 ++++---- src/Ai/Base/Value/TargetValue.h | 6 +-- src/Ai/Base/Value/ThreatValues.cpp | 2 +- 13 files changed, 95 insertions(+), 148 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index e1b8f636393..1dbca03122d 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -1387,8 +1387,8 @@ bool MovementAction::Flee(Unit* target) } } - HostileReference* ref = target->GetThreatMgr().getCurrentVictim(); - if (ref && ref->getTarget() == bot) // bot is target - try to flee to tank or master + Unit* currentVictim = target->GetThreatMgr().GetCurrentVictim(); + if (currentVictim && currentVictim == bot) // bot is target - try to flee to tank or master { if (Group* group = bot->GetGroup()) { diff --git a/src/Ai/Base/Actions/TellTargetAction.cpp b/src/Ai/Base/Actions/TellTargetAction.cpp index 422d5e542a4..aca05407d7d 100644 --- a/src/Ai/Base/Actions/TellTargetAction.cpp +++ b/src/Ai/Base/Actions/TellTargetAction.cpp @@ -6,7 +6,8 @@ #include "TellTargetAction.h" #include "Event.h" -#include "ThreatMgr.h" +#include "CombatManager.h" +#include "ThreatManager.h" #include "AiObjectContext.h" #include "PlayerbotAI.h" @@ -42,21 +43,21 @@ bool TellAttackersAction::Execute(Event /*event*/) botAI->TellMaster("--- Threat ---"); - HostileReference* ref = bot->getHostileRefMgr().getFirst(); - if (!ref) + auto const& threatenedByMe = bot->GetThreatMgr().GetThreatenedByMeList(); + if (threatenedByMe.empty()) return true; - while (ref) + for (auto const& [guid, ref] : threatenedByMe) { - ThreatMgr* threatMgr = ref->GetSource(); - Unit* unit = threatMgr->GetOwner(); + Unit* unit = ref->GetOwner(); + if (!unit) + continue; + float threat = ref->GetThreat(); std::ostringstream out; out << unit->GetName() << " (" << threat << ")"; botAI->TellMaster(out); - - ref = ref->next(); } return true; diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 8cee6bef0ed..735e1df7eaf 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -16,7 +16,7 @@ #include "PositionValue.h" #include "SharedDefines.h" #include "TemporarySummon.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" #include "Timer.h" #include "PlayerbotAI.h" #include "Player.h" @@ -217,7 +217,7 @@ bool LowTankThreatTrigger::IsActive() if (!current_target) return false; - ThreatMgr& mgr = current_target->GetThreatMgr(); + ThreatManager& mgr = current_target->GetThreatMgr(); float threat = mgr.GetThreat(bot); float tankThreat = mgr.GetThreat(mt); return tankThreat == 0.0f || threat > tankThreat * 0.5f; diff --git a/src/Ai/Base/Value/AttackersValue.cpp b/src/Ai/Base/Value/AttackersValue.cpp index dbde7ab8df6..65a8edeb3e1 100644 --- a/src/Ai/Base/Value/AttackersValue.cpp +++ b/src/Ai/Base/Value/AttackersValue.cpp @@ -92,21 +92,15 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set& t if (!player || !player->IsInWorld() || player->IsBeingTeleported()) return; - HostileRefMgr& refManager = player->getHostileRefMgr(); - HostileReference* ref = refManager.getFirst(); - if (!ref) - return; - - while (ref) + for (auto const& [guid, ref] : player->GetThreatMgr().GetThreatenedByMeList()) { - ThreatMgr* threatMgr = ref->GetSource(); - Unit* attacker = threatMgr->GetOwner(); + Unit* attacker = ref->GetOwner(); + if (!attacker) + continue; if (player->IsValidAttackTarget(attacker) && player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance) targets.insert(attacker); - - ref = ref->next(); } } @@ -131,7 +125,6 @@ bool AttackersValue::hasRealThreat(Unit* attacker) return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() && // !attacker->isInRoots() && !attacker->IsFriendlyTo(bot); - (attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast(attacker)); } bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) @@ -241,9 +234,6 @@ bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) { return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker); - // (attacker->GetThreatMgr().getCurrentVictim() || attacker->GetGuidValue(UNIT_FIELD_TARGET) || - // attacker->GetGUID().IsPlayer() || attacker->GetGUID() == - // GET_PLAYERBOT_AI(bot)->GetAiObjectContext()->GetValue("pull target")->Get()); } bool PossibleAddsValue::Calculate() @@ -255,27 +245,24 @@ bool PossibleAddsValue::Calculate() { if (find(attackers.begin(), attackers.end(), guid) != attackers.end()) continue; + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsInWorld() || add->IsDuringRemoveFromWorld()) + continue; - if (Unit* add = botAI->GetUnit(guid)) + if (!add->GetTarget() && !add->GetThreatMgr().GetLastVictim() && add->IsHostileTo(bot)) { - if (!add->IsInWorld() || add->IsDuringRemoveFromWorld()) - continue; - - if (!add->GetTarget() && !add->GetThreatMgr().getCurrentVictim() && add->IsHostileTo(bot)) + for (ObjectGuid const attackerGUID : attackers) { - for (ObjectGuid const attackerGUID : attackers) - { - Unit* attacker = botAI->GetUnit(attackerGUID); - if (!attacker) - continue; - - float dist = ServerFacade::instance().GetDistance2d(attacker, add); - if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) - continue; - - if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) - return true; - } + Unit* attacker = botAI->GetUnit(attackerGUID); + if (!attacker) + continue; + + float dist = ServerFacade::instance().GetDistance2d(attacker, add); + if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) + continue; + + if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) + return true; } } } diff --git a/src/Ai/Base/Value/CcTargetValue.cpp b/src/Ai/Base/Value/CcTargetValue.cpp index 0559656f7a1..a8de7a10ed6 100644 --- a/src/Ai/Base/Value/CcTargetValue.cpp +++ b/src/Ai/Base/Value/CcTargetValue.cpp @@ -20,7 +20,7 @@ class FindTargetForCcStrategy : public FindTargetStrategy } public: - void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override { Player* bot = botAI->GetBot(); if (!botAI->CanCastSpell(spell, creature)) diff --git a/src/Ai/Base/Value/CurrentCcTargetValue.cpp b/src/Ai/Base/Value/CurrentCcTargetValue.cpp index 97f35c1b553..b095c09d728 100644 --- a/src/Ai/Base/Value/CurrentCcTargetValue.cpp +++ b/src/Ai/Base/Value/CurrentCcTargetValue.cpp @@ -13,7 +13,7 @@ class FindCurrentCcTargetStrategy : public FindTargetStrategy { } - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (botAI->HasAura(spell, attacker)) result = attacker; diff --git a/src/Ai/Base/Value/DpsTargetValue.cpp b/src/Ai/Base/Value/DpsTargetValue.cpp index 28f308471f4..010e4719105 100644 --- a/src/Ai/Base/Value/DpsTargetValue.cpp +++ b/src/Ai/Base/Value/DpsTargetValue.cpp @@ -13,16 +13,14 @@ class FindMaxThreatGapTargetStrategy : public FindTargetStrategy public: FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), minThreat(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -32,7 +30,7 @@ class FindMaxThreatGapTargetStrategy : public FindTargetStrategy if (!result || CalcThreatGap(attacker, threatMgr) > CalcThreatGap(result, &result->GetThreatMgr())) result = attacker; } - float CalcThreatGap(Unit* attacker, ThreatMgr* threatMgr) + float CalcThreatGap(Unit* attacker, ThreatManager* threatMgr) { Unit* victim = attacker->GetVictim(); return threatMgr->GetThreat(victim) - threatMgr->GetThreat(attacker); @@ -52,7 +50,7 @@ class CasterFindTargetSmartStrategy : public FindTargetStrategy result = nullptr; } - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -61,13 +59,11 @@ class CasterFindTargetSmartStrategy : public FindTargetStrategy return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -90,24 +86,19 @@ class CasterFindTargetSmartStrategy : public FindTargetStrategy int new_level = GetIntervalLevel(new_unit); int old_level = GetIntervalLevel(old_unit); if (new_level != old_level) - { return new_level > old_level; - } + int32_t level = new_level; if (level % 10 == 2 || level % 10 == 0) - { return new_time < old_time; - } // dont switch targets when all of them with low health Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("current target")->Get(); if (currentTarget == new_unit) - { return true; - } + if (currentTarget == old_unit) - { return false; - } + return new_time > old_time; } int32_t GetIntervalLevel(Unit* unit) @@ -119,13 +110,11 @@ class CasterFindTargetSmartStrategy : public FindTargetStrategy attackRange += 5.0f; int level = dis < attackRange ? 10 : 0; if (time >= 5 && time <= 30) - { return level + 2; - } + if (time > 30) - { return level; - } + return level + 1; } @@ -143,7 +132,7 @@ class GeneralFindTargetSmartStrategy : public FindTargetStrategy { } - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -152,13 +141,11 @@ class GeneralFindTargetSmartStrategy : public FindTargetStrategy return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -186,9 +173,8 @@ class GeneralFindTargetSmartStrategy : public FindTargetStrategy // attack enemy in range and with lowest health int level = new_level; if (level == 10) - { return new_time < old_time; - } + // all targets are far away, choose the closest one return botAI->GetBot()->GetDistance(new_unit) < botAI->GetBot()->GetDistance(old_unit); } @@ -216,7 +202,7 @@ class ComboFindTargetSmartStrategy : public FindTargetStrategy { } - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -225,13 +211,11 @@ class ComboFindTargetSmartStrategy : public FindTargetStrategy return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -254,9 +238,8 @@ class ComboFindTargetSmartStrategy : public FindTargetStrategy int new_level = GetIntervalLevel(new_unit); int old_level = GetIntervalLevel(old_unit); if (new_level != old_level) - { return new_level > old_level; - } + // attack enemy in range and with lowest health int level = new_level; Player* bot = botAI->GetBot(); @@ -264,9 +247,8 @@ class ComboFindTargetSmartStrategy : public FindTargetStrategy { Unit* combo_unit = bot->GetComboTarget(); if (new_unit == combo_unit) - { return true; - } + return new_time < old_time; } // all targets are far away, choose the closest one @@ -319,7 +301,7 @@ class FindMaxHpTargetStrategy : public FindTargetStrategy public: FindMaxHpTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), maxHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { diff --git a/src/Ai/Base/Value/EnemyPlayerValue.cpp b/src/Ai/Base/Value/EnemyPlayerValue.cpp index c2f6e056a2f..41206a4eba2 100644 --- a/src/Ai/Base/Value/EnemyPlayerValue.cpp +++ b/src/Ai/Base/Value/EnemyPlayerValue.cpp @@ -5,6 +5,7 @@ #include "EnemyPlayerValue.h" +#include "CombatManager.h" #include "Playerbots.h" #include "ServerFacade.h" #include "Vehicle.h" @@ -51,34 +52,21 @@ Unit* EnemyPlayerValue::Calculate() controllingVehicle = true; } - // 1. Check units we are currently in combat with. + // 1. Check units we are currently in PvP combat with. std::vector targets; Unit* pVictim = bot->GetVictim(); - HostileReference* pReference = bot->getHostileRefMgr().getFirst(); - while (pReference) + for (auto const& [guid, combatRef] : bot->GetCombatManager().GetPvPCombatRefs()) { - ThreatMgr* threatMgr = pReference->GetSource(); - if (Unit* pTarget = threatMgr->GetOwner()) - { - if (pTarget != pVictim && pTarget->IsPlayer() && pTarget->CanSeeOrDetect(bot) && - bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) - { - if (bot->GetTeamId() == TEAM_HORDE) - { - if (pTarget->HasAura(23333)) - return pTarget; - } - else - { - if (pTarget->HasAura(23335)) - return pTarget; - } - - targets.push_back(pTarget); - } - } + Unit* pTarget = combatRef->GetOther(bot); + if (!pTarget || pTarget == pVictim || !pTarget->IsPlayer() || !pTarget->CanSeeOrDetect(bot) || + !bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) + continue; + + if ((bot->GetTeamId() == TEAM_HORDE && Target->HasAura(23333)) || + (bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335))) + return pTarget; - pReference = pReference->next(); + targets.push_back(pTarget); } if (!targets.empty()) diff --git a/src/Ai/Base/Value/LeastHpTargetValue.cpp b/src/Ai/Base/Value/LeastHpTargetValue.cpp index 2992952b3c4..c185628fad4 100644 --- a/src/Ai/Base/Value/LeastHpTargetValue.cpp +++ b/src/Ai/Base/Value/LeastHpTargetValue.cpp @@ -13,7 +13,7 @@ class FindLeastHpTargetStrategy : public FindNonCcTargetStrategy public: FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (IsCcTarget(attacker)) return; diff --git a/src/Ai/Base/Value/TankTargetValue.cpp b/src/Ai/Base/Value/TankTargetValue.cpp index 90c759a7dae..80def1cf97a 100644 --- a/src/Ai/Base/Value/TankTargetValue.cpp +++ b/src/Ai/Base/Value/TankTargetValue.cpp @@ -15,12 +15,11 @@ class FindTargetForTankStrategy : public FindNonCcTargetStrategy public: FindTargetForTankStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minThreat(0) {} - void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override { if (!creature || !creature->IsAlive()) - { return; - } + Player* bot = botAI->GetBot(); float threat = threatMgr->GetThreat(bot); if (!result) @@ -29,14 +28,10 @@ class FindTargetForTankStrategy : public FindNonCcTargetStrategy result = creature; } // neglect if victim is main tank, or no victim (for untauntable target) - if (threatMgr->getCurrentVictim()) + if (Unit* victim = threatMgr->GetCurrentVictim()) { - // float max_threat = threatMgr->GetThreat(threatMgr->getCurrentVictim()->getTarget()); - Unit* victim = threatMgr->getCurrentVictim()->getTarget(); - if (victim && victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer())) - { + if (victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer())) return; - } } if (minThreat >= threat) { @@ -54,7 +49,7 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy public: FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -63,13 +58,10 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy return; } if (!attacker->IsAlive()) - { return; - } + if (!result || IsBetter(attacker, result)) - { result = attacker; - } } bool IsBetter(Unit* new_unit, Unit* old_unit) { @@ -80,6 +72,7 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy { if (old_unit == currentTarget) return false; + if (new_unit == currentTarget) return true; } @@ -89,26 +82,22 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy float old_dis = bot->GetDistance(old_unit); // hasAggro? -> withinMelee? -> threat if (GetIntervalLevel(new_unit) != GetIntervalLevel(old_unit)) - { return GetIntervalLevel(new_unit) > GetIntervalLevel(old_unit); - } + int32_t interval = GetIntervalLevel(new_unit); if (interval == 2) - { return new_dis < old_dis; - } + return new_threat < old_threat; } int32_t GetIntervalLevel(Unit* unit) { if (!botAI->HasAggro(unit)) - { return 2; - } + if (botAI->GetBot()->IsWithinMeleeRange(unit)) - { return 1; - } + return 0; } }; diff --git a/src/Ai/Base/Value/TargetValue.cpp b/src/Ai/Base/Value/TargetValue.cpp index 21621545ead..19578daf4fb 100644 --- a/src/Ai/Base/Value/TargetValue.cpp +++ b/src/Ai/Base/Value/TargetValue.cpp @@ -5,12 +5,13 @@ #include "TargetValue.h" +#include "CombatManager.h" #include "LastMovementValue.h" #include "ObjectGuid.h" #include "Playerbots.h" #include "RtiTargetValue.h" #include "ScriptedCreature.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" Unit* FindTargetStrategy::GetResult() { return result; } @@ -23,8 +24,8 @@ Unit* TargetValue::FindTarget(FindTargetStrategy* strategy) if (!unit) continue; - ThreatMgr& ThreatMgr = unit->GetThreatMgr(); - strategy->CheckAttacker(unit, &ThreatMgr); + ThreatManager& threatMgr = unit->GetThreatMgr(); + strategy->CheckAttacker(unit, &threatMgr); } return strategy->GetResult(); @@ -144,24 +145,23 @@ Unit* FindTargetValue::Calculate() { return nullptr; } - HostileReference* ref = bot->getHostileRefMgr().getFirst(); - while (ref) + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) { - ThreatMgr* threatManager = ref->GetSource(); - Unit* unit = threatManager->GetOwner(); + Unit* unit = ref->GetOwner(); + if (!unit) + continue; + std::wstring wnamepart; Utf8toWStr(unit->GetName(), wnamepart); wstrToLower(wnamepart); if (!qualifier.empty() && qualifier.length() == wnamepart.length() && Utf8FitTo(qualifier, wnamepart)) - { return unit; - } - ref = ref->next(); } + return nullptr; } -void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatMgr* threatManager) +void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* threatManager) { UnitAI* unitAI = attacker->GetAI(); BossAI* bossAI = dynamic_cast(unitAI); diff --git a/src/Ai/Base/Value/TargetValue.h b/src/Ai/Base/Value/TargetValue.h index fcd7a58890c..e9e6cdea4db 100644 --- a/src/Ai/Base/Value/TargetValue.h +++ b/src/Ai/Base/Value/TargetValue.h @@ -11,7 +11,7 @@ #include "Value.h" class PlayerbotAI; -class ThreatMgr; +class ThreatManager; class Unit; class FindTargetStrategy @@ -20,7 +20,7 @@ class FindTargetStrategy FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {} Unit* GetResult(); - virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) = 0; + virtual void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) = 0; void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount); bool IsHighPriority(Unit* attacker); @@ -129,7 +129,7 @@ class FindBossTargetStrategy : public FindTargetStrategy { public: FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {} - virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatManager); + virtual void CheckAttacker(Unit* attacker, ThreatManager* threatManager); }; class BossTargetValue : public TargetValue, public Qualified diff --git a/src/Ai/Base/Value/ThreatValues.cpp b/src/Ai/Base/Value/ThreatValues.cpp index d95b00142c0..afd253193da 100644 --- a/src/Ai/Base/Value/ThreatValues.cpp +++ b/src/Ai/Base/Value/ThreatValues.cpp @@ -6,7 +6,7 @@ #include "ThreatValues.h" #include "Playerbots.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" uint8 ThreatValue::Calculate() { From d0d1171e067997402dc266544ef473011ee68669 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 22 Mar 2026 15:22:03 +0100 Subject: [PATCH 23/87] Fixed typo (#2230) ## Pull Request Description Fixed typo ## How to Test the Changes - compile test-staging with 20260320-ac-merge ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Value/EnemyPlayerValue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ai/Base/Value/EnemyPlayerValue.cpp b/src/Ai/Base/Value/EnemyPlayerValue.cpp index 41206a4eba2..3732e83cb63 100644 --- a/src/Ai/Base/Value/EnemyPlayerValue.cpp +++ b/src/Ai/Base/Value/EnemyPlayerValue.cpp @@ -62,7 +62,7 @@ Unit* EnemyPlayerValue::Calculate() !bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) continue; - if ((bot->GetTeamId() == TEAM_HORDE && Target->HasAura(23333)) || + if ((bot->GetTeamId() == TEAM_HORDE && pTarget->HasAura(23333)) || (bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335))) return pTarget; From f00fe15ff120b373edce0cb5d5a7d71d9963cfb5 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Mon, 23 Mar 2026 06:31:24 +0100 Subject: [PATCH 24/87] PR template checkboxes displaying fix (#2232) Maintenance PR --- PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 825717f83da..91484cb2e58 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -67,7 +67,7 @@ Bot messages have to be translatable, but you don't need to do the translations the message is in a translatable format, and list in the table the message_key and the default English message. Search for GetBotTextOrDefault in the codebase for examples. --> -Does this change add bot messages to translate? +- Does this change add bot messages to translate? - - [ ] No - - [ ] Yes (**list messages in the table**) @@ -81,7 +81,7 @@ Does this change add bot messages to translate? AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. We expect contributors to be honest about what they do and do not understand. --> -Was AI assistance used while working on this change? +- Was AI assistance used while working on this change? - - [ ] No - - [ ] Yes (**explain below**) ## Pull Request Description Updates the PR template regarding translation workflow, noting in the hidden instructions to the contributor that they must provide the full translations for any new lines. Also in the instructions the contributor is pointed to an example in the codebase of how this is done. Forward facing, the entire section about translations is deleted as it's not relevant anymore, and a step is added in the final checklist as a reminder for any needed translations. This PR itself is using the edited template. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- PULL_REQUEST_TEMPLATE.md | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 91484cb2e58..c8c2d7ed037 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -61,29 +61,14 @@ any impact on performance, you may skip these question. If necessary, a maintain -## Messages to Translate - -- Does this change add bot messages to translate? - - - [ ] No - - - [ ] Yes (**list messages in the table**) - -| Message key | Default message | -| --------------- | ------------------ | -| | | -| | | - ## AI Assistance -- Was AI assistance used while working on this change? - - - [ ] No - - - [ ] Yes (**explain below**) +Was AI assistance used while working on this change? +- - [ ] No +- - [ ] Yes (**explain below**) + ## Final Checklist - - [ ] Stability is not compromised. - - [ ] Performance impact is understood, tested, and acceptable. - - [ ] Added logic complexity is justified and explained. +- - [ ] Any new bot dialogue lines are translated. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers + + From c7ac849fbe6c8adb03f7d2dfdbcda52d9f974d55 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:38:02 -0600 Subject: [PATCH 26/87] Fix: Logout refactor and RNDbot logout error handling (#2131) # Pull Request * Improper crash fix by 297f11d3e59f52a29f68188245e6b786c9fa838e. That fix worked not because setting `logout = true` stops the server from crashing, but because it stopped the following `if (!logout)` block from executing at all. In that block `delete target;` was the actual cause of the crashing and this was recreated in testing. `botWorldSessionPtr->LogoutPlayer(true);` already deleted target internally, then comes `delete target;` to delete already freed memory and the whole thing crashes. * Players had the ability to logout anyone's alt/addClass bots. Now there's a check to make sure command is from master. * When commanded to logout, RNDbots in player's party used to reply with "I'm logging out!", but they don't, because they shouldn't, because they are not alt/adClass bots. Now they say "You can't command me to logout!" * Added early exits for the "logout cancel" block, then I remembered bots were made to always instantly logout because of past issues with timed logout. Need to review whether or not we should re-implement timed logout, or if it's not worth it and its dead code removed with instant logout remaining the only option. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? This PR removes more code than it adds, and makes sure that exits happen as early as possible. It has no effect on processing power and makes the code slightly more maintainable. --- ## How to Test the Changes 1. Whisper `logout` to any bot whose master is not you. The bot can be RND/alt/addClass. The bot may have someone else as a master or may not have a master at all. The bot may be part of a party or not. Regardless, you are not its master. It should tell you "You are not my master!". Two players or two instances of the client from two different accounts are needed for this test, in order for Player A to command a bot to logout, when the bot's master is Player B. 2. Invite an RND bot to your party. As along as it's in your party, you are it's master, but RND bots cannot be logged out through chat commands. If you whisper `logout` to it, it should say "You can't command me to logout!", and not logout. 3. Whisper to an alt/addClass bot `logout`. The bot can be in your party or could've been uninvited. All that matters is that you are its master. It should reply "I'm logging out!" ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) In that it fixes wrong behavior. If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [x] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Used Claude for code review, and translations. If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Lines to Translate These are keys and defaults of lines that were added/edited, and to be translated at a later SQL update. | Key | Default line | | --- | --- | | bot_not_your_master | You are not my master! | | bot_rndbot_no_logout | You can't command me to logout! | --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- .../2026_03_26_ai_playerbot_logout_texts.sql | 59 ++++++++++++++++++ src/Bot/PlayerbotAI.cpp | 56 +++++++++++++---- src/Bot/PlayerbotMgr.cpp | 60 ++++--------------- 3 files changed, 114 insertions(+), 61 deletions(-) create mode 100644 data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql diff --git a/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql b/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql new file mode 100644 index 00000000000..344e6109d5e --- /dev/null +++ b/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql @@ -0,0 +1,59 @@ +-- Translations for additional logout related messages +DELETE FROM ai_playerbot_texts WHERE name IN ('bot_not_your_master', 'bot_rndbot_no_logout'); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ('bot_not_your_master', 'bot_rndbot_no_logout'); + +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 ( + 1740, + 'bot_not_your_master', + "You are not my master!", + 0, 0, + -- koKR + "당신은 내 주인이 아닙니다!", + -- frFR + "Tu n'es pas mon maître !", + -- deDE + "Du bist nicht mein Meister!", + -- zhCN + "你不是我的主人!", + -- zhTW + "你不是我的主人!", + -- esES + "¡No eres mi amo!", + -- esMX + "¡No eres mi amo!", + -- ruRU + "Ты не мой хозяин!"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('bot_not_your_master', 100); + +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 ( + 1741, + 'bot_rndbot_no_logout', + "You can't command me to logout!", + 0, 0, + -- koKR + "당신은 나에게 로그아웃을 명령할 수 없습니다!", + -- frFR + "Tu ne peux pas m'ordonner de me déconnecter !", + -- deDE + "Du kannst mir nicht befehlen, mich auszuloggen!", + -- zhCN + "你不能命令我下线!", + -- zhTW + "你不能命令我登出!", + -- esES + "¡No puedes ordenarme que cierre sesión!", + -- esMX + "¡No puedes ordenarme que cierre sesión!", + -- ruRU + "Ты не можешь приказать мне выйти из игры!"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('bot_rndbot_no_logout', 100); \ No newline at end of file diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 36631cead34..eb758d64555 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1015,28 +1015,58 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro } else if (filtered == "logout") { - if (!bot->GetSession()->isLogingOut()) + if (bot->GetSession()->isLogingOut()) + return; + + // Verify the command came from this bot's master. Also handles nullptr + if (fromPlayer != master) { if (type == CHAT_MSG_WHISPER) - TellMaster("I'm logging out!"); + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bot_not_your_master", "You are not my master!", {}); + bot->Whisper(message, LANG_UNIVERSAL, fromPlayer); + } + return; + } - PlayerbotMgr* masterBotMgr = nullptr; - if (master) - masterBotMgr = GET_PLAYERBOT_MGR(master); - if (masterBotMgr) - masterBotMgr->LogoutPlayerBot(bot->GetGUID()); + PlayerbotMgr* masterBotMgr = GET_PLAYERBOT_MGR(master); + if (!masterBotMgr) + return; + + // Only respond if this bot is in master's collection (alt/addclass) + if (masterBotMgr->GetPlayerBot(bot->GetGUID())) + { + if (type == CHAT_MSG_WHISPER) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "logout_start", "I'm logging out!", {}); + TellMaster(message); + } + + masterBotMgr->LogoutPlayerBot(bot->GetGUID()); + } + else if (type == CHAT_MSG_WHISPER) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bot_rndbot_no_logout", "You can't command me to logout!", {}); + TellMaster(message); } } else if (filtered == "logout cancel") { - if (bot->GetSession()->isLogingOut()) - { - if (type == CHAT_MSG_WHISPER) - TellMaster("Logout cancelled!"); + if (!bot->GetSession()->isLogingOut()) + return; - WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); - bot->GetSession()->HandleLogoutCancelOpcode(data); + if (type == CHAT_MSG_WHISPER) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "logout_cancel", "Logout cancelled!", {}); + TellMaster(message); } + + WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); + bot->GetSession()->HandleLogoutCancelOpcode(data); } else { diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index 9859877f94b..c3b614a98fb 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -362,6 +362,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) if (master) masterWorldSessionPtr = master->GetSession(); + // TODO: Review whether or not to implement timed logout. + // Unused block. Useful only for timed logout. +/* // check for instant logout bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr)); @@ -373,61 +376,22 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || botWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)) - { logout = true; - } if (master && (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || master->HasUnitState(UNIT_STATE_IN_FLIGHT) || (masterWorldSessionPtr && masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))) - { logout = true; - } - - TravelTarget* target = nullptr; - if (botAI->GetAiObjectContext()) // Maybe some day re-write to delate all pointer values. - { - target = botAI->GetAiObjectContext()->GetValue("travel target")->Get(); - } - - // Peiru: Allow bots to always instant logout to see if this resolves logout crashes - logout = true; - - // if no instant logout, request normal logout - if (!logout) - { - if (bot->GetSession()->isLogingOut()) - return; - else if (bot) - { - botAI->TellMaster("I'm logging out!"); - WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); - botWorldSessionPtr->HandleLogoutRequestOpcode(data); - if (!bot) - { - RemoveFromPlayerbotsMap(guid); - delete botWorldSessionPtr; - if (target) - delete target; - } - return; - } - else - { - RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap - delete botWorldSessionPtr; // finally delete the bot's WorldSession - if (target) - delete target; - } - return; - } // if instant logout possible, do it - else if (bot && (logout || !botWorldSessionPtr->isLogingOut())) - { - botAI->TellMaster("Goodbye!"); - RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap - botWorldSessionPtr->LogoutPlayer(true); // this will delete the bot Player object and PlayerbotAI object - delete botWorldSessionPtr; // finally delete the bot's WorldSession +*/ + // Instant logout (the only option right now) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "goodbye", "Goodbye!", {}); + botAI->TellMaster(message); + RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap + botWorldSessionPtr->LogoutPlayer(true); // this will delete the bot Player object and PlayerbotAI object + delete botWorldSessionPtr; // finally delete the bot's WorldSession } } } From 91eac70ca2dc0edbed689ccf0587f32cd894106c Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 27 Mar 2026 12:38:12 -0500 Subject: [PATCH 27/87] Implement Zul'Aman Strategies (#2186) # Pull Request _Implement strategies for all bosses in Zul'Aman. See next post for overview of implemented strategies._ --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? _I have attempted to order checks while taking into account cost and likelihood and have opted to find lower-cost methods where possible. I have also not gone as in depth as I have with other strategies, partially because it is not necessary to complete encounters but also to try to limit the performance impact. From my observation, including with pmon, none of the methods should be overly taxing._ --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it _Run Zul'Aman. See next post for strategies to test._ ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [x] Yes (**explain below**) _Only in the context of raid strategies, with new methods to consider and new multipliers to evaluate when bots perform actions with the instance strategy active._ Does this change increase per-bot or per-tick processing? - - [ ] No - - [x] Yes (**describe and justify impact**) _The impact is only with the "zulaman" strategy active, which will be applied only in the instance. There is currently a PR open to also remove instance strategies when leaving the map to get rid of the residual performance impact._ Could this logic scale poorly under load? - - [ ] No - - [x] Yes (**explain why**) _Technically yes, but I think it is unlikely to have an appreciable difference unless there are many groups running the instance at the same time on a large server._ ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) _Only in the instance. It is a necessary trade-off to consider with raid strategies that I always keep in mind (degree of automation vs. player choice)._ If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable _Not exactly sure how to address this question in the context of this PR, but there aren't any techniques or methods for this strategy that I have not tried before (or something very similar)._ ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted _Gemini and GPT for some questions about the codebase and C++ and assistance with drafting a few things like containers that I find more tedious to do myself._ AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Raid/RaidStrategyContext.h | 3 + .../RaidTempestKeepActionContext.h | 6 +- .../Strategy/RaidTempestKeepStrategy.cpp | 2 +- .../ZulAman/Action/RaidZulAmanActions.cpp | 745 ++++++++++++++++++ .../Raid/ZulAman/Action/RaidZulAmanActions.h | 253 ++++++ .../Multiplier/RaidZulAmanMultipliers.cpp | 382 +++++++++ .../Multiplier/RaidZulAmanMultipliers.h | 167 ++++ .../Raid/ZulAman/RaidZulAmanActionContext.h | 202 +++++ .../Raid/ZulAman/RaidZulAmanTriggerContext.h | 206 +++++ .../ZulAman/Strategy/RaidZulAmanStrategy.cpp | 134 ++++ .../ZulAman/Strategy/RaidZulAmanStrategy.h | 23 + .../ZulAman/Trigger/RaidZulAmanTriggers.cpp | 271 +++++++ .../ZulAman/Trigger/RaidZulAmanTriggers.h | 249 ++++++ .../Raid/ZulAman/Util/RaidZulAmanHelpers.cpp | 190 +++++ src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h | 110 +++ src/Bot/Engine/BuildSharedActionContexts.cpp | 2 + src/Bot/Engine/BuildSharedTriggerContexts.cpp | 2 + src/Bot/PlayerbotAI.cpp | 5 +- 18 files changed, 2947 insertions(+), 5 deletions(-) create mode 100644 src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.cpp create mode 100644 src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.h create mode 100644 src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp create mode 100644 src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.h create mode 100644 src/Ai/Raid/ZulAman/RaidZulAmanActionContext.h create mode 100644 src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h create mode 100644 src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.cpp create mode 100644 src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h create mode 100644 src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp create mode 100644 src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.h create mode 100644 src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp create mode 100644 src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index d117c459e2d..970ecf4aa59 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -11,6 +11,7 @@ #include "RaidNaxxStrategy.h" #include "RaidSSCStrategy.h" #include "RaidTempestKeepStrategy.h" +#include "RaidZulAmanStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" #include "RaidVoAStrategy.h" @@ -32,6 +33,7 @@ class RaidStrategyContext : public NamedObjectContext creators["naxx"] = &RaidStrategyContext::naxx; creators["ssc"] = &RaidStrategyContext::ssc; creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; + creators["zulaman"] = &RaidStrategyContext::zulaman; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; @@ -50,6 +52,7 @@ class RaidStrategyContext : public NamedObjectContext static Strategy* naxx(PlayerbotAI* botAI) { return new RaidNaxxStrategy(botAI); } static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } + static Strategy* zulaman(PlayerbotAI* botAI) { return new RaidZulAmanStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h index 50b2de1f41e..125b27c0f15 100644 --- a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h @@ -116,8 +116,8 @@ class RaidTempestKeepActionContext : public NamedObjectContext creators["kael'thas sunstrider assign legendary weapon dps priority"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_legendary_weapon_dps_priority; - creators["kael'thas sunstrider main tank move devastation away"] = - &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_move_devastation_away; + creators["kael'thas sunstrider move devastation away"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_move_devastation_away; creators["kael'thas sunstrider loot legendary weapons"] = &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; @@ -255,7 +255,7 @@ class RaidTempestKeepActionContext : public NamedObjectContext static Action* kaelthas_sunstrider_assign_legendary_weapon_dps_priority( PlayerbotAI* botAI) { return new KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction(botAI); } - static Action* kaelthas_sunstrider_main_tank_move_devastation_away( + static Action* kaelthas_sunstrider_move_devastation_away( PlayerbotAI* botAI) { return new KaelthasSunstriderMoveDevastationAwayAction(botAI); } static Action* kaelthas_sunstrider_loot_legendary_weapons( diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp index 74950c3ad9e..627d3d7abd0 100644 --- a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp @@ -105,7 +105,7 @@ void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1) })); triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { - NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1) })); + NextAction("kael'thas sunstrider move devastation away", ACTION_EMERGENCY + 1) })); triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID) })); diff --git a/src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.cpp b/src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.cpp new file mode 100644 index 00000000000..7ff3d64f270 --- /dev/null +++ b/src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.cpp @@ -0,0 +1,745 @@ +/* + * 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 "RaidZulAmanActions.h" +#include "RaidZulAmanHelpers.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace ZulAmanHelpers; + +// Trash + +bool AmanishiMedicineManMarkWardAction::Execute(Event /*event*/) +{ + if (Unit* protectiveWard = GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_AMANI_PROTECTIVE_WARD))) + { + MarkTargetWithSkull(bot, protectiveWard); + } + else if (Unit* healingWard = GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_AMANI_HEALING_WARD))) + { + MarkTargetWithSkull(bot, healingWard); + } + + return false; +} + +// Akil'zon + +bool AkilzonMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + if (!akilzon) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", akilzon)) + return botAI->CastSpell("steady shot", akilzon); + + return false; +} + +bool AkilzonTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + if (!akilzon) + return false; + + if (bot->GetVictim() != akilzon) + return Attack(akilzon); + + if (akilzon->GetVictim() == bot) + { + const Position& position = AKILZON_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool AkilzonSpreadRangedAction::Execute(Event /*event*/) +{ + constexpr float minDistance = 13.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance)) + return FleePosition(nearestPlayer->GetPosition(), minDistance, minInterval); + + return false; +} + +bool AkilzonMoveToEyeOfTheStormAction::Execute(Event /*event*/) +{ + Player* target = GetElectricalStormTarget(bot); + if (!target && !botAI->IsMainTank(bot)) + target = GetGroupMainTank(botAI, bot); + + if (target && bot->GetExactDist2d(target) > 2.0f) + { + botAI->Reset(); + return MoveTo(ZULAMAN_MAP_ID, target->GetPositionX(), target->GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool AkilzonManageElectricalStormTimerAction::Execute(Event /*event*/) +{ + const time_t now = std::time(nullptr); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + if (akilzon) + { + auto [it, inserted] = akilzonStormTimer.try_emplace(instanceId, now); + return inserted; + } + else if (!bot->IsInCombat() && !akilzon && akilzonStormTimer.erase(instanceId) > 0) + { + return true; + } + + return false; +} + +// Nalorakk + +bool NalorakkMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + if (!nalorakk) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", nalorakk)) + return botAI->CastSpell("steady shot", nalorakk); + + return false; +} + +bool NalorakkTanksPositionBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + if (!nalorakk) + return false; + + if (botAI->IsMainTank(bot)) + return MainTankPositionTrollForm(nalorakk); + else + return FirstAssistTankPositionBearForm(nalorakk); +} + +bool NalorakkTanksPositionBossAction::MainTankPositionTrollForm(Unit* nalorakk) +{ + if (!nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + { + if (bot->GetVictim() != nalorakk) + return Attack(nalorakk); + + if (nalorakk->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + const Position& position = NALORAKK_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool NalorakkTanksPositionBossAction::FirstAssistTankPositionBearForm(Unit* nalorakk) +{ + if (nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + { + if (bot->GetVictim() != nalorakk) + return Attack(nalorakk); + + if (nalorakk->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + const Position& position = NALORAKK_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool NalorakkSpreadRangedAction::Execute(Event /*event*/) +{ + constexpr float minDistance = 11.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance)) + return FleePosition(nearestPlayer->GetPosition(), minDistance, minInterval); + + return false; +} + +// Jan'alai + +bool JanalaiMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* janalai = AI_VALUE2(Unit*, "find target", "jan'alai"); + if (!janalai) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", janalai)) + return botAI->CastSpell("steady shot", janalai); + + return false; +} + +bool JanalaiTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* janalai = AI_VALUE2(Unit*, "find target", "jan'alai"); + if (!janalai) + return false; + + if (bot->GetVictim() != janalai) + return Attack(janalai); + + if (janalai->GetVictim() == bot) + { + const Position& position = JANALAI_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool JanalaiSpreadRangedInCircleAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector rangedMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + + if (rangedMembers.empty()) + return false; + + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = + (findIt != rangedMembers.end()) ? std::distance(rangedMembers.begin(), findIt) : 0; + size_t count = rangedMembers.size(); + if (count == 0) + return false; + + constexpr float radius = 15.0f; + float angle = (count == 1) ? 0.0f : + (2.0f * M_PI * static_cast(botIndex) / static_cast(count)); + + float targetX = JANALAI_TANK_POSITION.GetPositionX() + radius * std::cos(angle); + float targetY = JANALAI_TANK_POSITION.GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + return MoveTo(ZULAMAN_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool JanalaiAvoidFireBombsAction::Execute(Event /*event*/) +{ + auto const& bombs = GetAllHazardTriggers( + bot, static_cast(ZulAmanNPCs::NPC_FIRE_BOMB), 50.0f); + + if (bombs.empty()) + return false; + + constexpr float hazardRadius = 5.0f; + bool inDanger = false; + for (Unit* bomb : bombs) + { + if (bot->GetDistance2d(bomb) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + const Position& janalaiCenter = JANALAI_TANK_POSITION; + constexpr float safeZoneRadius = 17.0f; + + Position safestPos = + FindSafestNearbyPosition(bot, bombs, janalaiCenter, safeZoneRadius, hazardRadius, false); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(ZULAMAN_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool JanalaiMarkAmanishiHatchersAction::Execute(Event /*event*/) +{ + auto [hatcherLow, hatcherHigh] = GetAmanishiHatcherPair(botAI); + + if (hatcherLow && hatcherHigh && hatcherHigh != hatcherLow) + { + MarkTargetWithSkull(bot, hatcherLow); + MarkTargetWithMoon(bot, hatcherHigh); + SetRtiTarget(botAI, "skull", hatcherLow); + } + + return false; +} + +// Halazzi + +bool HalazziMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi"); + if (!halazzi) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", halazzi)) + return botAI->CastSpell("steady shot", halazzi); + + return false; +} + +bool HalazziMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi"); + if (!halazzi) + return false; + + MarkTargetWithStar(bot, halazzi); + SetRtiTarget(botAI, "star", halazzi); + + if (bot->GetVictim() != halazzi) + return Attack(halazzi); + + if (halazzi->GetVictim() == bot) + { + const Position& position = HALAZZI_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool HalazziFirstAssistTankAttackSpiritLynxAction::Execute(Event /*event*/) +{ + bool targetFound = false; + + if (Unit* lynx = AI_VALUE2(Unit*, "find target", "spirit of the lynx")) + { + MarkTargetWithCircle(bot, lynx); + SetRtiTarget(botAI, "circle", lynx); + + if (bot->GetVictim() != lynx) + return Attack(lynx); + + if (lynx->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + + targetFound = true; + } + else if (Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi")) + { + SetRtiTarget(botAI, "star", halazzi); + + if (bot->GetVictim() != halazzi) + return Attack(halazzi); + + targetFound = true; + } + + if (!targetFound) + return false; + + const Position& position = HALAZZI_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool HalazziAssignDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Corrupted Lightning Totems + if (Unit* totem = GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_CORRUPTED_LIGHTNING_TOTEM))) + { + MarkTargetWithSkull(bot, totem); + SetRtiTarget(botAI, "skull", totem); + + if (bot->GetTarget() != totem->GetGUID()) + return Attack(totem); + + return false; + } + + // Target priority 2: Halazzi + if (Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi")) + { + SetRtiTarget(botAI, "star", halazzi); + + if (bot->GetTarget() != halazzi->GetGUID()) + return Attack(halazzi); + } + + // Don't attack the Lynx + return false; +} + +// Hex Lord Malacrass + +bool HexLordMalacrassMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", malacrass)) + return botAI->CastSpell("steady shot", malacrass); + + return false; +} + +bool HexLordMalacrassAssignDpsPriorityAction::Execute(Event /*event*/) +{ + static constexpr uint32 priorityEntries[] = + { + static_cast(ZulAmanNPCs::NPC_LORD_RAADAN), + static_cast(ZulAmanNPCs::NPC_ALYSON_ANTILLE), + static_cast(ZulAmanNPCs::NPC_KORAGG), + static_cast(ZulAmanNPCs::NPC_DARKHEART), + static_cast(ZulAmanNPCs::NPC_FENSTALKER), + static_cast(ZulAmanNPCs::NPC_GAZAKROTH), + static_cast(ZulAmanNPCs::NPC_THURG), + static_cast(ZulAmanNPCs::NPC_SLITHER), + static_cast(ZulAmanNPCs::NPC_HEX_LORD_MALACRASS) + }; + + auto const& targets = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + + Unit* priorityTarget = nullptr; + + for (uint32 entry : priorityEntries) + { + for (auto const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + { + priorityTarget = unit; + break; + } + } + + if (priorityTarget) + break; + } + + if (priorityTarget) + { + MarkTargetWithSkull(bot, priorityTarget); + SetRtiTarget(botAI, "skull", priorityTarget); + } + + return false; +} + +bool HexLordMalacrassRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + if (Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass")) + { + float currentDistance = bot->GetDistance2d(malacrass); + constexpr float safeDistance = 9.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(malacrass, safeDistance - currentDistance); + } + } + + return false; +} + +bool HexLordMalacrassCastersStopAttackingAction::Execute(Event /*event*/) +{ + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_SPELL_REFLECTION))) + return false; + + if (bot->GetVictim() == malacrass) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return true; + } + + return false; +} + +bool HexLordMalacrassMoveAwayFromFreezingTrapAction::Execute(Event /*event*/) +{ + GameObject* trapGo = bot->FindNearestGameObject( + static_cast(ZulAmanObjects::GO_FREEZING_TRAP), 20.0f, true); + + if (!trapGo) + return false; + + float currentDistance = bot->GetDistance2d(trapGo); + constexpr float safeDistance = 6.0f; + constexpr uint32 minInterval = 0; + if (currentDistance < safeDistance) + return FleePosition(trapGo->GetPosition(), safeDistance, minInterval); + + return false; +} + +// Zul'jin + +bool ZuljinMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", zuljin)) + return botAI->CastSpell("steady shot", zuljin); + + return false; +} + +bool ZuljinTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin) + return false; + + if (bot->GetVictim() != zuljin) + return Attack(zuljin); + + if (zuljin->GetVictim() == bot) + { + const Position& position = ZULJIN_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool ZuljinRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + if (Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin")) + { + float currentDistance = bot->GetExactDist2d(zuljin); + constexpr float safeDistance = 10.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(zuljin, safeDistance - currentDistance); + } + } + + return false; +} + +bool ZuljinAvoidCyclonesAction::Execute(Event /*event*/) +{ + auto const& cyclones = GetAllHazardTriggers( + bot, static_cast(ZulAmanNPCs::NPC_FEATHER_VORTEX), 50.0f); + + if (cyclones.empty()) + return false; + + constexpr float hazardRadius = 6.0f; + bool inDanger = false; + for (Unit* cyclone : cyclones) + { + if (bot->GetDistance2d(cyclone) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + const Position& zuljinCenter = ZULJIN_TANK_POSITION; + constexpr float safeZoneRadius = 30.0f; + + Position safestPos = + FindSafestNearbyPosition(bot, cyclones, zuljinCenter, safeZoneRadius, hazardRadius, true); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(ZULAMAN_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool ZuljinSpreadRangedAction::Execute(Event /*event*/) +{ + constexpr float minDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance)) + return FleePosition(nearestPlayer->GetPosition(), minDistance, minInterval); + + return false; +} diff --git a/src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.h b/src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.h new file mode 100644 index 00000000000..5542e8f3812 --- /dev/null +++ b/src/Ai/Raid/ZulAman/Action/RaidZulAmanActions.h @@ -0,0 +1,253 @@ +/* + * 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_RAIDZULAMANACTIONS_H +#define _PLAYERBOT_RAIDZULAMANACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +// Trash + +class AmanishiMedicineManMarkWardAction : public Action +{ +public: + AmanishiMedicineManMarkWardAction( + PlayerbotAI* botAI, std::string const name = "amani'shi medicine man mark ward") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Akil'zon + +class AkilzonMisdirectBossToMainTankAction : public AttackAction +{ +public: + AkilzonMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "akil'zon misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonTanksPositionBossAction : public AttackAction +{ +public: + AkilzonTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "akil'zon tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonSpreadRangedAction : public MovementAction +{ +public: + AkilzonSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "akil'zon spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonMoveToEyeOfTheStormAction : public MovementAction +{ +public: + AkilzonMoveToEyeOfTheStormAction( + PlayerbotAI* botAI, std::string const name = "akil'zon move to eye of the storm") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonManageElectricalStormTimerAction : public Action +{ +public: + AkilzonManageElectricalStormTimerAction( + PlayerbotAI* botAI, std::string const name = "akil'zon manage electrical storm timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Nalorakk + +class NalorakkMisdirectBossToMainTankAction : public AttackAction +{ +public: + NalorakkMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "nalorakk misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class NalorakkTanksPositionBossAction : public AttackAction +{ +public: + NalorakkTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "nalorakk tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool MainTankPositionTrollForm(Unit* nalorakk); + bool FirstAssistTankPositionBearForm(Unit* nalorakk); +}; + +class NalorakkSpreadRangedAction : public MovementAction +{ +public: + NalorakkSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "nalorakk spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Jan'alai + +class JanalaiMisdirectBossToMainTankAction : public AttackAction +{ +public: + JanalaiMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "jan'alai misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiTanksPositionBossAction : public AttackAction +{ +public: + JanalaiTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "jan'alai tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiSpreadRangedInCircleAction : public MovementAction +{ +public: + JanalaiSpreadRangedInCircleAction( + PlayerbotAI* botAI, std::string const name = "jan'alai spread ranged in circle") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiAvoidFireBombsAction : public MovementAction +{ +public: + JanalaiAvoidFireBombsAction(PlayerbotAI* botAI, std::string const name = "jan'alai avoid fire bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiMarkAmanishiHatchersAction : public Action +{ +public: + JanalaiMarkAmanishiHatchersAction( + PlayerbotAI* botAI, std::string const name = "jan'alai mark amani'shi hatchers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Halazzi + +class HalazziMisdirectBossToMainTankAction : public AttackAction +{ +public: + HalazziMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "halazzi misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HalazziMainTankPositionBossAction : public AttackAction +{ +public: + HalazziMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "halazzi main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HalazziFirstAssistTankAttackSpiritLynxAction : public AttackAction +{ +public: + HalazziFirstAssistTankAttackSpiritLynxAction( + PlayerbotAI* botAI, std::string const name = "halazzi first assist tank attack spirit lynx") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HalazziAssignDpsPriorityAction : public AttackAction +{ +public: + HalazziAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "halazzi assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Hex Lord Malacrass + +class HexLordMalacrassMisdirectBossToMainTankAction : public AttackAction +{ +public: + HexLordMalacrassMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassAssignDpsPriorityAction : public AttackAction +{ +public: + HexLordMalacrassAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassRunAwayFromWhirlwindAction : public MovementAction +{ +public: + HexLordMalacrassRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass run away from whirlwind") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassCastersStopAttackingAction : public Action +{ +public: + HexLordMalacrassCastersStopAttackingAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass casters stop attacking") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassMoveAwayFromFreezingTrapAction : public MovementAction +{ +public: + HexLordMalacrassMoveAwayFromFreezingTrapAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass move away from freezing trap") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Zul'jin + +class ZuljinMisdirectBossToMainTankAction : public AttackAction +{ +public: + ZuljinMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "zul'jin misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinTanksPositionBossAction : public AttackAction +{ +public: + ZuljinTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "zul'jin tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinRunAwayFromWhirlwindAction : public MovementAction +{ +public: + ZuljinRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "zul'jin run away from whirlwind") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinAvoidCyclonesAction : public MovementAction +{ +public: + ZuljinAvoidCyclonesAction(PlayerbotAI* botAI, std::string const name = "zul'jin avoid cyclones") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinSpreadRangedAction : public MovementAction +{ +public: + ZuljinSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "zul'jin spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp b/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp new file mode 100644 index 00000000000..e7d16920fc9 --- /dev/null +++ b/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp @@ -0,0 +1,382 @@ +/* + * 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 "RaidZulAmanMultipliers.h" +#include "RaidZulAmanActions.h" +#include "RaidZulAmanHelpers.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidBearActions.h" +#include "FollowActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "PriestActions.h" +#include "RaidBossHelpers.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +using namespace ZulAmanHelpers; + +// Akil'zon + +float AkilzonDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "akil'zon")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AkilzonStayInEyeOfTheStormMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "akil'zon") /* || + !GetElectricalStormTarget(bot)*/) + return 1.0f; + + auto it = akilzonStormTimer.find(bot->GetMap()->GetInstanceId()); + if (it == akilzonStormTimer.end() || + !IsInStormWindow(it->second, std::time(nullptr))) + return 1.0f; + + 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)) + return 0.0f; + + return 1.0f; +} + +// Nalorakk + +float NalorakkDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + if (!nalorakk) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + bool shouldTankBoss = false; + + if (botAI->IsMainTank(bot) && + !nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + shouldTankBoss = true; + + if (botAI->IsAssistTankOfIndex(bot, 0, true) && + nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + shouldTankBoss = true; + + if (!shouldTankBoss && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float NalorakkControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "nalorakk")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Jan'alai + +float JanalaiDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (botAI->IsMainTank(bot) && + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsAssistTank(bot) && + !GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_AMANI_DRAGONHAWK_HATCHLING)) && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiStayAwayFromFireBombsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (!HasFireBombNearby(botAI, bot)) + return 1.0f; + + if (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 JanalaiDoNotCrowdControlHatchersMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "amani'shi hatcher")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "amani dragonhawk hatchling")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Halazzi + +float HalazziDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "halazzi")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() != nullptr && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float HalazziControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "halazzi")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Hex Lord Malacrass + +float HexLordMalacrassAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot)) + return 1.0f; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_WHIRLWIND))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier::GetValue(Action* action) +{ + if (!botAI->IsCaster(bot)) + return 1.0f; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_SPELL_REFLECTION))) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (!castSpellAction) + return 1.0f; + + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe || + (bot->GetVictim() == malacrass && + castSpellAction->getThreatType() == Action::ActionThreatType::Single)) + return 0.0f; + + return 1.0f; +} + +float HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_PRIEST && + bot->getClass() != CLASS_PALADIN && + bot->getClass() != CLASS_WARLOCK) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "hex lord malacrass")) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + bool hasUnstableAffliction = false; + for (GroupReference* ref = bot->GetGroup()->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(static_cast(ZulAmanSpells::SPELL_UNSTABLE_AFFLICTION))) + { + hasUnstableAffliction = true; + break; + } + } + + if (!hasUnstableAffliction) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Zul'jin + +float ZuljinDisableTankFaceMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK))) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float ZuljinAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot)) + return 1.0f; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_ZULJIN_WHIRLWIND))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float ZuljinDisableAvoidAoeMultiplier::GetValue(Action* action) +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE))) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float ZuljinDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.h b/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.h new file mode 100644 index 00000000000..c3bdffbe107 --- /dev/null +++ b/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.h @@ -0,0 +1,167 @@ +/* + * 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_RAIDZULAMANMULTIPLIERS_H +#define _PLAYERBOT_RAIDZULAMANMULTIPLIERS_H + +#include "Multiplier.h" + +// Akil'zon + +class AkilzonDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + AkilzonDisableCombatFormationMoveMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "akil'zon disable combat formation move") {} + virtual float GetValue(Action* action); +}; + +class AkilzonStayInEyeOfTheStormMultiplier : public Multiplier +{ +public: + AkilzonStayInEyeOfTheStormMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "akil'zon stay in eye of the storm") {} + virtual float GetValue(Action* action); +}; + +// Nalorakk + +class NalorakkDisableTankActionsMultiplier : public Multiplier +{ +public: + NalorakkDisableTankActionsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "nalorakk disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class NalorakkControlMisdirectionMultiplier : public Multiplier +{ +public: + NalorakkControlMisdirectionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "nalorakk control misdirection") {} + virtual float GetValue(Action* action); +}; + +// Jan'alai + +class JanalaiDisableTankActionsMultiplier : public Multiplier +{ +public: + JanalaiDisableTankActionsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class JanalaiDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + JanalaiDisableCombatFormationMoveMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai disable combat formation move") {} + virtual float GetValue(Action* action); +}; + +class JanalaiStayAwayFromFireBombsMultiplier : public Multiplier +{ +public: + JanalaiStayAwayFromFireBombsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai stay away from fire bombs") {} + virtual float GetValue(Action* action); +}; + +class JanalaiDoNotCrowdControlHatchersMultiplier : public Multiplier +{ +public: + JanalaiDoNotCrowdControlHatchersMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai do not crowd control hatchers") {} + virtual float GetValue(Action* action); +}; + +class JanalaiDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + JanalaiDelayBloodlustAndHeroismMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +// Halazzi + +class HalazziDisableTankActionsMultiplier : public Multiplier +{ +public: + HalazziDisableTankActionsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "halazzi disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class HalazziControlMisdirectionMultiplier : public Multiplier +{ +public: + HalazziControlMisdirectionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "halazzi control misdirection") {} + virtual float GetValue(Action* action); +}; + +// Hex Lord Malacrass + +class HexLordMalacrassAvoidWhirlwindMultiplier : public Multiplier +{ +public: + HexLordMalacrassAvoidWhirlwindMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "hex lord malacrass avoid whirlwind") {} + virtual float GetValue(Action* action); +}; + +class HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier : public Multiplier +{ +public: + HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "hex lord malacrass do not dispel unstable affliction") {} + virtual float GetValue(Action* action); +}; + +class HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier : public Multiplier +{ +public: + HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "hex lord malacrass stop attacking during spell reflection") {} + virtual float GetValue(Action* action); +}; + +// Zul'jin + +class ZuljinDisableTankFaceMultiplier : public Multiplier +{ +public: + ZuljinDisableTankFaceMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin disable tank face") {} + virtual float GetValue(Action* action); +}; + +class ZuljinAvoidWhirlwindMultiplier : public Multiplier +{ +public: + ZuljinAvoidWhirlwindMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin avoid whirlwind") {} + virtual float GetValue(Action* action); +}; + +class ZuljinDisableAvoidAoeMultiplier : public Multiplier +{ +public: + ZuljinDisableAvoidAoeMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin disable avoid aoe") {} + virtual float GetValue(Action* action); +}; + +class ZuljinDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + ZuljinDelayBloodlustAndHeroismMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/ZulAman/RaidZulAmanActionContext.h b/src/Ai/Raid/ZulAman/RaidZulAmanActionContext.h new file mode 100644 index 00000000000..852a180b321 --- /dev/null +++ b/src/Ai/Raid/ZulAman/RaidZulAmanActionContext.h @@ -0,0 +1,202 @@ +/* + * 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_RAIDZULAMANACTIONCONTEXT_H +#define _PLAYERBOT_RAIDZULAMANACTIONCONTEXT_H + +#include "RaidZulAmanActions.h" +#include "NamedObjectContext.h" + +class RaidZulAmanActionContext : public NamedObjectContext +{ +public: + RaidZulAmanActionContext() + { + // Trash + creators["amani'shi medicine man mark ward"] = + &RaidZulAmanActionContext::amanishi_medicine_man_mark_ward; + + // Akil'zon + creators["akil'zon misdirect boss to main tank"] = + &RaidZulAmanActionContext::akilzon_misdirect_boss_to_main_tank; + + creators["akil'zon tanks position boss"] = + &RaidZulAmanActionContext::akilzon_tanks_position_boss; + + creators["akil'zon spread ranged"] = + &RaidZulAmanActionContext::akilzon_spread_ranged; + + creators["akil'zon move to eye of the storm"] = + &RaidZulAmanActionContext::akilzon_move_to_eye_of_the_storm; + + creators["akil'zon manage electrical storm timer"] = + &RaidZulAmanActionContext::akilzon_manage_electrical_storm_timer; + + // Nalorakk + creators["nalorakk misdirect boss to main tank"] = + &RaidZulAmanActionContext::nalorakk_misdirect_boss_to_main_tank; + + creators["nalorakk tanks position boss"] = + &RaidZulAmanActionContext::nalorakk_tanks_position_boss; + + creators["nalorakk spread ranged"] = + &RaidZulAmanActionContext::nalorakk_spread_ranged; + + // Jan'alai + creators["jan'alai misdirect boss to main tank"] = + &RaidZulAmanActionContext::janalai_misdirect_boss_to_main_tank; + + creators["jan'alai tanks position boss"] = + &RaidZulAmanActionContext::janalai_tanks_position_boss; + + creators["jan'alai spread ranged in circle"] = + &RaidZulAmanActionContext::janalai_spread_ranged_in_circle; + + creators["jan'alai avoid fire bombs"] = + &RaidZulAmanActionContext::janalai_avoid_fire_bombs; + + creators["jan'alai mark amani'shi hatchers"] = + &RaidZulAmanActionContext::janalai_mark_amanishi_hatchers; + + // Halazzi + creators["halazzi misdirect boss to main tank"] = + &RaidZulAmanActionContext::halazzi_misdirect_boss_to_main_tank; + + creators["halazzi main tank position boss"] = + &RaidZulAmanActionContext::halazzi_main_tank_position_boss; + + creators["halazzi first assist tank attack spirit lynx"] = + &RaidZulAmanActionContext::halazzi_first_assist_tank_attack_spirit_lynx; + + creators["halazzi assign dps priority"] = + &RaidZulAmanActionContext::halazzi_assign_dps_priority; + + // Hex Lord Malacrass + creators["hex lord malacrass misdirect boss to main tank"] = + &RaidZulAmanActionContext::hex_lord_malacrass_misdirect_boss_to_main_tank; + + creators["hex lord malacrass assign dps priority"] = + &RaidZulAmanActionContext::hex_lord_malacrass_assign_dps_priority; + + creators["hex lord malacrass run away from whirlwind"] = + &RaidZulAmanActionContext::hex_lord_malacrass_run_away_from_whirlwind; + + creators["hex lord malacrass casters stop attacking"] = + &RaidZulAmanActionContext::hex_lord_malacrass_casters_stop_attacking; + + creators["hex lord malacrass move away from freezing trap"] = + &RaidZulAmanActionContext::hex_lord_malacrass_move_away_from_freezing_trap; + + // Zul'jin + creators["zul'jin misdirect boss to main tank"] = + &RaidZulAmanActionContext::zuljin_misdirect_boss_to_main_tank; + + creators["zul'jin tanks position boss"] = + &RaidZulAmanActionContext::zuljin_tanks_position_boss; + + creators["zul'jin run away from whirlwind"] = + &RaidZulAmanActionContext::zuljin_run_away_from_whirlwind; + + creators["zul'jin avoid cyclones"] = + &RaidZulAmanActionContext::zuljin_avoid_cyclones; + + creators["zul'jin spread ranged"] = + &RaidZulAmanActionContext::zuljin_spread_ranged; + } + +private: + // Trash + static Action* amanishi_medicine_man_mark_ward( + PlayerbotAI* botAI) { return new AmanishiMedicineManMarkWardAction(botAI); } + + // Akil'zon + static Action* akilzon_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new AkilzonMisdirectBossToMainTankAction(botAI); } + + static Action* akilzon_tanks_position_boss( + PlayerbotAI* botAI) { return new AkilzonTanksPositionBossAction(botAI); } + + static Action* akilzon_spread_ranged( + PlayerbotAI* botAI) { return new AkilzonSpreadRangedAction(botAI); } + + static Action* akilzon_move_to_eye_of_the_storm( + PlayerbotAI* botAI) { return new AkilzonMoveToEyeOfTheStormAction(botAI); } + + static Action* akilzon_manage_electrical_storm_timer( + PlayerbotAI* botAI) { return new AkilzonManageElectricalStormTimerAction(botAI); } + + // Nalorakk + static Action* nalorakk_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new NalorakkMisdirectBossToMainTankAction(botAI); } + + static Action* nalorakk_tanks_position_boss( + PlayerbotAI* botAI) { return new NalorakkTanksPositionBossAction(botAI); } + + static Action* nalorakk_spread_ranged( + PlayerbotAI* botAI) { return new NalorakkSpreadRangedAction(botAI); } + + // Jan'alai + static Action* janalai_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new JanalaiMisdirectBossToMainTankAction(botAI); } + + static Action* janalai_tanks_position_boss( + PlayerbotAI* botAI) { return new JanalaiTanksPositionBossAction(botAI); } + + static Action* janalai_spread_ranged_in_circle( + PlayerbotAI* botAI) { return new JanalaiSpreadRangedInCircleAction(botAI); } + + static Action* janalai_avoid_fire_bombs( + PlayerbotAI* botAI) { return new JanalaiAvoidFireBombsAction(botAI); } + + static Action* janalai_mark_amanishi_hatchers( + PlayerbotAI* botAI) { return new JanalaiMarkAmanishiHatchersAction(botAI); } + + // Halazzi + static Action* halazzi_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new HalazziMisdirectBossToMainTankAction(botAI); } + + static Action* halazzi_main_tank_position_boss( + PlayerbotAI* botAI) { return new HalazziMainTankPositionBossAction(botAI); } + + static Action* halazzi_first_assist_tank_attack_spirit_lynx( + PlayerbotAI* botAI) { return new HalazziFirstAssistTankAttackSpiritLynxAction(botAI); } + + static Action* halazzi_assign_dps_priority( + PlayerbotAI* botAI) { return new HalazziAssignDpsPriorityAction(botAI); } + + // Hex Lord Malacrass + static Action* hex_lord_malacrass_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new HexLordMalacrassMisdirectBossToMainTankAction(botAI); } + + static Action* hex_lord_malacrass_assign_dps_priority( + PlayerbotAI* botAI) { return new HexLordMalacrassAssignDpsPriorityAction(botAI); } + + static Action* hex_lord_malacrass_run_away_from_whirlwind( + PlayerbotAI* botAI) { return new HexLordMalacrassRunAwayFromWhirlwindAction(botAI); } + + static Action* hex_lord_malacrass_casters_stop_attacking( + PlayerbotAI* botAI) { return new HexLordMalacrassCastersStopAttackingAction(botAI); } + + static Action* hex_lord_malacrass_move_away_from_freezing_trap( + PlayerbotAI* botAI) { return new HexLordMalacrassMoveAwayFromFreezingTrapAction(botAI); } + + // Zul'jin + static Action* zuljin_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new ZuljinMisdirectBossToMainTankAction(botAI); } + + static Action* zuljin_tanks_position_boss( + PlayerbotAI* botAI) { return new ZuljinTanksPositionBossAction(botAI); } + + static Action* zuljin_run_away_from_whirlwind( + PlayerbotAI* botAI) { return new ZuljinRunAwayFromWhirlwindAction(botAI); } + + static Action* zuljin_avoid_cyclones( + PlayerbotAI* botAI) { return new ZuljinAvoidCyclonesAction(botAI); } + + static Action* zuljin_spread_ranged( + PlayerbotAI* botAI) { return new ZuljinSpreadRangedAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h b/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h new file mode 100644 index 00000000000..5be8bad7f08 --- /dev/null +++ b/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h @@ -0,0 +1,206 @@ +/* + * 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_RAIDZULAMANTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDZULAMANTRIGGERCONTEXT_H + +#include "RaidZulAmanTriggers.h" +#include "AiObjectContext.h" + +class RaidZulAmanTriggerContext : public NamedObjectContext +{ +public: + RaidZulAmanTriggerContext() + { + // Trash + creators["amani'shi medicine man summoned ward"] = + &RaidZulAmanTriggerContext::amanishi_medicine_man_summoned_ward; + + // Akil'zon + creators["akil'zon pulling boss"] = + &RaidZulAmanTriggerContext::akilzon_pulling_boss; + + creators["akil'zon boss engaged by tanks"] = + &RaidZulAmanTriggerContext::akilzon_boss_engaged_by_tanks; + + creators["akil'zon boss casts static disruption"] = + &RaidZulAmanTriggerContext::akilzon_boss_casts_static_disruption; + + creators["akil'zon electrical storm incoming"] = + &RaidZulAmanTriggerContext::akilzon_electrical_storm_incoming; + + creators["akil'zon bots need to prepare for electrical storm"] = + &RaidZulAmanTriggerContext::akilzon_bots_need_to_prepare_for_electrical_storm; + + // Nalorakk + creators["nalorakk pulling boss"] = + &RaidZulAmanTriggerContext::nalorakk_pulling_boss; + + creators["nalorakk boss casts surge"] = + &RaidZulAmanTriggerContext::nalorakk_boss_casts_surge; + + creators["nalorakk boss switches forms"] = + &RaidZulAmanTriggerContext::nalorakk_boss_switches_forms; + + // Jan'alai + creators["jan'alai pulling boss"] = + &RaidZulAmanTriggerContext::janalai_pulling_boss; + + creators["jan'alai boss engaged by tanks"] = + &RaidZulAmanTriggerContext::janalai_boss_engaged_by_tanks; + + creators["jan'alai boss casts flame breath"] = + &RaidZulAmanTriggerContext::janalai_boss_casts_flame_breath; + + creators["jan'alai boss summoning fire bombs"] = + &RaidZulAmanTriggerContext::janalai_boss_summoning_fire_bombs; + + creators["jan'alai amani'shi hatchers spawned"] = + &RaidZulAmanTriggerContext::janalai_amanishi_hatchers_spawned; + + // Halazzi + creators["halazzi pulling boss"] = + &RaidZulAmanTriggerContext::halazzi_pulling_boss; + + creators["halazzi boss engaged by main tank"] = + &RaidZulAmanTriggerContext::halazzi_boss_engaged_by_main_tank; + + creators["halazzi boss summons spirit lynx"] = + &RaidZulAmanTriggerContext::halazzi_boss_summons_spirit_lynx; + + creators["halazzi determining dps target"] = + &RaidZulAmanTriggerContext::halazzi_determining_dps_target; + + // Hex Lord Malacrass + + creators["hex lord malacrass pulling boss"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_pulling_boss; + + creators["hex lord malacrass determining kill order"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_determining_kill_order; + + creators["hex lord malacrass boss is channeling whirlwind"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_boss_is_channeling_whirlwind; + + creators["hex lord malacrass boss has spell reflection"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_boss_has_spell_reflection; + + creators["hex lord malacrass boss placed freezing trap"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_boss_placed_freezing_trap; + + // Zul'jin + + creators["zul'jin main tank needs aggro upon pull or phase change"] = + &RaidZulAmanTriggerContext::zuljin_main_tank_needs_aggro_upon_pull_or_phase_change; + + creators["zul'jin boss engaged by tanks"] = + &RaidZulAmanTriggerContext::zuljin_boss_engaged_by_tanks; + + creators["zul'jin boss is channeling whirlwind in troll form"] = + &RaidZulAmanTriggerContext::zuljin_boss_is_channeling_whirlwind_in_troll_form; + + creators["zul'jin boss is summoning cyclones in eagle form"] = + &RaidZulAmanTriggerContext::zuljin_boss_is_summoning_cyclones_in_eagle_form; + + creators["zul'jin boss casts aoe abilities in dragonhawk form"] = + &RaidZulAmanTriggerContext::zuljin_boss_casts_aoe_abilities_in_dragonhawk_form; + } + +private: + // Trash + static Trigger* amanishi_medicine_man_summoned_ward( + PlayerbotAI* botAI) { return new AmanishiMedicineManSummonedWardTrigger(botAI); } + + // Akil'zon + static Trigger* akilzon_pulling_boss( + PlayerbotAI* botAI) { return new AkilzonPullingBossTrigger(botAI); } + + static Trigger* akilzon_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new AkilzonBossEngagedByTanksTrigger(botAI); } + + static Trigger* akilzon_boss_casts_static_disruption( + PlayerbotAI* botAI) { return new AkilzonBossCastsStaticDisruptionTrigger(botAI); } + + static Trigger* akilzon_electrical_storm_incoming( + PlayerbotAI* botAI) { return new AkilzonElectricalStormIncomingTrigger(botAI); } + + static Trigger* akilzon_bots_need_to_prepare_for_electrical_storm( + PlayerbotAI* botAI) { return new AkilzonBotsNeedToPrepareForElectricalStormTrigger(botAI); } + + // Nalorakk + static Trigger* nalorakk_pulling_boss( + PlayerbotAI* botAI) { return new NalorakkPullingBossTrigger(botAI); } + + static Trigger* nalorakk_boss_casts_surge( + PlayerbotAI* botAI) { return new NalorakkBossCastsSurgeTrigger(botAI); } + + static Trigger* nalorakk_boss_switches_forms( + PlayerbotAI* botAI) { return new NalorakkBossSwitchesFormsTrigger(botAI); } + + // Jan'alai + static Trigger* janalai_pulling_boss( + PlayerbotAI* botAI) { return new JanalaiPullingBossTrigger(botAI); } + + static Trigger* janalai_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new JanalaiBossEngagedByTanksTrigger(botAI); } + + static Trigger* janalai_boss_casts_flame_breath( + PlayerbotAI* botAI) { return new JanalaiBossCastsFlameBreathTrigger(botAI); } + + static Trigger* janalai_boss_summoning_fire_bombs( + PlayerbotAI* botAI) { return new JanalaiBossSummoningFireBombsTrigger(botAI); } + + static Trigger* janalai_amanishi_hatchers_spawned( + PlayerbotAI* botAI) { return new JanalaiAmanishiHatchersSpawnedTrigger(botAI); } + + // Halazzi + static Trigger* halazzi_pulling_boss( + PlayerbotAI* botAI) { return new HalazziPullingBossTrigger(botAI); } + + static Trigger* halazzi_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new HalazziBossEngagedByMainTankTrigger(botAI); } + + static Trigger* halazzi_boss_summons_spirit_lynx( + PlayerbotAI* botAI) { return new HalazziBossSummonsSpiritLynxTrigger(botAI); } + + static Trigger* halazzi_determining_dps_target( + PlayerbotAI* botAI) { return new HalazziDeterminingDpsTargetTrigger(botAI); } + + // Hex Lord Malacrass + + static Trigger* hex_lord_malacrass_pulling_boss( + PlayerbotAI* botAI) { return new HexLordMalacrassPullingBossTrigger(botAI); } + + static Trigger* hex_lord_malacrass_determining_kill_order( + PlayerbotAI* botAI) { return new HexLordMalacrassDeterminingKillOrderTrigger(botAI); } + + static Trigger* hex_lord_malacrass_boss_is_channeling_whirlwind( + PlayerbotAI* botAI) { return new HexLordMalacrassBossIsChannelingWhirlwindTrigger(botAI); } + + static Trigger* hex_lord_malacrass_boss_has_spell_reflection( + PlayerbotAI* botAI) { return new HexLordMalacrassBossHasSpellReflectionTrigger(botAI); } + + static Trigger* hex_lord_malacrass_boss_placed_freezing_trap( + PlayerbotAI* botAI) { return new HexLordMalacrassBossPlacedFreezingTrapTrigger(botAI); } + + // Zul'jin + + static Trigger* zuljin_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new ZuljinBossEngagedByTanksTrigger(botAI); } + + static Trigger* zuljin_main_tank_needs_aggro_upon_pull_or_phase_change( + PlayerbotAI* botAI) { return new ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger(botAI); } + + static Trigger* zuljin_boss_is_channeling_whirlwind_in_troll_form( + PlayerbotAI* botAI) { return new ZuljinBossIsChannelingWhirlwindInTrollFormTrigger(botAI); } + + static Trigger* zuljin_boss_is_summoning_cyclones_in_eagle_form( + PlayerbotAI* botAI) { return new ZuljinBossIsSummoningCyclonesInEagleFormTrigger(botAI); } + + static Trigger* zuljin_boss_casts_aoe_abilities_in_dragonhawk_form( + PlayerbotAI* botAI) { return new ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.cpp b/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.cpp new file mode 100644 index 00000000000..4ba01a2a14a --- /dev/null +++ b/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.cpp @@ -0,0 +1,134 @@ +/* + * 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 "RaidZulAmanStrategy.h" +#include "RaidZulAmanMultipliers.h" + +void RaidZulAmanStrategy::InitTriggers(std::vector& triggers) +{ + // Trash + triggers.push_back(new TriggerNode("amani'shi medicine man summoned ward", { + NextAction("amani'shi medicine man mark ward", ACTION_RAID + 1) })); + + // Akil'zon + triggers.push_back(new TriggerNode("akil'zon pulling boss", { + NextAction("akil'zon misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("akil'zon boss engaged by main tank", { + NextAction("akil'zon main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("akil'zon boss casts static disruption", { + NextAction("akil'zon spread ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("akil'zon electrical storm incoming", { + NextAction("akil'zon move to eye of the storm", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("akil'zon bots need to prepare for electrical storm", { + NextAction("akil'zon manage electrical storm timer", ACTION_EMERGENCY + 10) })); + + // Nalorakk + triggers.push_back(new TriggerNode("nalorakk pulling boss", { + NextAction("nalorakk misdirect boss to main tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("nalorakk boss switches forms", { + NextAction("nalorakk tanks position boss", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("nalorakk boss casts surge", { + NextAction("nalorakk spread ranged", ACTION_RAID + 1) })); + + // Jan'alai + triggers.push_back(new TriggerNode("jan'alai pulling boss", { + NextAction("jan'alai misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("jan'alai boss engaged by main tank", { + NextAction("jan'alai main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("jan'alai boss casts flame breath", { + NextAction("jan'alai spread ranged in circle", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("jan'alai boss summoning fire bombs", { + NextAction("jan'alai avoid fire bombs", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("jan'alai amani'shi hatchers spawned", { + NextAction("jan'alai mark amani'shi hatchers", ACTION_RAID + 2) })); + + // Halazzi + triggers.push_back(new TriggerNode("halazzi pulling boss", { + NextAction("halazzi misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("halazzi boss engaged by main tank", { + NextAction("halazzi main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("halazzi boss summons spirit lynx", { + NextAction("halazzi first assist tank attack spirit lynx", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("halazzi determining dps target", { + NextAction("halazzi assign dps priority", ACTION_RAID + 1) })); + + // Hex Lord Malacrass + triggers.push_back(new TriggerNode("hex lord malacrass pulling boss", { + NextAction("hex lord malacrass misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("hex lord malacrass determining kill order", { + NextAction("hex lord malacrass assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hex lord malacrass boss is channeling whirlwind", { + NextAction("hex lord malacrass run away from whirlwind", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("hex lord malacrass boss has spell reflection", { + NextAction("hex lord malacrass casters stop attacking", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("hex lord malacrass boss placed freezing trap", { + NextAction("hex lord malacrass move away from freezing trap", ACTION_EMERGENCY + 1) })); + + // Zul'jin + triggers.push_back(new TriggerNode("zul'jin main tank needs aggro upon pull or phase change", { + NextAction("zul'jin misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("zul'jin boss engaged by main tank", { + NextAction("zul'jin main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("zul'jin boss is channeling whirlwind in troll form", { + NextAction("zul'jin run away from whirlwind", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("zul'jin boss is summoning cyclones in eagle form", { + NextAction("zul'jin avoid cyclones", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("zul'jin boss casts aoe abilities in dragonhawk form", { + NextAction("zul'jin spread ranged", ACTION_RAID + 1) })); +} + +void RaidZulAmanStrategy::InitMultipliers(std::vector& multipliers) +{ + // Akil'zon + multipliers.push_back(new AkilzonDisableCombatFormationMoveMultiplier(botAI)); + multipliers.push_back(new AkilzonStayInEyeOfTheStormMultiplier(botAI)); + + // Nalorakk + multipliers.push_back(new NalorakkDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new NalorakkControlMisdirectionMultiplier(botAI)); + + // Jan'alai + multipliers.push_back(new JanalaiDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new JanalaiDisableCombatFormationMoveMultiplier(botAI)); + multipliers.push_back(new JanalaiStayAwayFromFireBombsMultiplier(botAI)); + multipliers.push_back(new JanalaiDoNotCrowdControlHatchersMultiplier(botAI)); + multipliers.push_back(new JanalaiDelayBloodlustAndHeroismMultiplier(botAI)); + + // Halazzi + multipliers.push_back(new HalazziDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new HalazziControlMisdirectionMultiplier(botAI)); + + // Hex Lord Malacrass + multipliers.push_back(new HexLordMalacrassAvoidWhirlwindMultiplier(botAI)); + multipliers.push_back(new HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier(botAI)); + multipliers.push_back(new HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier(botAI)); + + // Zul'jin + multipliers.push_back(new ZuljinDisableTankFaceMultiplier(botAI)); + multipliers.push_back(new ZuljinAvoidWhirlwindMultiplier(botAI)); + multipliers.push_back(new ZuljinDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new ZuljinDelayBloodlustAndHeroismMultiplier(botAI)); +} diff --git a/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h b/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h new file mode 100644 index 00000000000..c49e088886d --- /dev/null +++ b/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h @@ -0,0 +1,23 @@ +/* + * 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_RAIDZULAMANSTRATEGY_H_ +#define _PLAYERBOT_RAIDZULAMANSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidZulAmanStrategy : public Strategy +{ +public: + RaidZulAmanStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "zulaman"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp b/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp new file mode 100644 index 00000000000..ad338a5a7ad --- /dev/null +++ b/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp @@ -0,0 +1,271 @@ +/* + * 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 "RaidZulAmanTriggers.h" +#include "RaidZulAmanHelpers.h" +#include "RaidZulAmanActions.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace ZulAmanHelpers; + +// Trash + +bool AmanishiMedicineManSummonedWardTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "amani'shi medicine man"); +} + +// Akil'zon + +bool AkilzonPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + return akilzon && akilzon->GetHealthPct() > 95.0f; +} + +bool AkilzonBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "akil'zon")) + return false; + + return !GetElectricalStormTarget(bot); +} + +bool AkilzonBossCastsStaticDisruptionTrigger::IsActive() +{ + if (!botAI->IsRanged(bot) || + !AI_VALUE2(Unit*, "find target", "akil'zon")) + return false; + + auto it = akilzonStormTimer.find(bot->GetMap()->GetInstanceId()); + if (it == akilzonStormTimer.end()) + return true; + + return !IsInStormWindow(it->second, std::time(nullptr)); +} + +bool AkilzonElectricalStormIncomingTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "akil'zon")) + return false; + + auto it = akilzonStormTimer.find(bot->GetMap()->GetInstanceId()); + if (it == akilzonStormTimer.end()) + return false; + + return IsInStormWindow(it->second, std::time(nullptr)); +} + +bool AkilzonBotsNeedToPrepareForElectricalStormTrigger::IsActive() +{ + return IsMechanicTrackerBot(botAI, bot, ZULAMAN_MAP_ID); +} + +// Nalorakk + +bool NalorakkPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + return nalorakk && nalorakk->GetHealthPct() > 95.0f; +} + +bool NalorakkBossSwitchesFormsTrigger::IsActive() +{ + return (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true)) && + AI_VALUE2(Unit*, "find target", "nalorakk"); +} + +bool NalorakkBossCastsSurgeTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "nalorakk"); +} + +// Jan'alai + +bool JanalaiPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* janalai = AI_VALUE2(Unit*, "find target", "jan'alai"); + return janalai && janalai->GetHealthPct() > 95.0f; +} + +bool JanalaiBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai")) + return false; + + return !HasFireBombNearby(botAI, bot); +} + +bool JanalaiBossCastsFlameBreathTrigger::IsActive() +{ + if (!botAI->IsRanged(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai") || + AI_VALUE2(Unit*, "find target", "amani dragonhawk hatchling")) + return false; + + return !HasFireBombNearby(botAI, bot); +} + +bool JanalaiBossSummoningFireBombsTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "jan'alai") && + HasFireBombNearby(botAI, bot); +} + +bool JanalaiAmanishiHatchersSpawnedTrigger::IsActive() +{ + if (!botAI->IsRangedDps(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai")) + return false; + + return bot->FindNearestCreature( + static_cast(ZulAmanNPCs::NPC_AMANISHI_HATCHER), 40.0f); +} + +// Halazzi + +bool HalazziPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi"); + return halazzi && halazzi->GetHealthPct() > 95.0f; +} + +bool HalazziBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "halazzi"); +} + +bool HalazziBossSummonsSpiritLynxTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "halazzi"); +} + +bool HalazziDeterminingDpsTargetTrigger::IsActive() +{ + return botAI->IsDps(bot) && + AI_VALUE2(Unit*, "find target", "halazzi"); +} + +// Hex Lord Malacrass + +bool HexLordMalacrassPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + return malacrass && malacrass->GetHealthPct() > 95.0f; +} + +bool HexLordMalacrassDeterminingKillOrderTrigger::IsActive() +{ + return botAI->IsDps(bot) && + AI_VALUE2(Unit*, "find target", "hex lord malacrass"); +} + +bool HexLordMalacrassBossIsChannelingWhirlwindTrigger::IsActive() +{ + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_WHIRLWIND))) + return false; + + return !(botAI->IsTank(bot) && malacrass->GetVictim() == bot); +} + +bool HexLordMalacrassBossHasSpellReflectionTrigger::IsActive() +{ + if (!botAI->IsCaster(bot)) + return false; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + return malacrass && + malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_SPELL_REFLECTION)); +} + +bool HexLordMalacrassBossPlacedFreezingTrapTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hex lord malacrass") && + bot->FindNearestGameObject( + static_cast(ZulAmanObjects::GO_FREEZING_TRAP), 20.0f, true); +} + +// Zul'jin + +bool ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin) + return false; + + float hp = zuljin->GetHealthPct(); + + return (hp <= 100.0f && hp > 95.0f) || + (hp <= 80.0f && hp > 75.0f && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_BEAR))) || + (hp <= 40.0f && hp > 35.0f && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_LYNX))) || + (hp <= 20.0f && hp > 15.0f && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK))); +} + +bool ZuljinBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + return zuljin && + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE)) && + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK)); +} + +bool ZuljinBossIsChannelingWhirlwindInTrollFormTrigger::IsActive() +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_ZULJIN_WHIRLWIND))) + return false; + + return !(botAI->IsTank(bot) && zuljin->GetVictim() == bot); +} + +bool ZuljinBossIsSummoningCyclonesInEagleFormTrigger::IsActive() +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + return zuljin && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE)); +} + +bool ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + return zuljin && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK)); +} diff --git a/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.h b/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.h new file mode 100644 index 00000000000..8d0cc1e54af --- /dev/null +++ b/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.h @@ -0,0 +1,249 @@ +/* + * 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_RAIDZULAMANTRIGGERS_H +#define _PLAYERBOT_RAIDZULAMANTRIGGERS_H + +#include "Trigger.h" + +// Trash + +class AmanishiMedicineManSummonedWardTrigger : public Trigger +{ +public: + AmanishiMedicineManSummonedWardTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "amani'shi medicine man summoned ward") {} + bool IsActive() override; +}; + +// Akil'zon + +class AkilzonPullingBossTrigger : public Trigger +{ +public: + AkilzonPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon pulling boss") {} + bool IsActive() override; +}; + +class AkilzonBossEngagedByTanksTrigger : public Trigger +{ +public: + AkilzonBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon boss engaged by tanks") {} + bool IsActive() override; +}; + +class AkilzonBossCastsStaticDisruptionTrigger : public Trigger +{ +public: + AkilzonBossCastsStaticDisruptionTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon boss casts static disruption") {} + bool IsActive() override; +}; + +class AkilzonElectricalStormIncomingTrigger : public Trigger +{ +public: + AkilzonElectricalStormIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon electrical storm incoming") {} + bool IsActive() override; +}; + +class AkilzonBotsNeedToPrepareForElectricalStormTrigger : public Trigger +{ +public: + AkilzonBotsNeedToPrepareForElectricalStormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon bots need to prepare for electrical storm") {} + bool IsActive() override; +}; + +// Nalorakk + +class NalorakkPullingBossTrigger : public Trigger +{ +public: + NalorakkPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nalorakk pulling boss") {} + bool IsActive() override; +}; + +class NalorakkBossSwitchesFormsTrigger : public Trigger +{ +public: + NalorakkBossSwitchesFormsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nalorakk boss switches forms") {} + bool IsActive() override; +}; + +class NalorakkBossCastsSurgeTrigger : public Trigger +{ +public: + NalorakkBossCastsSurgeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nalorakk boss casts surge") {} + bool IsActive() override; +}; + +// Jan'alai + +class JanalaiPullingBossTrigger : public Trigger +{ +public: + JanalaiPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai pulling boss") {} + bool IsActive() override; +}; + +class JanalaiBossEngagedByTanksTrigger : public Trigger +{ +public: + JanalaiBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai boss engaged by tanks") {} + bool IsActive() override; +}; + +class JanalaiBossCastsFlameBreathTrigger : public Trigger +{ +public: + JanalaiBossCastsFlameBreathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai boss casts flame breath") {} + bool IsActive() override; +}; + +class JanalaiBossSummoningFireBombsTrigger : public Trigger +{ +public: + JanalaiBossSummoningFireBombsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai boss summoning fire bombs") {} + bool IsActive() override; +}; + +class JanalaiAmanishiHatchersSpawnedTrigger : public Trigger +{ +public: + JanalaiAmanishiHatchersSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai amani'shi hatchers spawned") {} + bool IsActive() override; +}; + +// Halazzi + +class HalazziPullingBossTrigger : public Trigger +{ +public: + HalazziPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi pulling boss") {} + bool IsActive() override; +}; + +class HalazziBossEngagedByMainTankTrigger : public Trigger +{ +public: + HalazziBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi boss engaged by main tank") {} + bool IsActive() override; +}; + +class HalazziBossSummonsSpiritLynxTrigger : public Trigger +{ +public: + HalazziBossSummonsSpiritLynxTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi boss summons spirit lynx") {} + bool IsActive() override; +}; + +class HalazziDeterminingDpsTargetTrigger : public Trigger +{ +public: + HalazziDeterminingDpsTargetTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi determining dps target") {} + bool IsActive() override; +}; + +// Hex Lord Malacrass + +class HexLordMalacrassPullingBossTrigger : public Trigger +{ +public: + HexLordMalacrassPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass pulling boss") {} + bool IsActive() override; +}; + +class HexLordMalacrassDeterminingKillOrderTrigger : public Trigger +{ +public: + HexLordMalacrassDeterminingKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass determining kill order") {} + bool IsActive() override; +}; + +class HexLordMalacrassBossIsChannelingWhirlwindTrigger : public Trigger +{ +public: + HexLordMalacrassBossIsChannelingWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass boss is channeling whirlwind") {} + bool IsActive() override; +}; + +class HexLordMalacrassBossHasSpellReflectionTrigger : public Trigger +{ +public: + HexLordMalacrassBossHasSpellReflectionTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass boss has spell reflection") {} + bool IsActive() override; +}; + +class HexLordMalacrassBossPlacedFreezingTrapTrigger : public Trigger +{ +public: + HexLordMalacrassBossPlacedFreezingTrapTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass boss placed freezing trap") {} + bool IsActive() override; +}; + +// Zul'jin + +class ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger : public Trigger +{ +public: + ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin main tank needs aggro upon pull or phase change") {} + bool IsActive() override; +}; + +class ZuljinBossEngagedByTanksTrigger : public Trigger +{ +public: + ZuljinBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss engaged by tanks") {} + bool IsActive() override; +}; + +class ZuljinBossIsChannelingWhirlwindInTrollFormTrigger : public Trigger +{ +public: + ZuljinBossIsChannelingWhirlwindInTrollFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss is channeling whirlwind in troll form") {} + bool IsActive() override; +}; + +class ZuljinBossIsSummoningCyclonesInEagleFormTrigger : public Trigger +{ +public: + ZuljinBossIsSummoningCyclonesInEagleFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss is summoning cyclones in eagle form") {} + bool IsActive() override; +}; + +class ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger : public Trigger +{ +public: +ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss casts aoe abilities in dragonhawk form") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp new file mode 100644 index 00000000000..77c268817b9 --- /dev/null +++ b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp @@ -0,0 +1,190 @@ +/* + * 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 "RaidZulAmanHelpers.h" +#include "Group.h" +#include "Playerbots.h" + +namespace ZulAmanHelpers +{ + // General + Position FindSafestNearbyPosition(Player* bot, + const std::vector& hazards, const Position& safeZoneCenter, + float safeZoneRadius, float hazardRadius, bool requireSafePath) + { + constexpr float searchStep = M_PI / 8.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = 0.0f; + distance <= safeZoneRadius; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + float x = bot->GetPositionX() + distance * std::cos(angle); + float y = bot->GetPositionY() + distance * std::sin(angle); + + if (safeZoneCenter.GetExactDist2d(x, y) > safeZoneRadius) + continue; + + if (!IsPositionSafeFromHazards(x, y, hazards, hazardRadius)) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = true; + if (requireSafePath) + { + pathSafe = + IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, hazardRadius); + if (!pathSafe) + continue; + } + + float moveDistance = bot->GetExactDist2d(x, y); + if (!foundSafe || moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = pathSafe; + } + } + + if (foundSafe) + break; + } + + return bestPos; + } + + bool IsPathSafeFromHazards(const Position& start, const Position& end, + const std::vector& hazards, float hazardRadius) + { + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + if (!IsPositionSafeFromHazards(checkX, checkY, hazards, hazardRadius)) + return false; + } + + return true; + } + + bool IsPositionSafeFromHazards( + float x, float y, const std::vector& hazards, float hazardRadius) + { + for (Unit* hazard : hazards) + { + if (hazard->GetDistance2d(x, y) < hazardRadius) + return false; + } + + return true; + } + + std::vector GetAllHazardTriggers(Player* bot, uint32 entry, float searchRadius) + { + std::vector triggers; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, entry, searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + triggers.push_back(creature); + } + + return triggers; + } + + // Akil'zon + const Position AKILZON_TANK_POSITION = { 378.369f, 1407.718f, 74.797f }; + std::unordered_map akilzonStormTimer; + + bool IsInStormWindow(time_t start, time_t now) + { + time_t elapsed = now - start; + uint32 seconds = elapsed % 60; + return elapsed >= 55 && (seconds >= 55 || seconds < 10); + } + + Player* GetElectricalStormTarget(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && + member->HasAura(static_cast(ZulAmanSpells::SPELL_ELECTRICAL_STORM))) + return member; + } + + return nullptr; + } + + // Nalorakk + const Position NALORAKK_TANK_POSITION = { -80.208f, 1324.530f, 40.942f }; + + // Jan'alai + const Position JANALAI_TANK_POSITION = { -33.873f, 1149.571f, 19.146f }; + + bool HasFireBombNearby(PlayerbotAI* botAI, Player* bot) + { + constexpr float searchRadius = 30.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(ZulAmanNPCs::NPC_FIRE_BOMB), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + return true; + } + + return false; + } + + std::pair GetAmanishiHatcherPair(PlayerbotAI* botAI) + { + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && + unit->GetEntry() == static_cast(ZulAmanNPCs::NPC_AMANISHI_HATCHER)) + { + if (!lowest || unit->GetGUID().GetCounter() < lowest->GetGUID().GetCounter()) + lowest = unit; + + if (!highest || unit->GetGUID().GetCounter() > highest->GetGUID().GetCounter()) + highest = unit; + } + } + + return {lowest, highest}; + } + + // Halazzi + const Position HALAZZI_TANK_POSITION = { 370.733f, 1131.202f, 6.516f }; + + // Zul'jin + const Position ZULJIN_TANK_POSITION = { 120.210f, 705.564f, 45.111f }; +} diff --git a/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h new file mode 100644 index 00000000000..4c27f0238e0 --- /dev/null +++ b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h @@ -0,0 +1,110 @@ +/* + * 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_RAIDZULAMANHELPERS_H_ +#define _PLAYERBOT_RAIDZULAMANHELPERS_H_ + +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace ZulAmanHelpers +{ + enum class ZulAmanSpells : uint32 + { + // Akil'zon + SPELL_ELECTRICAL_STORM = 43648, + + // Nalorakk + SPELL_BEARFORM = 42377, + + // Hex Lord Malacrass + SPELL_HEX_LORD_WHIRLWIND = 43442, + SPELL_HEX_LORD_SPELL_REFLECTION = 43443, + SPELL_UNSTABLE_AFFLICTION = 43522, + + // Zul'jin + SPELL_ZULJIN_WHIRLWIND = 17207, + SPELL_SHAPE_OF_THE_BEAR = 42594, + SPELL_SHAPE_OF_THE_EAGLE = 42606, + SPELL_SHAPE_OF_THE_LYNX = 42607, + SPELL_SHAPE_OF_THE_DRAGONHAWK = 42608, + // SPELL_CLAW_RAGE = 43149, // Would require getting Zul'jin's bossai + + // Hunter + SPELL_MISDIRECTION = 35079, + }; + + enum class ZulAmanNPCs : uint32 + { + // Trash + NPC_AMANI_HEALING_WARD = 23757, + NPC_AMANI_PROTECTIVE_WARD = 23822, + + // Jan'alai + NPC_AMANI_DRAGONHAWK_HATCHLING = 23598, + NPC_AMANISHI_HATCHER = 23818, + NPC_FIRE_BOMB = 23920, + + // Halazzi + NPC_CORRUPTED_LIGHTNING_TOTEM = 24224, + + // Hex Lord Malacrass + NPC_HEX_LORD_MALACRASS = 24239, + NPC_ALYSON_ANTILLE = 24240, + NPC_THURG = 24241, + NPC_SLITHER = 24242, + NPC_LORD_RAADAN = 24243, + NPC_GAZAKROTH = 24244, + NPC_FENSTALKER = 24245, + NPC_DARKHEART = 24246, + NPC_KORAGG = 24247, + + // Zul'jin + NPC_FEATHER_VORTEX = 24136, + }; + + enum class ZulAmanObjects : uint32 + { + GO_FREEZING_TRAP = 186669, + }; + + // General + constexpr uint32 ZULAMAN_MAP_ID = 568; + Position FindSafestNearbyPosition( + Player* bot, const std::vector& hazards, const Position& center, + float safeZoneRadius, float hazardRadius, bool requireSafePath); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, + const std::vector& hazards, float hazardRadius); + bool IsPositionSafeFromHazards( + float x, float y, const std::vector& hazards, float hazardRadius); + std::vector GetAllHazardTriggers( + Player* bot, uint32 entry, float searchRadius); + + // Akil'zon + extern const Position AKILZON_TANK_POSITION; + extern std::unordered_map akilzonStormTimer; + bool IsInStormWindow(time_t start, time_t now); + Player* GetElectricalStormTarget(Player* bot); + + // Nalorakk + extern const Position NALORAKK_TANK_POSITION; + + // Jan'alai + extern const Position JANALAI_TANK_POSITION; + bool HasFireBombNearby(PlayerbotAI* botAI, Player* bot); + std::pair GetAmanishiHatcherPair(PlayerbotAI* botAI); + + // Halazzi + extern const Position HALAZZI_TANK_POSITION; + + // Zul'jin + extern const Position ZULJIN_TANK_POSITION; +} + +#endif diff --git a/src/Bot/Engine/BuildSharedActionContexts.cpp b/src/Bot/Engine/BuildSharedActionContexts.cpp index dc4ccdfe862..8fbb6c13532 100644 --- a/src/Bot/Engine/BuildSharedActionContexts.cpp +++ b/src/Bot/Engine/BuildSharedActionContexts.cpp @@ -11,6 +11,7 @@ #include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" #include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" +#include "Ai/Raid/ZulAman/RaidZulAmanActionContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" @@ -32,6 +33,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList Date: Fri, 27 Mar 2026 13:38:39 -0400 Subject: [PATCH 28/87] Resto druids should always be in tree form (#2192) # Pull Request Resto druids would not go into tree of life form when in combat until certain triggers were hit. They should be in tree of life form all the time. Move tree form actions from the various triggers to default actions instead, with highest priority ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior?| When healer druids enter combat their first priority should be entering tree form. - Describe the **cheapest implementation** that produces an acceptable result? Add tree form action to default healer druid actions instead of triggers. - Describe the **runtime cost** when this logic executes across many bots? nil --- ## How to Test the Changes - Have a resto druid bot with tree of life form talented. - Enter combat - The druid should immediately enter tree of life form ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- src/Ai/Class/Druid/DruidAiObjectContext.cpp | 2 ++ src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp | 8 ++++---- src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp | 4 ++++ src/Ai/Class/Druid/Trigger/DruidTriggers.h | 11 +++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index cc12f009fdd..3a638eedc31 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -112,6 +112,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext creators["mangle (cat)"] = &DruidTriggerFactoryInternal::mangle_cat; creators["ferocious bite time"] = &DruidTriggerFactoryInternal::ferocious_bite_time; creators["hurricane channel check"] = &DruidTriggerFactoryInternal::hurricane_channel_check; + creators["no healer dps strategy"] = &DruidTriggerFactoryInternal::no_healer_dps_strategy; } private: @@ -149,6 +150,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext static Trigger* mangle_cat(PlayerbotAI* ai) { return new MangleCatTrigger(ai); } static Trigger* ferocious_bite_time(PlayerbotAI* ai) { return new FerociousBiteTimeTrigger(ai); } static Trigger* hurricane_channel_check(PlayerbotAI* ai) { return new HurricaneChannelCheckTrigger(ai); } + static Trigger* no_healer_dps_strategy(PlayerbotAI* ai) { return new NoHealerDpsStrategyTrigger(ai); } }; class DruidAiObjectContextInternal : public NamedObjectContext diff --git a/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp index d5ac8fcea5b..36b90a146a2 100644 --- a/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp @@ -149,10 +149,10 @@ void DruidHealerDpsStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("healer should attack", { - NextAction("cancel tree form", ACTION_DEFAULT + 0.3f), - NextAction("moonfire", ACTION_DEFAULT + 0.2f), - NextAction("wrath", ACTION_DEFAULT + 0.1f), - NextAction("starfire", ACTION_DEFAULT), + NextAction("cancel tree form", ACTION_DEFAULT + 0.4f), + NextAction("moonfire", ACTION_DEFAULT + 0.3f), + NextAction("wrath", ACTION_DEFAULT + 0.2f), + NextAction("starfire", ACTION_DEFAULT + 0.1f), })); } diff --git a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp index 5d2c4ce34c1..7529cbb8ea7 100644 --- a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp @@ -33,6 +33,10 @@ void HealDruidStrategy::InitTriggers(std::vector& triggers) { GenericDruidStrategy::InitTriggers(triggers); + // no healer dps strategy + triggers.push_back(new TriggerNode("no healer dps strategy", + { NextAction("tree form", ACTION_DEFAULT) })); + triggers.push_back(new TriggerNode( "party member to heal out of spell range", { NextAction("reach party member to heal", ACTION_CRITICAL_HEAL + 9) })); diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.h b/src/Ai/Class/Druid/Trigger/DruidTriggers.h index 5541cc10e3b..adad0d2a3cf 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.h +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.h @@ -280,4 +280,15 @@ class HurricaneChannelCheckTrigger : public Trigger static const std::set HURRICANE_SPELL_IDS; }; +class NoHealerDpsStrategyTrigger : public Trigger +{ +public: + NoHealerDpsStrategyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no healer dps strategy") {} + + bool IsActive() override + { + return !botAI->HasStrategy("healer dps", BOT_STATE_COMBAT); + } +}; + #endif From bbd9d3e37a61943fc288faf8ae8c0c633b998d8b Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 27 Mar 2026 18:38:46 +0100 Subject: [PATCH 29/87] Wait for attack strategy migration (#2211) ## Pull Request Description Migration of "wait for attack" strategy from cmangos playerbots. Resolves: https://github.com/mod-playerbots/mod-playerbots/issues/990 ## Feature Evaluation Optional strategy for bots which are in party with real player. ## How to Test the Changes - add strategy to bot "nc +wait for attack" and "co +wait for attack" - set time via command "wait for attack time x" where x is time which they wait in seconds (you should get response from bot) - attack any target (for example dummy in main city)(bot should wait with attack) ## Impact Assessment - [ ] No, not at all - [x] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) Performance wise only bots having this optinal strategy have additional cost in multiplier which check every attack action that should be execute. - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [ ] No - [x] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | 1740 | Please provide a time to set (in seconds) 1741 | Please provide valid time to set (in seconds) between 0 and 99 1742 | Wait for attack time set to %new_time seconds ## AI Assistance Was AI assistance used while working on this change? - [ ] No - [x] Yes (**explain below**) Copilot CLI - help with migration ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- ..._00_ai_playerbot_wait_for_attack_texts.sql | 104 +++++++++++ src/Ai/Base/ActionContext.h | 3 + src/Ai/Base/Actions/AttackAction.cpp | 4 +- src/Ai/Base/Actions/WaitForAttackAction.cpp | 166 ++++++++++++++++++ src/Ai/Base/Actions/WaitForAttackAction.h | 31 ++++ src/Ai/Base/ChatActionContext.h | 3 + src/Ai/Base/ChatTriggerContext.h | 2 + .../Strategy/ChatCommandHandlerStrategy.cpp | 1 + .../Base/Strategy/WaitForAttackStrategy.cpp | 93 ++++++++++ src/Ai/Base/Strategy/WaitForAttackStrategy.h | 39 ++++ src/Ai/Base/StrategyContext.h | 3 + src/Ai/Base/Trigger/WaitForAttackTriggers.h | 49 ++++++ src/Ai/Base/TriggerContext.h | 3 + src/Ai/Base/Value/WaitForAttackTimeValue.h | 25 +++ src/Ai/Base/ValueContext.h | 5 + 15 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql create mode 100644 src/Ai/Base/Actions/WaitForAttackAction.cpp create mode 100644 src/Ai/Base/Actions/WaitForAttackAction.h create mode 100644 src/Ai/Base/Strategy/WaitForAttackStrategy.cpp create mode 100644 src/Ai/Base/Strategy/WaitForAttackStrategy.h create mode 100644 src/Ai/Base/Trigger/WaitForAttackTriggers.h create mode 100644 src/Ai/Base/Value/WaitForAttackTimeValue.h diff --git a/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql b/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql new file mode 100644 index 00000000000..3c01179c476 --- /dev/null +++ b/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql @@ -0,0 +1,104 @@ +-- ######################################################### +-- Playerbots - Add texts for SetWaitForAttackTimeAction +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ('wait_for_attack_provide_time', 'wait_for_attack_invalid_time', 'wait_for_attack_time_set'); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ('wait_for_attack_provide_time', 'wait_for_attack_invalid_time', 'wait_for_attack_time_set'); + +-- --------------------------------------------------------- +-- wait_for_attack_provide_time +-- Please provide a time to set (in seconds) +-- --------------------------------------------------------- +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 ( + 1740, + 'wait_for_attack_provide_time', + 'Please provide a time to set (in seconds)', + 0, 0, + -- koKR + '설정할 시간을 입력해 주세요 (초 단위)', + -- frFR + 'Veuillez indiquer un temps à définir (en secondes)', + -- deDE + 'Bitte gib eine Zeit an (in Sekunden)', + -- zhCN + '请提供要设置的时间(以秒为单位)', + -- zhTW + '請提供要設定的時間(以秒為單位)', + -- esES + 'Por favor, indica un tiempo a establecer (en segundos)', + -- esMX + 'Por favor, indica un tiempo a establecer (en segundos)', + -- ruRU + 'Пожалуйста, укажите время (в секундах)'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('wait_for_attack_provide_time', 100); + +-- --------------------------------------------------------- +-- wait_for_attack_invalid_time +-- Please provide valid time to set (in seconds) between 0 and 99 +-- --------------------------------------------------------- +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 ( + 1741, + 'wait_for_attack_invalid_time', + 'Please provide valid time to set (in seconds) between 0 and 99', + 0, 0, + -- koKR + '0에서 99 사이의 유효한 시간을 입력해 주세요 (초 단위)', + -- frFR + 'Veuillez indiquer un temps valide (en secondes) entre 0 et 99', + -- deDE + 'Bitte gib eine gültige Zeit an (in Sekunden) zwischen 0 und 99', + -- zhCN + '请提供有效的时间(以秒为单位),范围为 0 到 99', + -- zhTW + '請提供有效的時間(以秒為單位),範圍為 0 到 99', + -- esES + 'Por favor, indica un tiempo válido (en segundos) entre 0 y 99', + -- esMX + 'Por favor, indica un tiempo válido (en segundos) entre 0 y 99', + -- ruRU + 'Пожалуйста, укажите допустимое время (в секундах) от 0 до 99'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('wait_for_attack_invalid_time', 100); + +-- --------------------------------------------------------- +-- wait_for_attack_time_set +-- Wait for attack time set to %new_time seconds +-- --------------------------------------------------------- +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 ( + 1742, + 'wait_for_attack_time_set', + 'Wait for attack time set to %new_time seconds', + 0, 0, + -- koKR + '공격 대기 시간이 %new_time초로 설정되었습니다', + -- frFR + 'Temps d''attente avant l''attaque défini à %new_time secondes', + -- deDE + 'Wartezeit vor dem Angriff auf %new_time Sekunden gesetzt', + -- zhCN + '等待攻击时间已设置为 %new_time 秒', + -- zhTW + '等待攻擊時間已設定為 %new_time 秒', + -- esES + 'Tiempo de espera para atacar establecido en %new_time segundos', + -- esMX + 'Tiempo de espera para atacar establecido en %new_time segundos', + -- ruRU + 'Время ожидания атаки установлено на %new_time секунд'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('wait_for_attack_time_set', 100); diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 55081b443ca..256c0de3882 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -65,6 +65,7 @@ #include "NewRpgAction.h" #include "FishingAction.h" #include "CancelChannelAction.h" +#include "WaitForAttackAction.h" class PlayerbotAI; @@ -263,6 +264,7 @@ class ActionContext : public NamedObjectContext creators["new rpg wander npc"] = &ActionContext::new_rpg_wander_npc; creators["new rpg do quest"] = &ActionContext::new_rpg_do_quest; creators["new rpg travel flight"] = &ActionContext::new_rpg_travel_flight; + creators["wait for attack keep safe distance"] = &ActionContext::wait_for_attack_keep_safe_distance; } private: @@ -458,6 +460,7 @@ class ActionContext : public NamedObjectContext static Action* new_rpg_wander_npc(PlayerbotAI* ai) { return new NewRpgWanderNpcAction(ai); } static Action* new_rpg_do_quest(PlayerbotAI* ai) { return new NewRpgDoQuestAction(ai); } static Action* new_rpg_travel_flight(PlayerbotAI* ai) { return new NewRpgTravelFlightAction(ai); } + static Action* wait_for_attack_keep_safe_distance(PlayerbotAI* ai) { return new WaitForAttackKeepSafeDistanceAction(ai); } }; #endif diff --git a/src/Ai/Base/Actions/AttackAction.cpp b/src/Ai/Base/Actions/AttackAction.cpp index 87bbd31c6ba..96bf5c4d327 100644 --- a/src/Ai/Base/Actions/AttackAction.cpp +++ b/src/Ai/Base/Actions/AttackAction.cpp @@ -14,6 +14,7 @@ #include "ServerFacade.h" #include "SharedDefines.h" #include "Unit.h" +#include "WaitForAttackStrategy.h" bool AttackAction::Execute(Event /*event*/) { @@ -164,7 +165,8 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) botAI->ChangeEngine(BOT_STATE_COMBAT); - bot->Attack(target, shouldMelee); + if (!WaitForAttackStrategy::ShouldWait(botAI)) + bot->Attack(target, shouldMelee); /* prevent pet dead immediately in group */ // if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat()) // { diff --git a/src/Ai/Base/Actions/WaitForAttackAction.cpp b/src/Ai/Base/Actions/WaitForAttackAction.cpp new file mode 100644 index 00000000000..4fb8918c62f --- /dev/null +++ b/src/Ai/Base/Actions/WaitForAttackAction.cpp @@ -0,0 +1,166 @@ +/* + * 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 "WaitForAttackAction.h" + +#include +#include + +#include "ObjectAccessor.h" +#include "PlayerbotAI.h" +#include "PlayerbotTextMgr.h" +#include "Playerbots.h" +#include "ServerFacade.h" +#include "TravelMgr.h" +#include "WaitForAttackStrategy.h" + +namespace +{ + +WorldPosition GetBestPoint(AiObjectContext* context, Player* bot, Unit* target, + float minDistance, float maxDistance) +{ + WorldPosition botPosition(bot); + WorldPosition targetPosition(target); + + int8 startDir = urand(0, 1) * 2 - 1; + float const radiansIncrement = (5.0f / 180.0f) * static_cast(M_PI); + float startAngle = targetPosition.getAngleTo(botPosition) + + frand(0.0f, radiansIncrement) * startDir; + float distance = frand(minDistance, maxDistance); + + GuidVector enemies = AI_VALUE(GuidVector, "possible targets no los"); + + for (float tryAngle = 0.0f; tryAngle < static_cast(M_PI); tryAngle += radiansIncrement) + { + for (int8 tryDir = -1; tryAngle && tryDir < 1; tryDir += 2) + { + float pointAngle = startAngle + tryAngle * startDir * tryDir; + + float x = targetPosition.GetPositionX() + distance * cos(pointAngle); + float y = targetPosition.GetPositionY() + distance * sin(pointAngle); + float z = targetPosition.GetPositionZ() + 1.0f; + + WorldPosition point(targetPosition.GetMapId(), x, y, z); + point.setZ(point.getHeight()); + + // Check line of sight to target + if (!target->IsWithinLOS(point.GetPositionX(), point.GetPositionY(), + point.GetPositionZ() + bot->GetCollisionHeight())) + continue; + + // Check if enemies are close to this point + bool enemyClose = false; + for (ObjectGuid const& enemyGUID : enemies) + { + Unit* enemy = ObjectAccessor::GetUnit(*bot, enemyGUID); + if (enemy && enemy->IsWithinLOSInMap(bot) && enemy->IsHostileTo(bot)) + { + float enemyAttackRange = enemy->GetCombatReach() + ATTACK_DISTANCE; + WorldPosition enemyPos(enemy); + if (enemyPos.sqDistance(point) <= (enemyAttackRange * enemyAttackRange)) + { + enemyClose = true; + break; + } + } + } + + if (enemyClose) + continue; + + // Check if bot can path to this point + if (!botPosition.canPathTo(point, bot)) + continue; + + return point; + } + } + + return botPosition; +} + +} // namespace + +bool WaitForAttackKeepSafeDistanceAction::Execute(Event /*event*/) +{ + Unit* target = AI_VALUE(Unit*, "current target"); + + // If our target is moving towards a stationary unit, use that unit as anchor + if (target && !target->IsStopped()) + { + ObjectGuid targetGuid = target->GetTarget(); + if (targetGuid) + { + Unit* targetsTarget = ObjectAccessor::GetUnit(*target, targetGuid); + if (targetsTarget && targetsTarget->IsStopped()) + target = targetsTarget; + } + } + + if (target && target->IsAlive()) + { + float safeDistance = std::max( + target->GetCombatReach() + ATTACK_DISTANCE, + WaitForAttackStrategy::GetSafeDistance()); + float safeDistanceThreshold = WaitForAttackStrategy::GetSafeDistanceThreshold(); + + WorldPosition bestPoint = GetBestPoint(context, bot, target, + safeDistance - safeDistanceThreshold, safeDistance); + + if (bestPoint) + return MoveTo(bestPoint.GetMapId(), bestPoint.GetPositionX(), + bestPoint.GetPositionY(), bestPoint.GetPositionZ()); + } + + return false; +} + +bool SetWaitForAttackTimeAction::Execute(Event event) +{ + std::string newTimeStr = event.getParam(); + + if (newTimeStr.empty()) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_provide_time", + "Please provide a time to set (in seconds)", + std::map()); + botAI->TellMaster(text); + return false; + } + + if (!std::all_of(newTimeStr.begin(), newTimeStr.end(), ::isdigit)) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_invalid_time", + "Please provide valid time to set (in seconds) between 0 and 99", + std::map()); + botAI->TellMaster(text); + return false; + } + + int newTime = std::stoi(newTimeStr); + if (newTime < 0 || newTime > 99) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_invalid_time", + "Please provide valid time to set (in seconds) between 0 and 99", + std::map()); + botAI->TellMaster(text); + return false; + } + + context->GetValue("wait for attack time")->Set(static_cast(newTime)); + + std::map placeholders; + placeholders["%new_time"] = std::to_string(newTime); + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_time_set", + "Wait for attack time set to %new_time seconds", + placeholders); + botAI->TellMaster(text); + return true; +} diff --git a/src/Ai/Base/Actions/WaitForAttackAction.h b/src/Ai/Base/Actions/WaitForAttackAction.h new file mode 100644 index 00000000000..ab12b3908cd --- /dev/null +++ b/src/Ai/Base/Actions/WaitForAttackAction.h @@ -0,0 +1,31 @@ +/* + * 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_WAITFORATTACKACTION_H +#define _PLAYERBOT_WAITFORATTACKACTION_H + +#include "MovementActions.h" + +class PlayerbotAI; + +class WaitForAttackKeepSafeDistanceAction : public MovementAction +{ +public: + WaitForAttackKeepSafeDistanceAction(PlayerbotAI* botAI) + : MovementAction(botAI, "wait for attack keep safe distance") {} + + bool Execute(Event event) override; +}; + +class SetWaitForAttackTimeAction : public Action +{ +public: + SetWaitForAttackTimeAction(PlayerbotAI* botAI) + : Action(botAI, "wait for attack time") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index edd111bb9e9..c3a6b3d0219 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -84,6 +84,7 @@ #include "TellGlyphsAction.h" #include "EquipGlyphsAction.h" #include "PetsAction.h" +#include "WaitForAttackAction.h" class ChatActionContext : public NamedObjectContext { @@ -199,6 +200,7 @@ class ChatActionContext : public NamedObjectContext creators["pet"] = &ChatActionContext::pet; creators["pet attack"] = &ChatActionContext::pet_attack; creators["roll"] = &ChatActionContext::roll_action; + creators["wait for attack time"] = &ChatActionContext::wait_for_attack_time; } private: @@ -311,6 +313,7 @@ class ChatActionContext : public NamedObjectContext static Action* pet(PlayerbotAI* botAI) { return new PetsAction(botAI); } static Action* pet_attack(PlayerbotAI* botAI) { return new PetsAction(botAI, "attack"); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } + static Action* wait_for_attack_time(PlayerbotAI* botAI) { return new SetWaitForAttackTimeAction(botAI); } }; #endif diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index b305b19eae7..6bdfcdc4134 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -145,6 +145,7 @@ class ChatTriggerContext : public NamedObjectContext creators["pet"] = &ChatTriggerContext::pet; creators["pet attack"] = &ChatTriggerContext::pet_attack; creators["roll"] = &ChatTriggerContext::roll_action; + creators["wait for attack time"] = &ChatTriggerContext::wait_for_attack_time; } private: @@ -269,6 +270,7 @@ class ChatTriggerContext : public NamedObjectContext static Trigger* pet(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet"); } static Trigger* pet_attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet attack"); } static Trigger* roll_action(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "roll"); } + static Trigger* wait_for_attack_time(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "wait for attack time"); } }; #endif diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index 64334b799ff..e13b57dd10c 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -199,4 +199,5 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("glyph equip"); // Added for custom Glyphs supported.push_back("pet"); supported.push_back("pet attack"); + supported.push_back("wait for attack time"); } diff --git a/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp b/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp new file mode 100644 index 00000000000..a381405124f --- /dev/null +++ b/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp @@ -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. + */ + +#include "WaitForAttackStrategy.h" + +#include "Action.h" +#include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "Strategy.h" + +void WaitForAttackStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "wait for attack safe distance", + { + NextAction("wait for attack keep safe distance", ACTION_RAID) + } + )); +} + +void WaitForAttackStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new WaitForAttackMultiplier(botAI)); +} + +bool WaitForAttackStrategy::ShouldWait(PlayerbotAI* botAI) +{ + if (botAI->HasStrategy("wait for attack", BOT_STATE_COMBAT)) + { + Player* bot = botAI->GetBot(); + if (bot->GetGroup() && botAI->HasRealPlayerMaster()) + { + // Don't wait if the current target is an enemy player + Unit* target = botAI->GetAiObjectContext()->GetValue("current target")->Get(); + if (target && target->IsPlayer()) + return false; + + AiObjectContext* context = botAI->GetAiObjectContext(); + time_t combatStartTime = context->GetValue("combat start time")->Get(); + + if (bot->IsInCombat()) + { + if (combatStartTime == 0) + { + combatStartTime = time(nullptr); + context->GetValue("combat start time")->Set(combatStartTime); + } + + time_t elapsedTime = time(nullptr) - combatStartTime; + return elapsedTime < GetWaitTime(botAI); + } + else + { + if (combatStartTime != 0) + context->GetValue("combat start time")->Set(0); + } + } + } + + return false; +} + +uint8 WaitForAttackStrategy::GetWaitTime(PlayerbotAI* botAI) +{ + return botAI->GetAiObjectContext()->GetValue("wait for attack time")->Get(); +} + +float WaitForAttackStrategy::GetSafeDistance() +{ + return sPlayerbotAIConfig.spellDistance; +} + +float WaitForAttackMultiplier::GetValue(Action* action) +{ + std::string const& actionName = action->getName(); + + if (actionName != "wait for attack keep safe distance" && + actionName != "dps assist" && + actionName != "set facing" && + actionName != "pull my target" && + actionName != "pull rti target" && + actionName != "pull start" && + actionName != "pull action" && + actionName != "pull end") + { + return WaitForAttackStrategy::ShouldWait(botAI) ? 0.0f : 1.0f; + } + + return 1.0f; +} diff --git a/src/Ai/Base/Strategy/WaitForAttackStrategy.h b/src/Ai/Base/Strategy/WaitForAttackStrategy.h new file mode 100644 index 00000000000..085b4fe0bb8 --- /dev/null +++ b/src/Ai/Base/Strategy/WaitForAttackStrategy.h @@ -0,0 +1,39 @@ +/* + * 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_WAITFORATTACKSTRATEGY_H +#define _PLAYERBOT_WAITFORATTACKSTRATEGY_H + +#include "Multiplier.h" +#include "Strategy.h" + +class PlayerbotAI; + +class WaitForAttackStrategy : public Strategy +{ +public: + WaitForAttackStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "wait for attack"; } + + static bool ShouldWait(PlayerbotAI* botAI); + static uint8 GetWaitTime(PlayerbotAI* botAI); + static float GetSafeDistance(); + static float GetSafeDistanceThreshold() { return 2.5f; } + +private: + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +class WaitForAttackMultiplier : public Multiplier +{ +public: + WaitForAttackMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "wait for attack") {} + + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/Ai/Base/StrategyContext.h b/src/Ai/Base/StrategyContext.h index 60e6808ca72..c16573fa071 100644 --- a/src/Ai/Base/StrategyContext.h +++ b/src/Ai/Base/StrategyContext.h @@ -50,6 +50,7 @@ #include "TravelStrategy.h" #include "UseFoodStrategy.h" #include "UsePotionsStrategy.h" +#include "WaitForAttackStrategy.h" #include "WorldPacketHandlerStrategy.h" class StrategyContext : public NamedObjectContext @@ -124,6 +125,7 @@ class StrategyContext : public NamedObjectContext creators["worldbuff"] = &StrategyContext::world_buff; creators["use bobber"] = &StrategyContext::bobber_strategy; creators["master fishing"] = &StrategyContext::master_fishing; + creators["wait for attack"] = &StrategyContext::wait_for_attack; } private: @@ -195,6 +197,7 @@ class StrategyContext : public NamedObjectContext static Strategy* world_buff(PlayerbotAI* botAI) { return new WorldBuffStrategy(botAI); } static Strategy* bobber_strategy(PlayerbotAI* botAI) { return new UseBobberStrategy(botAI); } static Strategy* master_fishing(PlayerbotAI* botAI) { return new MasterFishingStrategy(botAI); } + static Strategy* wait_for_attack(PlayerbotAI* botAI) { return new WaitForAttackStrategy(botAI); } }; class MovementStrategyContext : public NamedObjectContext diff --git a/src/Ai/Base/Trigger/WaitForAttackTriggers.h b/src/Ai/Base/Trigger/WaitForAttackTriggers.h new file mode 100644 index 00000000000..152434b1f32 --- /dev/null +++ b/src/Ai/Base/Trigger/WaitForAttackTriggers.h @@ -0,0 +1,49 @@ +/* + * 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_WAITFORATTACKTRIGGERS_H +#define _PLAYERBOT_WAITFORATTACKTRIGGERS_H + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "ServerFacade.h" +#include "Trigger.h" +#include "WaitForAttackStrategy.h" + +class PlayerbotAI; + +class WaitForAttackSafeDistanceTrigger : public Trigger +{ +public: + WaitForAttackSafeDistanceTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "wait for attack safe distance") {} + + bool IsActive() override + { + if (!WaitForAttackStrategy::ShouldWait(botAI)) + return false; + + // Do not move if stay strategy is set + if (botAI->HasStrategy("stay", botAI->GetState())) + return false; + + // Do not move if currently being targeted + if (!bot->getAttackers().empty()) + return false; + + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) + return false; + + float safeDistance = WaitForAttackStrategy::GetSafeDistance(); + float safeDistanceThreshold = WaitForAttackStrategy::GetSafeDistanceThreshold(); + float distanceToTarget = ServerFacade::instance().GetDistance2d(bot, target); + + return (distanceToTarget > (safeDistance + safeDistanceThreshold)) || + (distanceToTarget < (safeDistance - safeDistanceThreshold)); + } +}; + +#endif diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index ca662f3d45c..6536c0d9c97 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -20,6 +20,7 @@ #include "RtiTriggers.h" #include "StuckTriggers.h" #include "TravelTriggers.h" +#include "WaitForAttackTriggers.h" class PlayerbotAI; @@ -231,6 +232,7 @@ class TriggerContext : public NamedObjectContext creators["can fish"] = &TriggerContext::can_fish; creators["can use fishing bobber"] = &TriggerContext::can_use_fishing_bobber; creators["new pet"] = &TriggerContext::new_pet; + creators["wait for attack safe distance"] = &TriggerContext::wait_for_attack_safe_distance; } private: @@ -434,6 +436,7 @@ class TriggerContext : public NamedObjectContext static Trigger* can_fish(PlayerbotAI* ai) { return new CanFishTrigger(ai); } static Trigger* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberTrigger(ai); } static Trigger* new_pet(PlayerbotAI* ai) { return new NewPetTrigger(ai); } + static Trigger* wait_for_attack_safe_distance(PlayerbotAI* ai) { return new WaitForAttackSafeDistanceTrigger(ai); } }; #endif diff --git a/src/Ai/Base/Value/WaitForAttackTimeValue.h b/src/Ai/Base/Value/WaitForAttackTimeValue.h new file mode 100644 index 00000000000..c2d26a430fd --- /dev/null +++ b/src/Ai/Base/Value/WaitForAttackTimeValue.h @@ -0,0 +1,25 @@ +/* + * 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_WAITFORATTACKTIMEVALUE_H +#define _PLAYERBOT_WAITFORATTACKTIMEVALUE_H + +#include "Value.h" + +class PlayerbotAI; + +class WaitForAttackTimeValue : public ManualSetValue +{ +public: + WaitForAttackTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 10, "wait for attack time") {} +}; + +class CombatStartTimeValue : public ManualSetValue +{ +public: + CombatStartTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 0, "combat start time") {} +}; + +#endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index 6038db3bc6e..d1b07b88cce 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -91,6 +91,7 @@ #include "ThreatValues.h" #include "TradeValues.h" #include "Value.h" +#include "WaitForAttackTimeValue.h" class PlayerbotAI; @@ -322,6 +323,8 @@ class ValueContext : public NamedObjectContext creators["can fish"] = &ValueContext::can_fish; creators["can use fishing bobber"] = &ValueContext::can_use_fishing_bobber; creators["fishing spot"] = &ValueContext::fishing_spot; + creators["wait for attack time"] = &ValueContext::wait_for_attack_time; + creators["combat start time"] = &ValueContext::combat_start_time; } private: @@ -578,6 +581,8 @@ class ValueContext : public NamedObjectContext { return new ManualSetValue(ai, false, "custom_glyphs"); } + static UntypedValue* wait_for_attack_time(PlayerbotAI* ai) { return new WaitForAttackTimeValue(ai); } + static UntypedValue* combat_start_time(PlayerbotAI* ai) { return new CombatStartTimeValue(ai); } }; #endif From 45aa6f2f8e1cbe3aabda663765cdd7bc50b24842 Mon Sep 17 00:00:00 2001 From: FutterSillo <98534244+FutterSillo@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:38:54 +0100 Subject: [PATCH 30/87] Update valid targets for Hunter Talent: "Improved Tracking" (#2218) ## Pull Request Description Updates the list of valid targets in NoTrackTrigger::IsActive() for the hunter talent "Improved Tracking". Including: "track beasts", "track demons", "track dragonkin", "track elementals", "track giants", "track humanoids", "track undead", // "track hidden", // "find herbs", // "find minerals", // "find fish", // "find treasure", This change ensures standard combat tracking is correctly recognized while providing commented-out utility options (Hidden, Herbs, Minerals, Fish, Treasure). This is specifically useful for "Selfbot" users who want to maintain a specific utility tracking without the bot logic automatically overriding it and switching back to "Track Humanoids". ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes - Track valid Targets for "Improved Tracking" with a Hunter in selfbot. Its stay the same. - Track any other Targets for "Improved Tracking" with a Hunter in selfbot. Its switch back to "Track Humanoids". ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp b/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp index 84ece04b76c..972332d1a0a 100644 --- a/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp +++ b/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp @@ -107,6 +107,8 @@ bool SwitchToMeleeTrigger::IsActive() ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "current target"), 8.0f)); } +// Valid targets for "Improved Tracking". +// Optional/Utility targets (uncomment for selfbot). bool NoTrackTrigger::IsActive() { std::vector track_list = { @@ -115,8 +117,13 @@ bool NoTrackTrigger::IsActive() "track dragonkin", "track elementals", "track giants", - "track hidden", - "track humanoids" + "track humanoids", + "track undead", + // "track hidden", + // "find herbs", + // "find minerals", + // "find fish", + // "find treasure", }; for (auto &track: track_list) From 6db44b5296b259de52618f05cb0b4adc2866d955 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 27 Mar 2026 19:53:53 +0100 Subject: [PATCH 31/87] Will of the forsaken (#2231) ## Pull Request Description Added Will of the Forsaken support. Made structure fix for affected files. Partially resolves: #2002 ## How to Test the Changes - create/invite undead bot - start fight (you can use dummy target) - use `.aura 6215` to fear bot - bot should use "Will of the Forsaken" to remove debuff ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Undead bot use now "Will of the Forsaken" to remove charm, fear or sleep. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) OpenCode to review changes ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers obraz --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> --- src/Ai/Base/ActionContext.h | 2 + src/Ai/Base/Actions/GenericBuffUtils.h | 63 ----------------- src/Ai/Base/Actions/GenericSpellActions.cpp | 34 ++++++++-- src/Ai/Base/Actions/GenericSpellActions.h | 10 +++ src/Ai/Base/Strategy/RacialsStrategy.cpp | 3 + src/Ai/Base/Trigger/GenericTriggers.cpp | 8 +++ src/Ai/Base/Trigger/GenericTriggers.h | 8 +++ src/Ai/Base/TriggerContext.h | 2 + .../{Actions => Util}/GenericBuffUtils.cpp | 27 +++++++- src/Ai/Base/Util/GenericBuffUtils.h | 68 +++++++++++++++++++ .../Class/Paladin/Action/PaladinActions.cpp | 2 +- 11 files changed, 158 insertions(+), 69 deletions(-) delete mode 100644 src/Ai/Base/Actions/GenericBuffUtils.h rename src/Ai/Base/{Actions => Util}/GenericBuffUtils.cpp (89%) create mode 100644 src/Ai/Base/Util/GenericBuffUtils.h diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 256c0de3882..41763be90f2 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -165,6 +165,7 @@ class ActionContext : public NamedObjectContext creators["blood fury"] = &ActionContext::blood_fury; creators["berserking"] = &ActionContext::berserking; creators["every man for himself"] = &ActionContext::every_man_for_himself; + creators["will of the forsaken"] = &ActionContext::will_of_the_forsaken; creators["use trinket"] = &ActionContext::use_trinket; creators["auto talents"] = &ActionContext::auto_talents; creators["auto share quest"] = &ActionContext::auto_share_quest; @@ -361,6 +362,7 @@ class ActionContext : public NamedObjectContext static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); } static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); } static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); } + static Action* will_of_the_forsaken(PlayerbotAI* botAI) { return new CastWillOfTheForsakenAction(botAI); } static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } diff --git a/src/Ai/Base/Actions/GenericBuffUtils.h b/src/Ai/Base/Actions/GenericBuffUtils.h deleted file mode 100644 index c893de59760..00000000000 --- a/src/Ai/Base/Actions/GenericBuffUtils.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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. - */ - -#pragma once - -#include -#include -#include "Common.h" -#include "Group.h" -#include "Chat.h" -#include "Language.h" - -class Player; -class PlayerbotAI; - -namespace ai::buff -{ - - // Build an aura qualifier "single + greater" to avoid double-buffing - std::string MakeAuraQualifierForBuff(std::string const& name); - - // Returns the group spell name for a given single-target buff. - // If no group equivalent exists, returns "". - std::string GroupVariantFor(std::string const& name); - - // Checks if the bot has the required reagents to cast a spell (by its spellId). - // Returns false if the spellId is invalid. - bool HasRequiredReagents(Player* bot, uint32 spellId); - - // Applies the "switch to group buff" policy if: the bot is in a group of size x+, - // the group variant is known/useful, and reagents are available. Otherwise, returns baseName. - // If announceOnMissing == true and reagents are missing, calls the 'announce' callback - // (if provided) to notify the party/raid. - std::string UpgradeToGroupIfAppropriate( - Player* bot, - PlayerbotAI* botAI, - std::string const& baseName, - bool announceOnMissing = false, - std::function announce = {} - ); -} - -namespace ai::chat { - inline std::function MakeGroupAnnouncer(Player* me) - { - return [me](std::string const& msg) - { - if (Group* g = me->GetGroup()) - { - WorldPacket data; - ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY; - ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str()); - g->BroadcastPacket(&data, true, -1, me->GetGUID()); - } - else - { - me->Say(msg, LANG_UNIVERSAL); - } - }; - } -} diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 392d1850076..82bdb74d2a2 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -17,10 +17,11 @@ #include "WorldPacket.h" #include "Group.h" #include "Chat.h" -#include "GenericBuffUtils.h" +#include "Ai/Base/Util/GenericBuffUtils.h" #include "PlayerbotAI.h" using ai::buff::MakeAuraQualifierForBuff; +using ai::spell::HasSpellOrCategoryCooldown; CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) @@ -320,7 +321,7 @@ bool CastEveryManForHimselfAction::isPossible() if (!bot->HasSpell(spellId)) return false; - if (bot->HasSpellCooldown(spellId)) + if (HasSpellOrCategoryCooldown(bot, spellId)) return false; return true; @@ -328,11 +329,36 @@ bool CastEveryManForHimselfAction::isPossible() bool CastEveryManForHimselfAction::isUseful() { - return bot->HasAuraType(SPELL_AURA_MOD_STUN) || + return (bot->HasAuraType(SPELL_AURA_MOD_STUN) || bot->HasAuraType(SPELL_AURA_MOD_FEAR) || bot->HasAuraType(SPELL_AURA_MOD_ROOT) || bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || - bot->HasAuraType(SPELL_AURA_MOD_CHARM); + bot->HasAuraType(SPELL_AURA_MOD_CHARM)) + && CastSpellAction::isUseful(); +} + +bool CastWillOfTheForsakenAction::isPossible() +{ + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + if (!spellId) + return false; + + if (!bot->HasSpell(spellId)) + return false; + + if (HasSpellOrCategoryCooldown(bot, spellId)) + return false; + + return true; +} + +bool CastWillOfTheForsakenAction::isUseful() +{ + return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM) || + bot->HasAuraType(SPELL_AURA_AOE_CHARM) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP)) + && CastSpellAction::isUseful(); } bool UseTrinketAction::Execute(Event /*event*/) diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index fdc0dcdcf13..b15c4894a3b 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -294,6 +294,16 @@ class CastEveryManForHimselfAction : public CastSpellAction bool isUseful() override; }; +class CastWillOfTheForsakenAction : public CastSpellAction +{ +public: + CastWillOfTheForsakenAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "will of the forsaken") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; + bool isUseful() override; +}; + class UseTrinketAction : public Action { public: diff --git a/src/Ai/Base/Strategy/RacialsStrategy.cpp b/src/Ai/Base/Strategy/RacialsStrategy.cpp index 753302c354e..ae45cdaaff6 100644 --- a/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -37,6 +37,9 @@ void RacialsStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) })); + triggers.push_back(new TriggerNode( + "fear charm sleep", { NextAction("will of the forsaken", ACTION_EMERGENCY + 1) })); + } RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI) diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 735e1df7eaf..68f5a523909 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -473,6 +473,14 @@ bool LossOfControlTrigger::IsActive() bot->HasAuraType(SPELL_AURA_MOD_CHARM); } +bool FearCharmSleepTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM) || + bot->HasAuraType(SPELL_AURA_AOE_CHARM) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP); +} + bool HasAuraStackTrigger::IsActive() { Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index 68fc4b61f5c..f78728cd5e8 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -754,6 +754,14 @@ class LossOfControlTrigger : public Trigger bool IsActive() override; }; +class FearCharmSleepTrigger : public Trigger +{ +public: + FearCharmSleepTrigger(PlayerbotAI* botAI) : Trigger(botAI, "fear charm sleep", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index 6536c0d9c97..63f9be4049f 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -61,6 +61,7 @@ class TriggerContext : public NamedObjectContext creators["generic boost"] = &TriggerContext::generic_boost; creators["loss of control"] = &TriggerContext::loss_of_control; + creators["fear charm sleep"] = &TriggerContext::fear_charm_sleep; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -367,6 +368,7 @@ class TriggerContext : public NamedObjectContext } static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); } + static Trigger* fear_charm_sleep(PlayerbotAI* botAI) { return new FearCharmSleepTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI); diff --git a/src/Ai/Base/Actions/GenericBuffUtils.cpp b/src/Ai/Base/Util/GenericBuffUtils.cpp similarity index 89% rename from src/Ai/Base/Actions/GenericBuffUtils.cpp rename to src/Ai/Base/Util/GenericBuffUtils.cpp index e33c12f180b..22d56c0ad8c 100644 --- a/src/Ai/Base/Actions/GenericBuffUtils.cpp +++ b/src/Ai/Base/Util/GenericBuffUtils.cpp @@ -67,7 +67,7 @@ namespace ai::buff if (info->Reagent[i] > 0) { uint32 const itemId = info->Reagent[i]; - int32 const need = info->ReagentCount[i]; + int32 const need = info->ReagentCount[i]; if ((int32)bot->GetItemCount(itemId, false) < need) return false; } @@ -143,3 +143,28 @@ namespace ai::buff return castName; } } + +namespace ai::spell +{ + bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId) + { + if (bot->HasSpellCooldown(spellId)) + return true; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return false; + + uint32 category = spellInfo->GetCategory(); + if (!category) + return false; + + for (auto const& [cooldownSpellId, cooldown] : bot->GetSpellCooldownMap()) + { + if (cooldown.category == category && bot->GetSpellCooldownDelay(cooldownSpellId) > 0) + return true; + } + + return false; + } +} diff --git a/src/Ai/Base/Util/GenericBuffUtils.h b/src/Ai/Base/Util/GenericBuffUtils.h new file mode 100644 index 00000000000..37bad58a6d9 --- /dev/null +++ b/src/Ai/Base/Util/GenericBuffUtils.h @@ -0,0 +1,68 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include "Common.h" +#include "Group.h" +#include "Chat.h" +#include "Language.h" + +class Player; +class PlayerbotAI; + +namespace ai::buff +{ + +// Build an aura qualifier "single + greater" to avoid double-buffing +std::string MakeAuraQualifierForBuff(std::string const& name); + +// Returns the group spell name for a given single-target buff. +// If no group equivalent exists, returns "". +std::string GroupVariantFor(std::string const& name); + +// Checks if the bot has the required reagents to cast a spell (by its spellId). +// Returns false if the spellId is invalid. +bool HasRequiredReagents(Player* bot, uint32 spellId); + +// Applies the "switch to group buff" policy if: the bot is in a group of size x+, +// the group variant is known/useful, and reagents are available. Otherwise, returns baseName. +// If announceOnMissing == true and reagents are missing, calls the 'announce' callback +// (if provided) to notify the party/raid. +std::string UpgradeToGroupIfAppropriate( + Player* bot, + PlayerbotAI* botAI, + std::string const& baseName, + bool announceOnMissing = false, + std::function announce = {} + ); +} + +namespace ai::spell +{ + bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId); +} + +namespace ai::chat { + inline std::function MakeGroupAnnouncer(Player* me) + { + return [me](std::string const& msg) + { + if (Group* g = me->GetGroup()) + { + WorldPacket data; + ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY; + ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str()); + g->BroadcastPacket(&data, true, -1, me->GetGUID()); + } + else + { + me->Say(msg, LANG_UNIVERSAL); + } + }; + } +} diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index c1521bb1e1a..944b6e68601 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -11,7 +11,7 @@ #include "Playerbots.h" #include "SharedDefines.h" #include "../../../../../src/server/scripts/Spells/spell_generic.cpp" -#include "GenericBuffUtils.h" +#include "Ai/Base/Util/GenericBuffUtils.h" #include "Group.h" #include "ObjectAccessor.h" From ac99f4569682d3ebd06355dbe8d798998f1f39e4 Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:54:24 +0100 Subject: [PATCH 32/87] Bugfix(issue-1878): floating players in certain conditions (#2245) ## Pull Request Description https://github.com/mod-playerbots/mod-playerbots/issues/1878 https://www.youtube.com/shorts/-HO-OosP0oY ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Base/Actions/MovementActions.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 1dbca03122d..4f4a110a305 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -948,14 +948,15 @@ void MovementAction::UpdateMovementState() const auto liquidState = bot->GetLiquidData().Status; const float gZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); const bool onGroundZ = bot->GetPositionZ() < gZ + 1.f; - const bool canSwim = liquidState == LIQUID_MAP_IN_WATER || liquidState == LIQUID_MAP_UNDER_WATER; - const bool canFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); + const bool wantsSwim = liquidState == LIQUID_MAP_IN_WATER || liquidState == LIQUID_MAP_UNDER_WATER; + const bool wantsFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); const bool canWaterWalk = bot->HasWaterWalkAura(); const bool isMasterFlying = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) : true; const bool isMasterSwimming = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING) : true; const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING); const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + const bool hasGravityDisabled = bot->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); bool movementFlagsUpdated = false; // handle water (fragile logic do not alter without testing every detail, animation and transition) @@ -970,11 +971,11 @@ void MovementAction::UpdateMovementState() else if ((!canWaterWalk || isMasterSwimming) && isWaterWalking) { bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - if (canSwim) + if (wantsSwim) bot->SetSwim(true); movementFlagsUpdated = true; } - else if (!canSwim && isSwimming) + else if (!wantsSwim && isSwimming) { bot->SetSwim(false); movementFlagsUpdated = true; @@ -990,17 +991,21 @@ void MovementAction::UpdateMovementState() } // handle flying - if ((canFly && !isFlying) && isMasterFlying) + if (wantsFly && !isFlying && isMasterFlying) { bot->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); bot->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); - - // required for transition and state monitoring. - if (MotionMaster* mm = bot->GetMotionMaster()) - mm->MoveTakeoff(0, {bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ() + 1.F}, 0.F, true); + movementFlagsUpdated = true; + } + else if (!wantsFly && !isWaterWalking && (isFlying || hasGravityDisabled)) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); + movementFlagsUpdated = true; } - else if ((!canFly && !isWaterWalking && isFlying) || (!isMasterFlying && isFlying && onGroundZ)) + else if (!isMasterFlying && isFlying && onGroundZ) { bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); From e1f5064c9df720d33bbe7a52525b44ee895e283b Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:47:29 -0600 Subject: [PATCH 33/87] Fix SQL ID conflict (#2251) ## Pull Request Description Just a quick update to SQL IDs after seeing that #2131 and #2211 (which are only in test-staging as of right now) use the same IDs. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [ ] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [ ] Yes (**explain below**) ## Final Checklist - - [ ] Stability is not compromised. - - [ ] Performance impact is understood, tested, and acceptable. - - [ ] Added logic complexity is justified and explained. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- .../updates/2026_03_26_ai_playerbot_logout_texts.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql b/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql index 344e6109d5e..05f6d5d03bb 100644 --- a/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql +++ b/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql @@ -7,7 +7,7 @@ INSERT INTO `ai_playerbot_texts` `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) VALUES ( - 1740, + 1743, 'bot_not_your_master', "You are not my master!", 0, 0, @@ -35,7 +35,7 @@ INSERT INTO `ai_playerbot_texts` `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) VALUES ( - 1741, + 1744, 'bot_rndbot_no_logout', "You can't command me to logout!", 0, 0, @@ -56,4 +56,4 @@ VALUES ( -- ruRU "Ты не можешь приказать мне выйти из игры!"); -INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('bot_rndbot_no_logout', 100); \ No newline at end of file +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('bot_rndbot_no_logout', 100); From 4dbcd2544c134f94f5fba16a2f7a015ccb251ae5 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 29 Mar 2026 16:59:18 +0200 Subject: [PATCH 34/87] Auto label PR - github action (#2257) Maintenance PR --- .github/workflows/label_translation-pr.yml | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/label_translation-pr.yml diff --git a/.github/workflows/label_translation-pr.yml b/.github/workflows/label_translation-pr.yml new file mode 100644 index 00000000000..5dbe92fc5c6 --- /dev/null +++ b/.github/workflows/label_translation-pr.yml @@ -0,0 +1,38 @@ +name: Label translation PRs + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label-translation: + runs-on: ubuntu-latest + steps: + - name: Fetch PR diff + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api \ + repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }} \ + --header "Accept: application/vnd.github.v3.diff" > pr.diff + + - name: Detect ai_playerbot_texts inserts + id: detect + run: | + if grep -E '^\+.*INSERT[[:space:]]+INTO[[:space:]]+`?ai_playerbot_texts`?' pr.diff; then + echo "has_translation=true" >> "$GITHUB_OUTPUT" + else + echo "has_translation=false" >> "$GITHUB_OUTPUT" + fi + + - name: Add label + if: steps.detect.outputs.has_translation == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr edit ${{ github.event.pull_request.number }} \ + --add-label "Added translation" From 7fa9ab7d34a0fc396cfb1331a30de863a33ac8dc Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:55:33 -0700 Subject: [PATCH 35/87] Module update for CollisionChange (#2242) ## Pull Request Description Some code changes due to core update change from MMapFactory.h ->MapCollisionData.h ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [X] Yes (**explain below**) Identifying problematic areas generated by the change ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Trigger/StuckTriggers.cpp | 5 ++-- src/Mgr/Travel/TravelMgr.cpp | 33 ++++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Ai/Base/Trigger/StuckTriggers.cpp b/src/Ai/Base/Trigger/StuckTriggers.cpp index b6ade75d867..0829a2394e8 100644 --- a/src/Ai/Base/Trigger/StuckTriggers.cpp +++ b/src/Ai/Base/Trigger/StuckTriggers.cpp @@ -8,7 +8,7 @@ #include "CellImpl.h" #include "PathGenerator.h" #include "Playerbots.h" -#include "MMapFactory.h" +#include "MapCollisionData.h" bool MoveStuckTrigger::IsActive() { @@ -89,8 +89,7 @@ bool MoveLongStuckTrigger::IsActive() return true; } - if (cell.GridX() > 0 && cell.GridY() > 0 && - !MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(botPos.GetMapId(), cell.GridX(), cell.GridY())) + if (bot->GetMap()->IsGridCreated(GridCoord(cell.GridX(), cell.GridY()))) { // LOG_INFO("playerbots", "Bot {} {}:{} <{}> was in unloaded grid {},{} on map {}", // bot->GetGUID().ToString().c_str(), bot->GetTeamId() == TEAM_ALLIANCE ? "A" : "H", bot->GetLevel(), diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index d6942deab4f..7a5ac4f2e47 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -14,7 +14,7 @@ #include "TravelNode.h" #include "Talentspec.h" #include "ChatHelper.h" -#include "MMapFactory.h" +#include "MapCollisionData.h" #include "MapMgr.h" #include "PathGenerator.h" #include "Playerbots.h" @@ -687,10 +687,11 @@ std::vector WorldPosition::frommGridCoord(mGridCoord GridCoord) return retVec; } +// TODO: Cleanup — make this actually work. void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y) { std::string const fileName = "load_map_grid.csv"; - +/* if (isOverworld() && false || false) { if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y)) @@ -745,22 +746,22 @@ void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y) sPlayerbotAIConfig.log(fileName, out.str().c_str()); } } +*/ + if (!TravelMgr::instance().isBadMmap(mapId, x, y)) + { + // load navmesh + Map* map = getMap(); + if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR) + TravelMgr::instance().addBadMmap(mapId, x, y); - if (!TravelMgr::instance().isBadMmap(mapId, x, y)) + if (sPlayerbotAIConfig.hasLog(fileName)) { - // load navmesh - if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y)) - TravelMgr::instance().addBadMmap(mapId, x, y); - - if (sPlayerbotAIConfig.hasLog(fileName)) - { - std::ostringstream out; - out << sPlayerbotAIConfig.GetTimestampStr(); - out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1") - << ","; - printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true); - sPlayerbotAIConfig.log(fileName, out.str().c_str()); - } + std::ostringstream out; + out << sPlayerbotAIConfig.GetTimestampStr(); + out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1") + << ","; + printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true); + sPlayerbotAIConfig.log(fileName, out.str().c_str()); } } } From 6d2ee7083136fc28b0030fe484d314815a171138 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:36:19 -0700 Subject: [PATCH 36/87] ci: Use test-staging branch of azerothcore-wotlk for test-staging PRs (#2280) ## Pull Request Description Should sync the test staging branch compile checks. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Wrote the changes. I tested them. Claude Opus. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/core_build.yml | 2 +- .github/workflows/macos_build.yml | 2 +- .github/workflows/windows_build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/core_build.yml b/.github/workflows/core_build.yml index ac6ee60d844..6021238e60b 100644 --- a/.github/workflows/core_build.yml +++ b/.github/workflows/core_build.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v3 with: repository: 'mod-playerbots/azerothcore-wotlk' - ref: 'Playerbot' + ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }} - name: Set reusable strings id: strings diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index ab4d83daa64..a7b5a5e13db 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'mod-playerbots/azerothcore-wotlk' - ref: 'Playerbot' + ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }} - name: Checkout Playerbot Module uses: actions/checkout@v4 with: diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 121d97751ff..94d6af10879 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3 with: repository: 'mod-playerbots/azerothcore-wotlk' - ref: 'Playerbot' + ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }} path: 'ac' - name: Checkout Playerbot Module uses: actions/checkout@v3 From 79562be2e57eb53089bf47899225cf8bd4eb864d Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 3 Apr 2026 15:24:37 -0500 Subject: [PATCH 37/87] Fix Hunter Aspect Switching + Trigger Cleanups (#2203) # Pull Request Note: When I reference Aspect of the Hawk below, it also means Aspect of the Dragonhawk (the code will use Dragonhawk if the Hunter has it, Hawk if not, they share actions, triggers, and strategies). Hunter Aspects are currently bugged. All Hunters, regardless of spec or strategy, are hardcoded to use Aspect of the Hawk when mana is at 70%+ and Aspect of the Viper when mana drops to "lowMana" from the config (default is 15%), divided by 2. This means the following: - Hawk (bdps) and Viper (bmana) strategies are useless - Pack (bspeed) and Wild (rnature) strategies are applied, but bots will rapidly switch back and forth between Pack/Wild and Hawk/Viper, depending on strategy and mana level. This PR addresses the issues by doing the following: - Global Hawk strategy is removed. Now you need to set bdps for Hunters to use Hawk, but bdps remains the default Aspect strategy for all Hunters. - Dedicated Viper strategy is removed, leaving the global strategy. However, Viper will be used (when lowMana/2) ONLY if the bot is set to bdps. If the bot has the Wild or Pack strategy, they will not switch to Viper at all. I did this because I am assuming if you are using Wild or Pack, you need them for reasons other than to pump DPS. - The threshold to switch back to Hawk is lowered from 70% to 60%. The gap between lowMana/2 and 60% is now filled--if bdps is on, Hunters will switch to Hawk whenever above the Viper threshold, _except_ for when they have the Viper aura, in which case they will not switch to Hawk until 60% mana. This lets the Hunter build back mana before swapping back to Hawk (more like general player behavior) while still letting them swap from other Aspects to Hawk without needing to be all the way at 60% mana. - Gets rid of a weird condition in the Hawk trigger that would make it so that Hunters would switch to Hawk when at exactly 0 mana. I'm not sure what the point of that is. Also, I refactored the triggers a bit because I noticed there was some dead code in there. I didn't do a comprehensive refactor, but there was a lot of stuff that clearly didn't make sense even to my eyes, like back-to-back returns. I think there's more unnecessary code even just in the triggers, but I didn't want to get too into the weeds with this PR. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? I don't expect there to be any impact on costs, and if anything this PR removes some unneeded checks from triggers. --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it The easiest way is to go shoot a dummy with Volley until low on mana and then toggle on selfbot. You can do this with various Aspects active to test. ## Complexity & Impact Does this change add new decision branches? - - [X] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [X] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [X] Yes (**explain why**) Described above. Default behavior is broken. If this introduces more advanced or AI-heavy logic: - - [X] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [X] Yes (**explain below**) I asked Claude some questions about the triggers to make sure I didn't screw anything up. If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [X] Stability is not compromised - - [X] Performance impact is understood, tested, and acceptable - - [X] Added logic complexity is justified and explained - - [X] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Class/Hunter/Action/HunterActions.cpp | 49 ++-- src/Ai/Class/Hunter/Action/HunterActions.h | 219 +++++++++++------- src/Ai/Class/Hunter/HunterAiObjectContext.cpp | 63 +++-- .../Strategy/BeastMasteryHunterStrategy.cpp | 34 +-- .../GenericHunterNonCombatStrategy.cpp | 57 ++--- .../Hunter/Strategy/GenericHunterStrategy.cpp | 58 +---- .../Hunter/Strategy/GenericHunterStrategy.h | 9 - .../Hunter/Strategy/HunterBuffStrategies.cpp | 58 +++-- .../Hunter/Strategy/HunterBuffStrategies.h | 27 +-- .../Strategy/MarksmanshipHunterStrategy.cpp | 38 +-- .../Strategy/SurvivalHunterStrategy.cpp | 73 ++---- .../Class/Hunter/Trigger/HunterTriggers.cpp | 77 +++--- src/Ai/Class/Hunter/Trigger/HunterTriggers.h | 22 +- .../Multiplier/RaidIccMultipliers.cpp | 4 +- src/Bot/Factory/AiFactory.cpp | 2 +- src/Bot/Factory/PlayerbotFactory.cpp | 2 +- 16 files changed, 354 insertions(+), 438 deletions(-) diff --git a/src/Ai/Class/Hunter/Action/HunterActions.cpp b/src/Ai/Class/Hunter/Action/HunterActions.cpp index 18e5d590579..a49f9a10e63 100644 --- a/src/Ai/Class/Hunter/Action/HunterActions.cpp +++ b/src/Ai/Class/Hunter/Action/HunterActions.cpp @@ -16,18 +16,15 @@ bool CastViperStingAction::isUseful() AI_VALUE2(uint8, "mana", "current target") >= 30; } -bool CastAspectOfTheCheetahAction::isUseful() -{ - return !botAI->HasAnyAuraOf(GetTarget(), "aspect of the cheetah", "aspect of the pack", nullptr); -} - bool CastAspectOfTheHawkAction::isUseful() { Unit* target = GetTarget(); if (!target) return false; + if (bot->HasSpell(61846) || bot->HasSpell(61847)) // Aspect of the Dragonhawk spell IDs return false; + return true; } @@ -36,11 +33,14 @@ bool CastArcaneShotAction::isUseful() Unit* target = GetTarget(); if (!target) return false; - if (bot->HasSpell(53301) || bot->HasSpell(60051) || bot->HasSpell(60052) || bot->HasSpell(60053)) // Explosive Shot spell IDs + + if (bot->HasSpell(53301) || bot->HasSpell(60051) || + bot->HasSpell(60052) || bot->HasSpell(60053)) // Explosive Shot spell IDs return false; // Armor Penetration rating check - will not cast Arcane Shot above 435 ArP - int32 armorPenRating = bot->GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1) + bot->GetUInt32Value(CR_ARMOR_PENETRATION); + int32 armorPenRating = + bot->GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1) + bot->GetUInt32Value(CR_ARMOR_PENETRATION); if (armorPenRating > 435) return false; @@ -52,18 +52,26 @@ bool CastImmolationTrapAction::isUseful() Unit* target = GetTarget(); if (!target) return false; - if (bot->HasSpell(13813) || bot->HasSpell(14316) || bot->HasSpell(14317) || bot->HasSpell(27025) || bot->HasSpell(49066) || bot->HasSpell(49067)) // Explosive Trap spell IDs + + if (bot->HasSpell(13813) || bot->HasSpell(14316) || bot->HasSpell(14317) || bot->HasSpell(27025) || + bot->HasSpell(49066) || bot->HasSpell(49067)) // Explosive Trap spell IDs return false; + return true; } -Value* CastFreezingTrap::GetTargetValue() { return context->GetValue("cc target", "freezing trap"); } +Value* CastFreezingTrap::GetTargetValue() +{ + return context->GetValue("cc target", "freezing trap"); +} bool FeedPetAction::Execute(Event /*event*/) { - if (Pet* pet = bot->GetPet()) - if (pet->getPetType() == HUNTER_PET && pet->GetHappinessState() != HAPPY) - pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS))); + if (Pet* pet = bot->GetPet(); pet && pet->getPetType() == HUNTER_PET && + pet->GetHappinessState() != HAPPY) + { + pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS))); + } return true; } @@ -79,6 +87,7 @@ bool CastAutoShotAction::isUseful() { return false; } + return AI_VALUE(uint32, "active spell") != AI_VALUE2(uint32, "spell id", getName()); } @@ -87,6 +96,7 @@ bool CastDisengageAction::Execute(Event event) Unit* target = AI_VALUE(Unit*, "current target"); if (!target) return false; + // can cast spell check passed in isUseful() bot->SetOrientation(bot->GetAngle(target)); return CastSpellAction::Execute(event); @@ -97,11 +107,20 @@ bool CastDisengageAction::isUseful() return !botAI->HasStrategy("trap weave", BOT_STATE_COMBAT); } -Value* CastScareBeastCcAction::GetTargetValue() { return context->GetValue("cc target", "scare beast"); } +Value* CastScareBeastCcAction::GetTargetValue() +{ + return context->GetValue("cc target", "scare beast"); +} -bool CastScareBeastCcAction::Execute(Event /*event*/) { return botAI->CastSpell("scare beast", GetTarget()); } +bool CastScareBeastCcAction::Execute(Event /*event*/) +{ + return botAI->CastSpell("scare beast", GetTarget()); +} -bool CastWingClipAction::isUseful() { return CastSpellAction::isUseful() && !botAI->HasAura(spell, GetTarget()); } +bool CastWingClipAction::isUseful() +{ + return CastSpellAction::isUseful() && !botAI->HasAura(spell, GetTarget()); +} std::vector CastWingClipAction::getPrerequisites() { diff --git a/src/Ai/Class/Hunter/Action/HunterActions.h b/src/Ai/Class/Hunter/Action/HunterActions.h index 8e21135254d..a67f17780b5 100644 --- a/src/Ai/Class/Hunter/Action/HunterActions.h +++ b/src/Ai/Class/Hunter/Action/HunterActions.h @@ -19,52 +19,58 @@ class Unit; class CastTrueshotAuraAction : public CastBuffSpellAction { public: - CastTrueshotAuraAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "trueshot aura") {} + CastTrueshotAuraAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "trueshot aura") {} }; -class CastAspectOfTheHawkAction : public CastBuffSpellAction +class CastAspectOfTheDragonhawkAction : public CastBuffSpellAction { public: - CastAspectOfTheHawkAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the hawk") {} - bool isUseful() override; + CastAspectOfTheDragonhawkAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the dragonhawk") {} }; -class CastAspectOfTheMonkeyAction : public CastBuffSpellAction +class CastAspectOfTheHawkAction : public CastBuffSpellAction { public: - CastAspectOfTheMonkeyAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the monkey") {} + CastAspectOfTheHawkAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the hawk") {} + bool isUseful() override; }; -class CastAspectOfTheDragonhawkAction : public CastBuffSpellAction +class CastAspectOfTheMonkeyAction : public CastBuffSpellAction { public: - CastAspectOfTheDragonhawkAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the dragonhawk") {} + CastAspectOfTheMonkeyAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the monkey") {} }; class CastAspectOfTheWildAction : public CastBuffSpellAction { public: - CastAspectOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the wild") {} + CastAspectOfTheWildAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the wild") {} }; class CastAspectOfTheCheetahAction : public CastBuffSpellAction { public: - CastAspectOfTheCheetahAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the cheetah") {} - - bool isUseful() override; + CastAspectOfTheCheetahAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the cheetah") {} }; class CastAspectOfThePackAction : public CastBuffSpellAction { public: - CastAspectOfThePackAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the pack") {} + CastAspectOfThePackAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the pack") {} }; class CastAspectOfTheViperAction : public CastBuffSpellAction { public: - CastAspectOfTheViperAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the viper") {} + CastAspectOfTheViperAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the viper") {} }; // Cooldown Spells @@ -72,26 +78,28 @@ class CastAspectOfTheViperAction : public CastBuffSpellAction class CastRapidFireAction : public CastBuffSpellAction { public: - CastRapidFireAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "rapid fire") {} + CastRapidFireAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "rapid fire") {} }; class CastDeterrenceAction : public CastBuffSpellAction { public: - CastDeterrenceAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "deterrence") {} + CastDeterrenceAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "deterrence") {} }; class CastReadinessAction : public CastBuffSpellAction { public: - CastReadinessAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "readiness") {} + CastReadinessAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "readiness") {} }; class CastDisengageAction : public CastSpellAction { public: CastDisengageAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "disengage") {} - bool Execute(Event event) override; bool isUseful() override; }; @@ -101,14 +109,15 @@ class CastDisengageAction : public CastSpellAction class CastScareBeastAction : public CastSpellAction { public: - CastScareBeastAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "scare beast") {} + CastScareBeastAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "scare beast") {} }; class CastScareBeastCcAction : public CastSpellAction { public: - CastScareBeastCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "scare beast on cc") {} - + CastScareBeastCcAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "scare beast on cc") {} Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -116,34 +125,41 @@ class CastScareBeastCcAction : public CastSpellAction class CastFreezingTrap : public CastDebuffSpellAction { public: - CastFreezingTrap(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "freezing trap") {} - + CastFreezingTrap(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "freezing trap") {} Value* GetTargetValue() override; }; class CastWyvernStingAction : public CastDebuffSpellAction { public: - CastWyvernStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "wyvern sting", true) {} + CastWyvernStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "wyvern sting", true) {} }; class CastSilencingShotAction : public CastSpellAction { public: - CastSilencingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "silencing shot") {} + CastSilencingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "silencing shot") {} }; class CastConcussiveShotAction : public CastSnareSpellAction { public: - CastConcussiveShotAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "concussive shot") {} + CastConcussiveShotAction(PlayerbotAI* botAI) : + CastSnareSpellAction(botAI, "concussive shot") {} }; class CastIntimidationAction : public CastBuffSpellAction { public: - CastIntimidationAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "intimidation", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastIntimidationAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "intimidation", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; // Threat Spells @@ -151,19 +167,22 @@ class CastIntimidationAction : public CastBuffSpellAction class CastDistractingShotAction : public CastSpellAction { public: - CastDistractingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "distracting shot") {} + CastDistractingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "distracting shot") {} }; class CastMisdirectionOnMainTankAction : public BuffOnMainTankAction { public: - CastMisdirectionOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "misdirection", true) {} + CastMisdirectionOnMainTankAction(PlayerbotAI* botAI) : + BuffOnMainTankAction(botAI, "misdirection", true) {} }; class CastFeignDeathAction : public CastBuffSpellAction { public: - CastFeignDeathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feign death") {} + CastFeignDeathAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "feign death") {} }; // Pet Spells @@ -172,7 +191,6 @@ class FeedPetAction : public Action { public: FeedPetAction(PlayerbotAI* botAI) : Action(botAI, "feed pet") {} - bool Execute(Event event) override; }; @@ -186,27 +204,39 @@ class CastMendPetAction : public CastAuraSpellAction { public: CastMendPetAction(PlayerbotAI* botAI) : CastAuraSpellAction(botAI, "mend pet") {} - std::string const GetTargetName() override { return "pet target"; } + std::string const GetTargetName() override + { + return "pet target"; + } }; class CastRevivePetAction : public CastBuffSpellAction { public: - CastRevivePetAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "revive pet") {} + CastRevivePetAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "revive pet") {} }; class CastKillCommandAction : public CastBuffSpellAction { public: - CastKillCommandAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "kill command", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastKillCommandAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "kill command", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; class CastBestialWrathAction : public CastBuffSpellAction { public: - CastBestialWrathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "bestial wrath", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastBestialWrathAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "bestial wrath", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; // Direct Damage Spells @@ -215,14 +245,18 @@ class CastAutoShotAction : public CastSpellAction { public: CastAutoShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "auto shot") {} - ActionThreatType getThreatType() override { return ActionThreatType::None; } + ActionThreatType getThreatType() override + { + return ActionThreatType::None; + } bool isUseful() override; }; class CastArcaneShotAction : public CastSpellAction { public: - CastArcaneShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "arcane shot") {} + CastArcaneShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "arcane shot") {} bool isUseful() override; }; @@ -235,7 +269,8 @@ class CastAimedShotAction : public CastSpellAction class CastChimeraShotAction : public CastSpellAction { public: - CastChimeraShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "chimera shot") {} + CastChimeraShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "chimera shot") {} }; class CastSteadyShotAction : public CastSpellAction @@ -255,7 +290,8 @@ class CastKillShotAction : public CastSpellAction class CastHuntersMarkAction : public CastDebuffSpellAction { public: - CastHuntersMarkAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "hunter's mark") {} + CastHuntersMarkAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "hunter's mark") {} bool isUseful() override { // Bypass TTL check @@ -266,20 +302,23 @@ class CastHuntersMarkAction : public CastDebuffSpellAction class CastTranquilizingShotAction : public CastSpellAction { public: - CastTranquilizingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "tranquilizing shot") {} + CastTranquilizingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "tranquilizing shot") {} }; class CastViperStingAction : public CastDebuffSpellAction { public: - CastViperStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "viper sting", true) {} + CastViperStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "viper sting", true) {} bool isUseful() override; }; class CastSerpentStingAction : public CastDebuffSpellAction { public: - CastSerpentStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "serpent sting", true) {} + CastSerpentStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "serpent sting", true) {} bool isUseful() override { // Bypass TTL check @@ -290,7 +329,8 @@ class CastSerpentStingAction : public CastDebuffSpellAction class CastScorpidStingAction : public CastDebuffSpellAction { public: - CastScorpidStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "scorpid sting", true) {} + CastScorpidStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "scorpid sting", true) {} bool isUseful() override { // Bypass TTL check @@ -301,7 +341,8 @@ class CastScorpidStingAction : public CastDebuffSpellAction class CastSerpentStingOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastSerpentStingOnAttackerAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "serpent sting", true) {} + CastSerpentStingOnAttackerAction(PlayerbotAI* botAI) + : CastDebuffSpellOnAttackerAction(botAI, "serpent sting", true) {} bool isUseful() override { // Bypass TTL check @@ -312,20 +353,23 @@ class CastSerpentStingOnAttackerAction : public CastDebuffSpellOnAttackerAction class CastImmolationTrapAction : public CastSpellAction { public: - CastImmolationTrapAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "immolation trap") {} + CastImmolationTrapAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "immolation trap") {} bool isUseful() override; }; class CastExplosiveTrapAction : public CastSpellAction { public: - CastExplosiveTrapAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "explosive trap") {} + CastExplosiveTrapAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "explosive trap") {} }; -class CastBlackArrow : public CastDebuffSpellAction +class CastBlackArrowAction : public CastDebuffSpellAction { public: - CastBlackArrow(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "black arrow", true) {} + CastBlackArrowAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "black arrow", true) {} bool isUseful() override { if (botAI->HasStrategy("trap weave", BOT_STATE_COMBAT)) @@ -335,77 +379,89 @@ class CastBlackArrow : public CastDebuffSpellAction } }; -class CastExplosiveShotAction : public CastDebuffSpellAction +class CastExplosiveShotBaseAction : public CastDebuffSpellAction { public: - CastExplosiveShotAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - bool isUseful() override - { - // Bypass TTL check - return CastAuraSpellAction::isUseful(); - } + CastExplosiveShotBaseAction(PlayerbotAI* botAI) + : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} }; // Rank 4 -class CastExplosiveShotRank4Action : public CastDebuffSpellAction +class CastExplosiveShotRank4Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank4Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60053, GetTarget()); } + CastExplosiveShotRank4Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(60053, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60053); } }; // Rank 3 -class CastExplosiveShotRank3Action : public CastDebuffSpellAction +class CastExplosiveShotRank3Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank3Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60052, GetTarget()); } + CastExplosiveShotRank3Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(60052, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60052); } }; // Rank 2 -class CastExplosiveShotRank2Action : public CastDebuffSpellAction +class CastExplosiveShotRank2Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank2Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60051, GetTarget()); } + CastExplosiveShotRank2Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(60051, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60051); } }; // Rank 1 -class CastExplosiveShotRank1Action : public CastDebuffSpellAction +class CastExplosiveShotRank1Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank1Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(53301, GetTarget()); } + CastExplosiveShotRank1Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event event) override + { + return botAI->CastSpell(53301, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(53301); } }; @@ -415,8 +471,8 @@ class CastExplosiveShotRank1Action : public CastDebuffSpellAction class CastWingClipAction : public CastSpellAction { public: - CastWingClipAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wing clip") {} - + CastWingClipAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "wing clip") {} bool isUseful() override; std::vector getPrerequisites() override; }; @@ -424,13 +480,15 @@ class CastWingClipAction : public CastSpellAction class CastRaptorStrikeAction : public CastSpellAction { public: - CastRaptorStrikeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "raptor strike") {} + CastRaptorStrikeAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "raptor strike") {} }; class CastMongooseBiteAction : public CastSpellAction { public: - CastMongooseBiteAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "mongoose bite") {} + CastMongooseBiteAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "mongoose bite") {} }; // AoE Spells @@ -445,7 +503,10 @@ class CastVolleyAction : public CastSpellAction { public: CastVolleyAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "volley") {} - ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } + ActionThreatType getThreatType() override + { + return ActionThreatType::Aoe; + } }; #endif diff --git a/src/Ai/Class/Hunter/HunterAiObjectContext.cpp b/src/Ai/Class/Hunter/HunterAiObjectContext.cpp index def1e469391..2a836ff5e14 100644 --- a/src/Ai/Class/Hunter/HunterAiObjectContext.cpp +++ b/src/Ai/Class/Hunter/HunterAiObjectContext.cpp @@ -22,7 +22,6 @@ class HunterStrategyFactoryInternal : public NamedObjectContext HunterStrategyFactoryInternal() { creators["nc"] = &HunterStrategyFactoryInternal::nc; - creators["boost"] = &HunterStrategyFactoryInternal::boost; creators["pet"] = &HunterStrategyFactoryInternal::pet; creators["cc"] = &HunterStrategyFactoryInternal::cc; creators["trap weave"] = &HunterStrategyFactoryInternal::trap_weave; @@ -34,7 +33,6 @@ class HunterStrategyFactoryInternal : public NamedObjectContext private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericHunterNonCombatStrategy(botAI); } - static Strategy* boost(PlayerbotAI* botAI) { return new HunterBoostStrategy(botAI); } static Strategy* pet(PlayerbotAI* botAI) { return new HunterPetStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new HunterCcStrategy(botAI); } static Strategy* trap_weave(PlayerbotAI* botAI) { return new HunterTrapWeaveStrategy(botAI); } @@ -51,14 +49,12 @@ class HunterBuffStrategyFactoryInternal : public NamedObjectContext { creators["bspeed"] = &HunterBuffStrategyFactoryInternal::bspeed; creators["bdps"] = &HunterBuffStrategyFactoryInternal::bdps; - creators["bmana"] = &HunterBuffStrategyFactoryInternal::bmana; creators["rnature"] = &HunterBuffStrategyFactoryInternal::rnature; } private: static Strategy* bspeed(PlayerbotAI* botAI) { return new HunterBuffSpeedStrategy(botAI); } static Strategy* bdps(PlayerbotAI* botAI) { return new HunterBuffDpsStrategy(botAI); } - static Strategy* bmana(PlayerbotAI* botAI) { return new HunterBuffManaStrategy(botAI); } static Strategy* rnature(PlayerbotAI* botAI) { return new HunterNatureResistanceStrategy(botAI); } }; @@ -67,7 +63,6 @@ class HunterTriggerFactoryInternal : public NamedObjectContext public: HunterTriggerFactoryInternal() { - creators["aspect of the viper"] = &HunterTriggerFactoryInternal::aspect_of_the_viper; creators["black arrow"] = &HunterTriggerFactoryInternal::black_arrow; creators["no stings"] = &HunterTriggerFactoryInternal::NoStings; creators["hunters pet dead"] = &HunterTriggerFactoryInternal::hunters_pet_dead; @@ -75,10 +70,9 @@ class HunterTriggerFactoryInternal : public NamedObjectContext creators["hunters pet medium health"] = &HunterTriggerFactoryInternal::hunters_pet_medium_health; creators["hunter's mark"] = &HunterTriggerFactoryInternal::hunters_mark; creators["freezing trap"] = &HunterTriggerFactoryInternal::freezing_trap; - creators["aspect of the pack"] = &HunterTriggerFactoryInternal::aspect_of_the_pack; creators["rapid fire"] = &HunterTriggerFactoryInternal::rapid_fire; - creators["aspect of the hawk"] = &HunterTriggerFactoryInternal::aspect_of_the_hawk; - creators["aspect of the monkey"] = &HunterTriggerFactoryInternal::aspect_of_the_monkey; + creators["aspect of the pack"] = &HunterTriggerFactoryInternal::aspect_of_the_pack; + creators["aspect of the dragonhawk"] = &HunterTriggerFactoryInternal::aspect_of_the_dragonhawk; creators["aspect of the wild"] = &HunterTriggerFactoryInternal::aspect_of_the_wild; creators["aspect of the viper"] = &HunterTriggerFactoryInternal::aspect_of_the_viper; creators["trueshot aura"] = &HunterTriggerFactoryInternal::trueshot_aura; @@ -107,10 +101,8 @@ class HunterTriggerFactoryInternal : public NamedObjectContext private: static Trigger* auto_shot(PlayerbotAI* botAI) { return new AutoShotTrigger(botAI); } static Trigger* scare_beast(PlayerbotAI* botAI) { return new ScareBeastTrigger(botAI); } - static Trigger* concussive_shot_on_snare_target(PlayerbotAI* botAI) - { - return new ConsussiveShotSnareTrigger(botAI); - } + static Trigger* concussive_shot_on_snare_target(PlayerbotAI* botAI) { + return new ConcussiveShotOnSnareTargetTrigger(botAI); } static Trigger* pet_not_happy(PlayerbotAI* botAI) { return new HunterPetNotHappy(botAI); } static Trigger* serpent_sting_on_attacker(PlayerbotAI* botAI) { return new SerpentStingOnAttackerTrigger(botAI); } static Trigger* trueshot_aura(PlayerbotAI* botAI) { return new TrueshotAuraTrigger(botAI); } @@ -125,18 +117,17 @@ class HunterTriggerFactoryInternal : public NamedObjectContext static Trigger* freezing_trap(PlayerbotAI* botAI) { return new FreezingTrapTrigger(botAI); } static Trigger* aspect_of_the_pack(PlayerbotAI* botAI) { return new HunterAspectOfThePackTrigger(botAI); } static Trigger* rapid_fire(PlayerbotAI* botAI) { return new RapidFireTrigger(botAI); } - static Trigger* aspect_of_the_hawk(PlayerbotAI* botAI) { return new HunterAspectOfTheHawkTrigger(botAI); } - static Trigger* aspect_of_the_monkey(PlayerbotAI* botAI) { return new HunterAspectOfTheMonkeyTrigger(botAI); } + static Trigger* aspect_of_the_dragonhawk(PlayerbotAI* botAI) { return new HunterAspectOfTheDragonhawkTrigger(botAI); } static Trigger* aspect_of_the_wild(PlayerbotAI* botAI) { return new HunterAspectOfTheWildTrigger(botAI); } static Trigger* low_ammo(PlayerbotAI* botAI) { return new HunterLowAmmoTrigger(botAI); } static Trigger* no_ammo(PlayerbotAI* botAI) { return new HunterNoAmmoTrigger(botAI); } static Trigger* has_ammo(PlayerbotAI* botAI) { return new HunterHasAmmoTrigger(botAI); } static Trigger* switch_to_melee(PlayerbotAI* botAI) { return new SwitchToMeleeTrigger(botAI); } static Trigger* switch_to_ranged(PlayerbotAI* botAI) { return new SwitchToRangedTrigger(botAI); } - static Trigger* misdirection_on_main_tank(PlayerbotAI* ai) { return new MisdirectionOnMainTankTrigger(ai); } - static Trigger* remove_enrage(PlayerbotAI* ai) { return new TargetRemoveEnrageTrigger(ai); } - static Trigger* remove_magic(PlayerbotAI* ai) { return new TargetRemoveMagicTrigger(ai); } - static Trigger* immolation_trap_no_cd(PlayerbotAI* ai) { return new ImmolationTrapNoCdTrigger(ai); } + static Trigger* misdirection_on_main_tank(PlayerbotAI* botAI) { return new MisdirectionOnMainTankTrigger(botAI); } + static Trigger* remove_enrage(PlayerbotAI* botAI) { return new TargetRemoveEnrageTrigger(botAI); } + static Trigger* remove_magic(PlayerbotAI* botAI) { return new TargetRemoveMagicTrigger(botAI); } + static Trigger* immolation_trap_no_cd(PlayerbotAI* botAI) { return new ImmolationTrapNoCdTrigger(botAI); } static Trigger* kill_command(PlayerbotAI* botAI) { return new KillCommandTrigger(botAI); } static Trigger* explosive_shot(PlayerbotAI* botAI) { return new ExplosiveShotTrigger(botAI); } static Trigger* lock_and_load(PlayerbotAI* botAI) { return new LockAndLoadTrigger(botAI); } @@ -153,7 +144,6 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["auto shot"] = &HunterAiObjectContextInternal::auto_shot; creators["aimed shot"] = &HunterAiObjectContextInternal::aimed_shot; creators["chimera shot"] = &HunterAiObjectContextInternal::chimera_shot; - creators["explosive shot"] = &HunterAiObjectContextInternal::explosive_shot; creators["arcane shot"] = &HunterAiObjectContextInternal::arcane_shot; creators["concussive shot"] = &HunterAiObjectContextInternal::concussive_shot; creators["distracting shot"] = &HunterAiObjectContextInternal::distracting_shot; @@ -176,6 +166,7 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["deterrence"] = &HunterAiObjectContextInternal::deterrence; creators["readiness"] = &HunterAiObjectContextInternal::readiness; creators["aspect of the hawk"] = &HunterAiObjectContextInternal::aspect_of_the_hawk; + creators["aspect of the dragonhawk"] = &HunterAiObjectContextInternal::aspect_of_the_dragonhawk; creators["aspect of the monkey"] = &HunterAiObjectContextInternal::aspect_of_the_monkey; creators["aspect of the wild"] = &HunterAiObjectContextInternal::aspect_of_the_wild; creators["aspect of the viper"] = &HunterAiObjectContextInternal::aspect_of_the_viper; @@ -191,7 +182,6 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["bestial wrath"] = &HunterAiObjectContextInternal::bestial_wrath; creators["scare beast"] = &HunterAiObjectContextInternal::scare_beast; creators["scare beast on cc"] = &HunterAiObjectContextInternal::scare_beast_on_cc; - creators["aspect of the dragonhawk"] = &HunterAiObjectContextInternal::aspect_of_the_dragonhawk; creators["tranquilizing shot"] = &HunterAiObjectContextInternal::tranquilizing_shot; creators["steady shot"] = &HunterAiObjectContextInternal::steady_shot; creators["kill shot"] = &HunterAiObjectContextInternal::kill_shot; @@ -200,6 +190,7 @@ class HunterAiObjectContextInternal : public NamedObjectContext creators["disengage"] = &HunterAiObjectContextInternal::disengage; creators["immolation trap"] = &HunterAiObjectContextInternal::immolation_trap; creators["explosive trap"] = &HunterAiObjectContextInternal::explosive_trap; + creators["explosive shot base"] = &HunterAiObjectContextInternal::explosive_shot_base; creators["explosive shot rank 4"] = &HunterAiObjectContextInternal::explosive_shot_rank_4; creators["explosive shot rank 3"] = &HunterAiObjectContextInternal::explosive_shot_rank_3; creators["explosive shot rank 2"] = &HunterAiObjectContextInternal::explosive_shot_rank_2; @@ -218,7 +209,6 @@ class HunterAiObjectContextInternal : public NamedObjectContext static Action* auto_shot(PlayerbotAI* botAI) { return new CastAutoShotAction(botAI); } static Action* aimed_shot(PlayerbotAI* botAI) { return new CastAimedShotAction(botAI); } static Action* chimera_shot(PlayerbotAI* botAI) { return new CastChimeraShotAction(botAI); } - static Action* explosive_shot(PlayerbotAI* botAI) { return new CastExplosiveShotAction(botAI); } static Action* arcane_shot(PlayerbotAI* botAI) { return new CastArcaneShotAction(botAI); } static Action* concussive_shot(PlayerbotAI* botAI) { return new CastConcussiveShotAction(botAI); } static Action* distracting_shot(PlayerbotAI* botAI) { return new CastDistractingShotAction(botAI); } @@ -234,12 +224,13 @@ class HunterAiObjectContextInternal : public NamedObjectContext static Action* kill_command(PlayerbotAI* botAI) { return new CastKillCommandAction(botAI); } static Action* revive_pet(PlayerbotAI* botAI) { return new CastRevivePetAction(botAI); } static Action* call_pet(PlayerbotAI* botAI) { return new CastCallPetAction(botAI); } - static Action* black_arrow(PlayerbotAI* botAI) { return new CastBlackArrow(botAI); } + static Action* black_arrow(PlayerbotAI* botAI) { return new CastBlackArrowAction(botAI); } static Action* freezing_trap(PlayerbotAI* botAI) { return new CastFreezingTrap(botAI); } static Action* rapid_fire(PlayerbotAI* botAI) { return new CastRapidFireAction(botAI); } static Action* deterrence(PlayerbotAI* botAI) { return new CastDeterrenceAction(botAI); } static Action* readiness(PlayerbotAI* botAI) { return new CastReadinessAction(botAI); } static Action* aspect_of_the_hawk(PlayerbotAI* botAI) { return new CastAspectOfTheHawkAction(botAI); } + static Action* aspect_of_the_dragonhawk(PlayerbotAI* botAI) { return new CastAspectOfTheDragonhawkAction(botAI); } static Action* aspect_of_the_monkey(PlayerbotAI* botAI) { return new CastAspectOfTheMonkeyAction(botAI); } static Action* aspect_of_the_wild(PlayerbotAI* botAI) { return new CastAspectOfTheWildAction(botAI); } static Action* aspect_of_the_viper(PlayerbotAI* botAI) { return new CastAspectOfTheViperAction(botAI); } @@ -248,20 +239,20 @@ class HunterAiObjectContextInternal : public NamedObjectContext static Action* wing_clip(PlayerbotAI* botAI) { return new CastWingClipAction(botAI); } static Action* raptor_strike(PlayerbotAI* botAI) { return new CastRaptorStrikeAction(botAI); } static Action* mongoose_bite(PlayerbotAI* botAI) { return new CastMongooseBiteAction(botAI); } - static Action* aspect_of_the_dragonhawk(PlayerbotAI* ai) { return new CastAspectOfTheDragonhawkAction(ai); } - static Action* tranquilizing_shot(PlayerbotAI* ai) { return new CastTranquilizingShotAction(ai); } - static Action* steady_shot(PlayerbotAI* ai) { return new CastSteadyShotAction(ai); } - static Action* kill_shot(PlayerbotAI* ai) { return new CastKillShotAction(ai); } - static Action* misdirection_on_main_tank(PlayerbotAI* ai) { return new CastMisdirectionOnMainTankAction(ai); } - static Action* silencing_shot(PlayerbotAI* ai) { return new CastSilencingShotAction(ai); } - static Action* disengage(PlayerbotAI* ai) { return new CastDisengageAction(ai); } - static Action* immolation_trap(PlayerbotAI* ai) { return new CastImmolationTrapAction(ai); } - static Action* explosive_trap(PlayerbotAI* ai) { return new CastExplosiveTrapAction(ai); } - static Action* explosive_shot_rank_4(PlayerbotAI* ai) { return new CastExplosiveShotRank4Action(ai); } - static Action* explosive_shot_rank_3(PlayerbotAI* ai) { return new CastExplosiveShotRank3Action(ai); } - static Action* explosive_shot_rank_2(PlayerbotAI* ai) { return new CastExplosiveShotRank2Action(ai); } - static Action* explosive_shot_rank_1(PlayerbotAI* ai) { return new CastExplosiveShotRank1Action(ai); } - static Action* intimidation(PlayerbotAI* ai) { return new CastIntimidationAction(ai); } + static Action* tranquilizing_shot(PlayerbotAI* botAI) { return new CastTranquilizingShotAction(botAI); } + static Action* steady_shot(PlayerbotAI* botAI) { return new CastSteadyShotAction(botAI); } + static Action* kill_shot(PlayerbotAI* botAI) { return new CastKillShotAction(botAI); } + static Action* misdirection_on_main_tank(PlayerbotAI* botAI) { return new CastMisdirectionOnMainTankAction(botAI); } + static Action* silencing_shot(PlayerbotAI* botAI) { return new CastSilencingShotAction(botAI); } + static Action* disengage(PlayerbotAI* botAI) { return new CastDisengageAction(botAI); } + static Action* immolation_trap(PlayerbotAI* botAI) { return new CastImmolationTrapAction(botAI); } + static Action* explosive_trap(PlayerbotAI* botAI) { return new CastExplosiveTrapAction(botAI); } + static Action* explosive_shot_base(PlayerbotAI* botAI) { return new CastExplosiveShotBaseAction(botAI); } + static Action* explosive_shot_rank_4(PlayerbotAI* botAI) { return new CastExplosiveShotRank4Action(botAI); } + static Action* explosive_shot_rank_3(PlayerbotAI* botAI) { return new CastExplosiveShotRank3Action(botAI); } + static Action* explosive_shot_rank_2(PlayerbotAI* botAI) { return new CastExplosiveShotRank2Action(botAI); } + static Action* explosive_shot_rank_1(PlayerbotAI* botAI) { return new CastExplosiveShotRank1Action(botAI); } + static Action* intimidation(PlayerbotAI* botAI) { return new CastIntimidationAction(botAI); } }; SharedNamedObjectContextList HunterAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp index a2c302d379b..124da0b135f 100644 --- a/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp @@ -6,41 +6,9 @@ #include "BeastMasteryHunterStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class BeastMasteryHunterStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - BeastMasteryHunterStrategyActionNodeFactory() - { - creators["auto shot"] = &auto_shot; - creators["kill command"] = &kill_command; - creators["kill shot"] = &kill_shot; - creators["viper sting"] = &viper_sting; - creators["serpent sting"] = serpent_sting; - creators["aimed shot"] = &aimed_shot; - creators["arcane shot"] = &arcane_shot; - creators["steady shot"] = &steady_shot; - creators["multi-shot"] = &multi_shot; - creators["volley"] = &volley; - } - -private: - static ActionNode* auto_shot(PlayerbotAI*) { return new ActionNode("auto shot", {}, {}, {}); } - static ActionNode* kill_command(PlayerbotAI*) { return new ActionNode("kill command", {}, {}, {}); } - static ActionNode* kill_shot(PlayerbotAI*) { return new ActionNode("kill shot", {}, {}, {}); } - static ActionNode* viper_sting(PlayerbotAI*) { return new ActionNode("viper sting", {}, {}, {}); } - static ActionNode* serpent_sting(PlayerbotAI*) { return new ActionNode("serpent sting", {}, {}, {}); } - static ActionNode* aimed_shot(PlayerbotAI*) { return new ActionNode("aimed shot", {}, {}, {}); } - static ActionNode* arcane_shot(PlayerbotAI*) { return new ActionNode("arcane shot", {}, {}, {}); } - static ActionNode* steady_shot(PlayerbotAI*) { return new ActionNode("steady shot", {}, {}, {}); } - static ActionNode* multi_shot(PlayerbotAI*) { return new ActionNode("multi shot", {}, {}, {}); } - static ActionNode* volley(PlayerbotAI*) { return new ActionNode("volley", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== BeastMasteryHunterStrategy::BeastMasteryHunterStrategy(PlayerbotAI* botAI) : GenericHunterStrategy(botAI) { - actionNodeFactories.Add(new BeastMasteryHunterStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp b/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp index 00c35dc2a46..0093a490b3e 100644 --- a/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp @@ -4,62 +4,31 @@ */ #include "GenericHunterNonCombatStrategy.h" - #include "Playerbots.h" -class GenericHunterNonCombatStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - GenericHunterNonCombatStrategyActionNodeFactory() - { - creators["rapid fire"] = &rapid_fire; - creators["boost"] = &rapid_fire; - creators["aspect of the pack"] = &aspect_of_the_pack; - } - -private: - static ActionNode* rapid_fire([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("rapid fire", - /*P*/ {}, - /*A*/ { NextAction("readiness")}, - /*C*/ {}); - } - - static ActionNode* aspect_of_the_pack([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("aspect of the pack", - /*P*/ {}, - /*A*/ { NextAction("aspect of the cheetah")}, - /*C*/ {}); - } -}; - GenericHunterNonCombatStrategy::GenericHunterNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) { - actionNodeFactories.Add(new GenericHunterNonCombatStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } void GenericHunterNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("trueshot aura", { NextAction("trueshot aura", 2.0f)})); - triggers.push_back(new TriggerNode("often", { - NextAction("apply stone", 1.0f), - NextAction("apply oil", 1.0f), - })); - triggers.push_back(new TriggerNode("low ammo", { NextAction("say::low ammo", ACTION_NORMAL)})); - triggers.push_back(new TriggerNode("no track", { NextAction("track humanoids", ACTION_NORMAL)})); - triggers.push_back(new TriggerNode("no ammo", { NextAction("equip upgrades packet action", ACTION_HIGH + 1)})); + triggers.push_back(new TriggerNode("trueshot aura", { NextAction("trueshot aura", 2.0f) })); + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f), + NextAction("apply oil", 1.0f) })); + triggers.push_back(new TriggerNode("low ammo", { NextAction("say::low ammo", ACTION_NORMAL) })); + triggers.push_back(new TriggerNode("no track", { NextAction("track humanoids", ACTION_NORMAL) })); + triggers.push_back(new TriggerNode("no ammo", { NextAction("equip upgrades packet action", ACTION_HIGH + 1) })); } void HunterPetStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("no pet", { NextAction("call pet", 60.0f)})); - triggers.push_back(new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f)})); - triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f)})); - triggers.push_back(new TriggerNode("pet not happy", { NextAction("feed pet", 60.0f)})); - triggers.push_back(new TriggerNode("hunters pet medium health", { NextAction("mend pet", 60.0f)})); - triggers.push_back(new TriggerNode("hunters pet dead", { NextAction("revive pet", 60.0f)})); + triggers.push_back(new TriggerNode("no pet", { NextAction("call pet", 60.0f) })); + triggers.push_back(new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); + triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + triggers.push_back(new TriggerNode("pet not happy", { NextAction("feed pet", 60.0f) })); + triggers.push_back(new TriggerNode("hunters pet medium health", { NextAction("mend pet", 60.0f) })); + triggers.push_back(new TriggerNode("hunters pet dead", { NextAction("revive pet", 60.0f) })); } diff --git a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp index 0a9e88a5405..eb3907dd71b 100644 --- a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp @@ -11,11 +11,6 @@ class GenericHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) triggers.push_back(new TriggerNode("hunter's mark", { NextAction("hunter's mark", 29.5f) })); triggers.push_back(new TriggerNode("rapid fire", { NextAction("rapid fire", 29.0f) })); triggers.push_back(new TriggerNode("aspect of the viper", { NextAction("aspect of the viper", 28.0f) })); - triggers.push_back(new TriggerNode("aspect of the hawk", { NextAction("aspect of the dragonhawk", 27.5f) })); // Aggro/Threat/Defensive Triggers triggers.push_back(new TriggerNode("has aggro", { NextAction("concussive shot", 20.0f) })); @@ -118,14 +76,12 @@ void GenericHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("tranquilizing shot magic", { NextAction("tranquilizing shot", 61.0f) })); // Ranged-based Triggers - triggers.push_back(new TriggerNode("enemy within melee", { - NextAction("explosive trap", 37.0f), - NextAction("mongoose bite", 22.0f), - NextAction("wing clip", 21.0f) })); + triggers.push_back(new TriggerNode("enemy within melee", { NextAction("explosive trap", 37.0f), + NextAction("mongoose bite", 22.0f), + NextAction("wing clip", 21.0f) })); - triggers.push_back(new TriggerNode("enemy too close for auto shot", { - NextAction("disengage", 35.0f), - NextAction("flee", 34.0f) })); + triggers.push_back(new TriggerNode("enemy too close for auto shot", { NextAction("disengage", 35.0f), + NextAction("flee", 34.0f) })); } // ===== AoE Strategy, 2/3+ enemies ===== @@ -138,10 +94,6 @@ void AoEHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("light aoe", { NextAction("multi-shot", 21.0f) })); } -void HunterBoostStrategy::InitTriggers(std::vector& triggers) -{ -} - void HunterCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("scare beast", { NextAction("scare beast on cc", 23.0f) })); diff --git a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h index 01aef4cecca..b3a2248c377 100644 --- a/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h +++ b/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h @@ -30,15 +30,6 @@ class AoEHunterStrategy : public CombatStrategy std::string const getName() override { return "aoe"; } }; -class HunterBoostStrategy : public Strategy -{ -public: - HunterBoostStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - - std::string const getName() override { return "boost"; } - void InitTriggers(std::vector& triggers) override; -}; - class HunterCcStrategy : public Strategy { public: diff --git a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp index 3601d771198..6333f184c38 100644 --- a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp +++ b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp @@ -10,9 +10,21 @@ class BuffHunterStrategyActionNodeFactory : public NamedObjectFactory { public: - BuffHunterStrategyActionNodeFactory() { creators["aspect of the hawk"] = &aspect_of_the_hawk; } + BuffHunterStrategyActionNodeFactory() + { + creators["aspect of the dragonhawk"] = &aspect_of_the_dragonhawk; + creators["aspect of the hawk"] = &aspect_of_the_hawk; + creators["aspect of the pack"] = &aspect_of_the_pack; + } private: + static ActionNode* aspect_of_the_dragonhawk([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("aspect of the dragonhawk", + /*P*/ {}, + /*A*/ { NextAction("aspect of the hawk") }, + /*C*/ {}); + } static ActionNode* aspect_of_the_hawk([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("aspect of the hawk", @@ -20,9 +32,16 @@ class BuffHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) { triggers.push_back( - new TriggerNode("aspect of the hawk", { NextAction("aspect of the dragonhawk", 20.1f), - NextAction("aspect of the hawk", 20.0f) })); + new TriggerNode( + "aspect of the dragonhawk", + { + NextAction("aspect of the dragonhawk", ACTION_HIGH) + } + ) + ); } void HunterNatureResistanceStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("aspect of the wild", - { NextAction("aspect of the wild", 20.0f) })); + triggers.push_back( + new TriggerNode( + "aspect of the wild", + { + NextAction("aspect of the wild", ACTION_HIGH) + } + ) + ); } void HunterBuffSpeedStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("aspect of the pack", - { NextAction("aspect of the pack", 20.0f) })); -} - -void HunterBuffManaStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back(new TriggerNode("aspect of the viper", - { NextAction("aspect of the viper", 20.0f) })); + triggers.push_back( + new TriggerNode( + "aspect of the pack", + { + NextAction("aspect of the pack", ACTION_HIGH) + } + ) + ); } diff --git a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h index 7df8bc1bc38..93155caebbe 100644 --- a/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h +++ b/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h @@ -6,44 +6,35 @@ #ifndef _PLAYERBOT_HUNTERBUFFSTRATEGIES_H #define _PLAYERBOT_HUNTERBUFFSTRATEGIES_H -#include "NonCombatStrategy.h" +#include "Strategy.h" class PlayerbotAI; -class HunterBuffSpeedStrategy : public NonCombatStrategy +class HunterBuffSpeedStrategy : public Strategy { public: - HunterBuffSpeedStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + HunterBuffSpeedStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - std::string const getName() override { return "bspeed"; } - void InitTriggers(std::vector& triggers) override; -}; - -class HunterBuffManaStrategy : public NonCombatStrategy -{ -public: - HunterBuffManaStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} - - std::string const getName() override { return "bmana"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "bspeed"; } }; -class HunterBuffDpsStrategy : public NonCombatStrategy +class HunterBuffDpsStrategy : public Strategy { public: HunterBuffDpsStrategy(PlayerbotAI* botAI); - std::string const getName() override { return "bdps"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "bdps"; } }; -class HunterNatureResistanceStrategy : public NonCombatStrategy +class HunterNatureResistanceStrategy : public Strategy { public: - HunterNatureResistanceStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + HunterNatureResistanceStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - std::string const getName() override { return "rnature"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "rnature"; } }; #endif diff --git a/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp index e4e8a9a4f44..8289b90bf89 100644 --- a/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp @@ -6,45 +6,9 @@ #include "MarksmanshipHunterStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class MarksmanshipHunterStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - MarksmanshipHunterStrategyActionNodeFactory() - { - creators["auto shot"] = &auto_shot; - creators["silencing shot"] = &silencing_shot; - creators["kill command"] = &kill_command; - creators["kill shot"] = &kill_shot; - creators["viper sting"] = &viper_sting; - creators["serpent sting"] = serpent_sting; - creators["chimera shot"] = &chimera_shot; - creators["aimed shot"] = &aimed_shot; - creators["arcane shot"] = &arcane_shot; - creators["steady shot"] = &steady_shot; - creators["multi-shot"] = &multi_shot; - creators["volley"] = &volley; - } - -private: - static ActionNode* auto_shot(PlayerbotAI*) { return new ActionNode("auto shot", {}, {}, {}); } - static ActionNode* silencing_shot(PlayerbotAI*) { return new ActionNode("silencing shot", {}, {}, {}); } - static ActionNode* kill_command(PlayerbotAI*) { return new ActionNode("kill command", {}, {}, {}); } - static ActionNode* kill_shot(PlayerbotAI*) { return new ActionNode("kill shot", {}, {}, {}); } - static ActionNode* viper_sting(PlayerbotAI*) { return new ActionNode("viper sting", {}, {}, {}); } - static ActionNode* serpent_sting(PlayerbotAI*) { return new ActionNode("serpent sting", {}, {}, {}); } - static ActionNode* chimera_shot(PlayerbotAI*) { return new ActionNode("chimera shot", {}, {}, {}); } - static ActionNode* aimed_shot(PlayerbotAI*) { return new ActionNode("aimed shot", {}, {}, {}); } - static ActionNode* arcane_shot(PlayerbotAI*) { return new ActionNode("arcane shot", {}, {}, {}); } - static ActionNode* steady_shot(PlayerbotAI*) { return new ActionNode("steady shot", {}, {}, {}); } - static ActionNode* multi_shot(PlayerbotAI*) { return new ActionNode("multi shot", {}, {}, {}); } - static ActionNode* volley(PlayerbotAI*) { return new ActionNode("volley", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== MarksmanshipHunterStrategy::MarksmanshipHunterStrategy(PlayerbotAI* botAI) : GenericHunterStrategy(botAI) { - actionNodeFactories.Add(new MarksmanshipHunterStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp b/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp index 796891a9b0b..cc8f2a64c1b 100644 --- a/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp +++ b/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp @@ -12,36 +12,35 @@ class SurvivalHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) } ) ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 3", 27.5f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 2", 27.0f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 1", 26.5f) - } - ) - ); triggers.push_back( new TriggerNode( "kill command", diff --git a/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp b/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp index 972332d1a0a..733960bca3e 100644 --- a/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp +++ b/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp @@ -16,8 +16,7 @@ bool KillCommandTrigger::IsActive() { - Unit* target = GetTarget(); - return !botAI->HasAura("kill command", target); + return !botAI->HasAura("kill command", GetTarget()); } bool BlackArrowTrigger::IsActive() @@ -26,36 +25,46 @@ bool BlackArrowTrigger::IsActive() return false; return DebuffTrigger::IsActive(); - return BuffTrigger::IsActive(); } -bool HunterAspectOfTheHawkTrigger::IsActive() +bool HunterAspectOfTheDragonhawkTrigger::IsActive() { Unit* target = GetTarget(); - return SpellTrigger::IsActive() && !botAI->HasAura("aspect of the hawk", target) && - !botAI->HasAura("aspect of the dragonhawk", target) && - (!AI_VALUE2(bool, "has mana", "self target") || AI_VALUE2(uint8, "mana", "self target") > 70); + if (!target) + return false; + + if (!SpellTrigger::IsActive()) + return false; + + if (botAI->HasAura("aspect of the hawk", target) || + botAI->HasAura("aspect of the dragonhawk", target)) + return false; + + if (botAI->HasAura("aspect of the viper", target)) + return AI_VALUE2(uint8, "mana", "self target") >= 60; + + return true; } bool HunterNoStingsActiveTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); - return DebuffTrigger::IsActive() && target && !botAI->HasAura("serpent sting", target, false, true) && - !botAI->HasAura("scorpid sting", target, false, true) && !botAI->HasAura("viper sting", target, false, true); - return BuffTrigger::IsActive(); + return DebuffTrigger::IsActive() && target && + !botAI->HasAura("serpent sting", target, false, true) && + !botAI->HasAura("scorpid sting", target, false, true) && + !botAI->HasAura("viper sting", target, false, true); } bool HuntersPetDeadTrigger::IsActive() { - // Unit* pet = AI_VALUE(Unit*, "pet target"); - // return pet && AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); return AI_VALUE(bool, "pet dead") && !AI_VALUE2(bool, "mounted", "self target"); } bool HuntersPetLowHealthTrigger::IsActive() { Unit* pet = AI_VALUE(Unit*, "pet target"); - return pet && AI_VALUE2(uint8, "health", "pet target") < 40 && !AI_VALUE2(bool, "dead", "pet target") && + return pet && AI_VALUE2(uint8, "health", "pet target") < 40 && + !AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); } @@ -73,9 +82,14 @@ bool HunterPetNotHappy::IsActive() bool HunterAspectOfTheViperTrigger::IsActive() { - return SpellTrigger::IsActive() && !botAI->HasAura(spell, GetTarget()) && + if (botAI->HasStrategy("rnature", BotState::BOT_STATE_COMBAT) || + botAI->HasStrategy("rnature", BotState::BOT_STATE_NON_COMBAT) || + botAI->HasStrategy("bspeed", BotState::BOT_STATE_COMBAT) || + botAI->HasStrategy("bspeed", BotState::BOT_STATE_NON_COMBAT)) + return false; + + return BuffTrigger::IsActive() && AI_VALUE2(uint8, "mana", "self target") < (sPlayerbotAIConfig.lowMana / 2); - ; } bool HunterAspectOfThePackTrigger::IsActive() @@ -85,11 +99,14 @@ bool HunterAspectOfThePackTrigger::IsActive() bool HunterLowAmmoTrigger::IsActive() { - return bot->GetGroup() && (AI_VALUE2(uint32, "item count", "ammo") < 100) && - (AI_VALUE2(uint32, "item count", "ammo") > 0); + uint32 ammoCount = AI_VALUE2(uint32, "item count", "ammo"); + return bot->GetGroup() && ammoCount > 0 && ammoCount < 100; } -bool HunterHasAmmoTrigger::IsActive() { return !AmmoCountTrigger::IsActive(); } +bool HunterHasAmmoTrigger::IsActive() +{ + return !AmmoCountTrigger::IsActive(); +} bool SwitchToRangedTrigger::IsActive() { @@ -131,6 +148,7 @@ bool NoTrackTrigger::IsActive() if (botAI->HasAura(track, bot)) return false; } + return true; } @@ -138,17 +156,17 @@ bool SerpentStingOnAttackerTrigger::IsActive() { if (!DebuffOnAttackerTrigger::IsActive()) return false; + Unit* target = GetTarget(); if (!target) - { return false; - } + return !botAI->HasAura("scorpid sting", target, false, true) && !botAI->HasAura("viper sting", target, false, true); - return BuffTrigger::IsActive(); } -const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = { +const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = +{ 1510, // Volley Rank 1 14294, // Volley Rank 2 14295, // Volley Rank 3 @@ -159,19 +177,12 @@ const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = { bool VolleyChannelCheckTrigger::IsActive() { - Player* bot = botAI->GetBot(); - - // Check if the bot is channeling a spell - if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + spell && VOLLEY_SPELL_IDS.count(spell->m_spellInfo->Id)) { - // Only trigger if the spell being channeled is Volley - if (VOLLEY_SPELL_IDS.count(spell->m_spellInfo->Id)) - { - uint8 attackerCount = AI_VALUE(uint8, "attacker count"); - return attackerCount < minEnemies; - } + uint8 attackerCount = AI_VALUE(uint8, "attacker count"); + return attackerCount < minEnemies; } - // Not channeling Volley return false; } diff --git a/src/Ai/Class/Hunter/Trigger/HunterTriggers.h b/src/Ai/Class/Hunter/Trigger/HunterTriggers.h index 6bc5f3c44aa..7459f94bbc1 100644 --- a/src/Ai/Class/Hunter/Trigger/HunterTriggers.h +++ b/src/Ai/Class/Hunter/Trigger/HunterTriggers.h @@ -16,16 +16,10 @@ class PlayerbotAI; // Buff and Out of Combat Triggers -class HunterAspectOfTheMonkeyTrigger : public BuffTrigger +class HunterAspectOfTheDragonhawkTrigger : public BuffTrigger { public: - HunterAspectOfTheMonkeyTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the monkey") {} -}; - -class HunterAspectOfTheHawkTrigger : public BuffTrigger -{ -public: - HunterAspectOfTheHawkTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the hawk") {} + HunterAspectOfTheDragonhawkTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the dragonhawk") {} bool IsActive() override; }; @@ -130,10 +124,10 @@ class FreezingTrapTrigger : public HasCcTargetTrigger FreezingTrapTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "freezing trap") {} }; -class ConsussiveShotSnareTrigger : public SnareTargetTrigger +class ConcussiveShotOnSnareTargetTrigger : public SnareTargetTrigger { public: - ConsussiveShotSnareTrigger(PlayerbotAI* botAI) : SnareTargetTrigger(botAI, "concussive shot") {} + ConcussiveShotOnSnareTargetTrigger(PlayerbotAI* botAI) : SnareTargetTrigger(botAI, "concussive shot") {} }; class ScareBeastTrigger : public HasCcTargetTrigger @@ -212,25 +206,25 @@ class SwitchToMeleeTrigger : public Trigger class MisdirectionOnMainTankTrigger : public BuffOnMainTankTrigger { public: - MisdirectionOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "misdirection", true) {} + MisdirectionOnMainTankTrigger(PlayerbotAI* botAI) : BuffOnMainTankTrigger(botAI, "misdirection", true) {} }; class TargetRemoveEnrageTrigger : public TargetAuraDispelTrigger { public: - TargetRemoveEnrageTrigger(PlayerbotAI* ai) : TargetAuraDispelTrigger(ai, "tranquilizing shot", DISPEL_ENRAGE) {} + TargetRemoveEnrageTrigger(PlayerbotAI* botAI) : TargetAuraDispelTrigger(botAI, "tranquilizing shot", DISPEL_ENRAGE) {} }; class TargetRemoveMagicTrigger : public TargetAuraDispelTrigger { public: - TargetRemoveMagicTrigger(PlayerbotAI* ai) : TargetAuraDispelTrigger(ai, "tranquilizing shot", DISPEL_MAGIC) {} + TargetRemoveMagicTrigger(PlayerbotAI* botAI) : TargetAuraDispelTrigger(botAI, "tranquilizing shot", DISPEL_MAGIC) {} }; class ImmolationTrapNoCdTrigger : public SpellNoCooldownTrigger { public: - ImmolationTrapNoCdTrigger(PlayerbotAI* ai) : SpellNoCooldownTrigger(ai, "immolation trap") {} + ImmolationTrapNoCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "immolation trap") {} }; BEGIN_TRIGGER(HuntersPetDeadTrigger, Trigger) diff --git a/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp b/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp index c7f25e33174..710c15e0a03 100644 --- a/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp +++ b/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp @@ -647,7 +647,7 @@ float IccSindragosaMultiplier::GetValue(Action* 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; } @@ -774,7 +774,7 @@ float IccLichKingAddsMultiplier::GetValue(Action* 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)) + dynamic_cast(action) || dynamic_cast(action)) return 0.0f; } diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index 84b3a7dc53a..6121789a940 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -363,7 +363,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa else engine->addStrategiesNoInit("surv", nullptr); - engine->addStrategiesNoInit("cc", "dps assist", "aoe", nullptr); + engine->addStrategiesNoInit("cc", "dps assist", "aoe", "bdps", nullptr); break; case CLASS_ROGUE: if (tab == ROGUE_TAB_ASSASSINATION || tab == ROGUE_TAB_SUBTLETY) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 506ad9154fd..b66623c5825 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -3307,7 +3307,7 @@ void PlayerbotFactory::InitReagents() break; case CLASS_PALADIN: if (level >= 52) - items.push_back({21177, 80}); // Symbol of Kings + items.push_back({21177, 100}); // Symbol of Kings break; case CLASS_PRIEST: if (level >= 48 && level < 56) From 496d6c9e4c21a5bb8aa8890ea8d02ffcf0d01cda Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:24:55 -0400 Subject: [PATCH 38/87] Blood dk rotation update (#2227) ## Pull Request Description Improve the blood DK rotation, add hysteria use, improve blood pact use. Basic Blood priority should be: 1. Keep diseases up 2. Use rune strike whenever possible 3. Use frost and death runes on Icy Touch (highest threat ability) 4. Use unholy runes on death strike to trigger more death runes 5. use blood runes on heart/blood strike Hysteria should be used on a physical dps, or a tank if no physical dps is available. Never a caster or healer. Summon ghoul should be saved for death pact usage. They are honestly a liability that aggros everything in range without the unholy talent that turns them into a pet anyways. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. Removed plague strike from the general rotation. It should only be used for disease application. Added death strike on "high unholy rune" trigger instead. Remove raise dead from generic dk. Only cast it right before death pact. Add hysteria to bloods default actions. - Describe the **processing cost** when this logic executes across many bots. Minimal change ## How to Test the Changes To test rotation changes: Use a combat parsing addon such as Details! or watch the combat log of a Blood DK bot. They should now use death strike instead of using unholy runes on plague strike. To test hysteria: Ungrouped blood dk bots can be observed using it on themselves. Create a group with the blood dk + casters/healers only. Observe that they never use hysteria on the casters. Create a group with blood dk + physical dps. Observe that they use hysteria on a dps and not themselves. To test blood pact: I used GM command .damage to get my Blood DK bot into "critical health" trigger. They will use raise dead then blood pact. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Used for brainstorming and exploring the codebase to find similar patterns. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Class/Dk/Action/DKActions.cpp | 52 +++++++++++++++++++ src/Ai/Class/Dk/Action/DKActions.h | 7 +++ src/Ai/Class/Dk/DKAiObjectContext.cpp | 6 ++- src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp | 32 +++++++++--- src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp | 1 - .../Strategy/GenericDKNonCombatStrategy.cpp | 6 --- .../Class/Dk/Strategy/GenericDKStrategy.cpp | 16 +++--- src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp | 14 ++--- src/Ai/Class/Dk/Trigger/DKTriggers.h | 6 +++ 9 files changed, 109 insertions(+), 31 deletions(-) diff --git a/src/Ai/Class/Dk/Action/DKActions.cpp b/src/Ai/Class/Dk/Action/DKActions.cpp index 7788e481907..5cd475e81a6 100644 --- a/src/Ai/Class/Dk/Action/DKActions.cpp +++ b/src/Ai/Class/Dk/Action/DKActions.cpp @@ -48,3 +48,55 @@ bool CastRaiseDeadAction::Execute(Event event) return true; } + +Unit* CastHysteriaAction::GetTarget() +{ + Group* group = bot->GetGroup(); + if (!group) + { + if (!bot->HasAura(49016)) + return bot; + return nullptr; + } + + Unit* rangedDps = nullptr; + Unit* tank = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->GetMap() != bot->GetMap() || bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance) + continue; + + // Skip if already has hysteria + if (member->HasAura(49016)) + continue; + + // Priority 1: Melee DPS + if (botAI->IsMelee(member) && botAI->IsDps(member)) + return member; + + // Priority 2: Ranged DPS (physical, not casters) + if (!rangedDps && botAI->IsRanged(member) && botAI->IsDps(member) && !botAI->IsCaster(member)) + rangedDps = member; + + // Priority 3: Tank + if (!tank && botAI->IsTank(member)) + tank = member; + } + + if (rangedDps) + return rangedDps; + + if (tank) + return tank; + + // Fallback to self if no hysteria + if (!bot->HasAura(49016)) + return bot; + + return nullptr; +} diff --git a/src/Ai/Class/Dk/Action/DKActions.h b/src/Ai/Class/Dk/Action/DKActions.h index 74e066cd514..5e8bf968fe1 100644 --- a/src/Ai/Class/Dk/Action/DKActions.h +++ b/src/Ai/Class/Dk/Action/DKActions.h @@ -340,4 +340,11 @@ class CastBloodTapAction : public CastMeleeSpellAction CastBloodTapAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "blood tap") {} }; +class CastHysteriaAction : public CastSpellAction +{ +public: + CastHysteriaAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hysteria") {} + Unit* GetTarget() override; +}; + #endif diff --git a/src/Ai/Class/Dk/DKAiObjectContext.cpp b/src/Ai/Class/Dk/DKAiObjectContext.cpp index ac80c7dc46d..9a8271aa704 100644 --- a/src/Ai/Class/Dk/DKAiObjectContext.cpp +++ b/src/Ai/Class/Dk/DKAiObjectContext.cpp @@ -100,6 +100,7 @@ class DeathKnightTriggerFactoryInternal : public NamedObjectContext creators["dd cd and no desolation"] = &DeathKnightTriggerFactoryInternal::dd_cd_and_no_desolation; creators["death and decay cooldown"] = &DeathKnightTriggerFactoryInternal::death_and_decay_cooldown; creators["army of the dead"] = &DeathKnightTriggerFactoryInternal::army_of_the_dead; + creators["hysteria no cd"] = &DeathKnightTriggerFactoryInternal::hysteria_no_cd; } private: @@ -152,6 +153,7 @@ class DeathKnightTriggerFactoryInternal : public NamedObjectContext } static Trigger* death_and_decay_cooldown(PlayerbotAI* botAI) { return new DeathAndDecayCooldownTrigger(botAI); } static Trigger* army_of_the_dead(PlayerbotAI* botAI) { return new ArmyOfTheDeadTrigger(botAI); } + static Trigger* hysteria_no_cd(PlayerbotAI* botAI) { return new HysteriaNoCooldownTrigger(botAI); } }; class DeathKnightAiObjectContextInternal : public NamedObjectContext @@ -209,7 +211,7 @@ class DeathKnightAiObjectContextInternal : public NamedObjectContext creators["vampiric blood"] = &DeathKnightAiObjectContextInternal::vampiric_blood; creators["death pact"] = &DeathKnightAiObjectContextInternal::death_pact; creators["death rune_mastery"] = &DeathKnightAiObjectContextInternal::death_rune_mastery; - // creators["hysteria"] = &DeathKnightAiObjectContextInternal::hysteria; + creators["hysteria"] = &DeathKnightAiObjectContextInternal::hysteria; creators["dancing rune weapon"] = &DeathKnightAiObjectContextInternal::dancing_rune_weapon; creators["dark command"] = &DeathKnightAiObjectContextInternal::dark_command; } @@ -265,7 +267,7 @@ class DeathKnightAiObjectContextInternal : public NamedObjectContext static Action* vampiric_blood(PlayerbotAI* botAI) { return new CastVampiricBloodAction(botAI); } static Action* death_pact(PlayerbotAI* botAI) { return new CastDeathPactAction(botAI); } static Action* death_rune_mastery(PlayerbotAI* botAI) { return new CastDeathRuneMasteryAction(botAI); } - // static Action* hysteria(PlayerbotAI* botAI) { return new CastHysteriaAction(botAI); } + static Action* hysteria(PlayerbotAI* botAI) { return new CastHysteriaAction(botAI); } static Action* dancing_rune_weapon(PlayerbotAI* botAI) { return new CastDancingRuneWeaponAction(botAI); } static Action* dark_command(PlayerbotAI* botAI) { return new CastDarkCommandAction(botAI); } static Action* mind_freeze_on_enemy_healer(PlayerbotAI* botAI) diff --git a/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp index 344b3dc0957..9ced88ac7f3 100644 --- a/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp @@ -50,7 +50,9 @@ class BloodDKStrategyActionNodeFactory : public NamedObjectFactory { NextAction("frost presence") }, - /*A*/ {}, + /*A*/ { + NextAction("blood strike") + }, /*C*/ {} ); } @@ -89,13 +91,11 @@ BloodDKStrategy::BloodDKStrategy(PlayerbotAI* botAI) : GenericDKStrategy(botAI) std::vector BloodDKStrategy::getDefaultActions() { return { - NextAction("rune strike", ACTION_DEFAULT + 0.8f), - NextAction("icy touch", ACTION_DEFAULT + 0.7f), - NextAction("heart strike", ACTION_DEFAULT + 0.6f), - NextAction("blood strike", ACTION_DEFAULT + 0.5f), - NextAction("dancing rune weapon", ACTION_DEFAULT + 0.4f), - NextAction("death coil", ACTION_DEFAULT + 0.3f), - NextAction("plague strike", ACTION_DEFAULT + 0.2f), + NextAction("rune strike", ACTION_DEFAULT + 0.6f), + NextAction("icy touch", ACTION_DEFAULT + 0.5f), + NextAction("heart strike", ACTION_DEFAULT + 0.4f), + NextAction("dancing rune weapon", ACTION_DEFAULT + 0.3f), + NextAction("death coil", ACTION_DEFAULT + 0.2f), NextAction("horn of winter", ACTION_DEFAULT + 0.1f), NextAction("melee", ACTION_DEFAULT) }; @@ -105,6 +105,14 @@ void BloodDKStrategy::InitTriggers(std::vector& triggers) { GenericDKStrategy::InitTriggers(triggers); + triggers.push_back( + new TriggerNode( + "hysteria no cd", + { + NextAction("hysteria", ACTION_NORMAL + 4) + } + ) + ); triggers.push_back( new TriggerNode( "rune strike", @@ -162,4 +170,12 @@ void BloodDKStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back( + new TriggerNode( + "high unholy rune", + { + NextAction("death strike", ACTION_HIGH + 1) + } + ) + ); } diff --git a/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp index d0b0ee2037f..48a61a5ffcf 100644 --- a/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp @@ -91,7 +91,6 @@ std::vector FrostDKStrategy::getDefaultActions() return { NextAction("obliterate", ACTION_DEFAULT + 0.7f), NextAction("frost strike", ACTION_DEFAULT + 0.4f), - NextAction("empower rune weapon", ACTION_DEFAULT + 0.3f), NextAction("horn of winter", ACTION_DEFAULT + 0.1f), NextAction("melee", ACTION_DEFAULT) }; diff --git a/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp b/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp index d358d437054..0d3a43b7953 100644 --- a/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp @@ -41,16 +41,10 @@ void GenericDKNonCombatStrategy::InitTriggers(std::vector& trigger { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 1) })); triggers.push_back( new TriggerNode("horn of winter", { NextAction("horn of winter", 21.0f) })); triggers.push_back( new TriggerNode("bone shield", { NextAction("bone shield", 21.0f) })); - triggers.push_back( - new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); - triggers.push_back( - new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); } void DKBuffDpsStrategy::InitTriggers(std::vector& triggers) diff --git a/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp index 61ff8c74897..10934161245 100644 --- a/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp @@ -165,12 +165,6 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) { MeleeCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 5) })); - triggers.push_back( - new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); - triggers.push_back( - new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); triggers.push_back( new TriggerNode("mind freeze", { NextAction("mind freeze", ACTION_HIGH + 1) })); triggers.push_back( @@ -179,7 +173,8 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "horn of winter", { NextAction("horn of winter", ACTION_NORMAL + 1) })); triggers.push_back(new TriggerNode("critical health", - { NextAction("death pact", ACTION_HIGH + 5) })); + { NextAction("raise dead", ACTION_HIGH + 6), + NextAction("death pact", ACTION_HIGH + 5) })); triggers.push_back( new TriggerNode("low health", { NextAction("icebound fortitude", ACTION_HIGH + 5), @@ -190,4 +185,11 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) NextAction("blood boil", ACTION_NORMAL + 3) })); triggers.push_back( new TriggerNode("pestilence glyph", { NextAction("pestilence", ACTION_HIGH + 9) })); + triggers.push_back( + new TriggerNode("no rune", + { + NextAction("empower rune weapon", ACTION_HIGH + 1) + } + ) + ); } diff --git a/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp b/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp index d94a94ec313..40a0ac0417d 100644 --- a/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp @@ -87,6 +87,13 @@ void UnholyDKStrategy::InitTriggers(std::vector& triggers) { GenericDKStrategy::InitTriggers(triggers); + triggers.push_back( + new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 5) })); + triggers.push_back( + new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); + triggers.push_back( + new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + triggers.push_back( new TriggerNode( "death and decay cooldown", @@ -146,13 +153,6 @@ void UnholyDKStrategy::InitTriggers(std::vector& triggers) } ) ); - triggers.push_back( - new TriggerNode("no rune", - { - NextAction("empower rune weapon", ACTION_HIGH + 1) - } - ) - ); triggers.push_back( new TriggerNode( "army of the dead", diff --git a/src/Ai/Class/Dk/Trigger/DKTriggers.h b/src/Ai/Class/Dk/Trigger/DKTriggers.h index c46fbfe37fa..98070d85f5e 100644 --- a/src/Ai/Class/Dk/Trigger/DKTriggers.h +++ b/src/Ai/Class/Dk/Trigger/DKTriggers.h @@ -198,4 +198,10 @@ class ArmyOfTheDeadTrigger : public BoostTrigger ArmyOfTheDeadTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "army of the dead") {} }; +class HysteriaNoCooldownTrigger : public SpellNoCooldownTrigger +{ +public: + HysteriaNoCooldownTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "hysteria") {} +}; + #endif From 76dd91c4fafdb6a690cf2db7da0036299c0a9d09 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 3 Apr 2026 22:25:29 +0200 Subject: [PATCH 39/87] Hand of Freedom support (#2233) ## Pull Request Description Added Hand of Freedom action for paladin. Related with: #2002 ## How to Test the Changes - invite paladin bot to party - start fight (can use dummy target) - apply some snare effect to bot or yourself (for example `.aura 1715`) - bot should use hand of freedom ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Yes, paladin bots start using Hand of Freedom - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) OpenCode, as helper to create and review code ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers obraz --- .../Value/PartyMemberSnaredTargetValue.cpp | 64 +++++++++++++++++++ .../Base/Value/PartyMemberSnaredTargetValue.h | 24 +++++++ src/Ai/Base/ValueContext.h | 3 + .../Class/Paladin/Action/PaladinActions.cpp | 34 ++++++++++ src/Ai/Class/Paladin/Action/PaladinActions.h | 13 ++++ .../Class/Paladin/PaladinAiObjectContext.cpp | 4 ++ .../Strategy/GenericPaladinStrategy.cpp | 3 + .../Class/Paladin/Trigger/PaladinTriggers.cpp | 35 ++++++++++ .../Class/Paladin/Trigger/PaladinTriggers.h | 10 +++ src/Ai/Class/Paladin/Util/PaladinHelper.h | 43 +++++++++++++ src/Bot/PlayerbotAI.cpp | 5 ++ src/Bot/PlayerbotAI.h | 1 + 12 files changed, 239 insertions(+) create mode 100644 src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp create mode 100644 src/Ai/Base/Value/PartyMemberSnaredTargetValue.h create mode 100644 src/Ai/Class/Paladin/Util/PaladinHelper.h diff --git a/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp new file mode 100644 index 00000000000..02f726b0fa1 --- /dev/null +++ b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp @@ -0,0 +1,64 @@ +/* + * 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 "PartyMemberSnaredTargetValue.h" + +#include + +#include "PlayerbotAIAware.h" +#include "Playerbots.h" + +class PartyMemberSnaredTargetPredicate : public FindPlayerPredicate, public PlayerbotAIAware +{ +public: + PartyMemberSnaredTargetPredicate(PlayerbotAI* botAI) + : PlayerbotAIAware(botAI) + { + } + + bool Check(Unit* unit) override + { + if (!unit || !unit->IsAlive() || !unit->IsInWorld() || unit == botAI->GetBot()) + return false; + + if (unit->GetMapId() != botAI->GetBot()->GetMapId()) + return false; + + if (!botAI->GetBot()->IsWithinLOSInMap(unit)) + return false; + + return botAI->IsMovementImpaired(unit); + } +}; + +Unit* PartyMemberSnaredTargetValue::Calculate() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + PartyMemberSnaredTargetPredicate predicate(botAI); + Player* bestTarget = nullptr; + float closestDistanceSq = std::numeric_limits::max(); + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member) + continue; + + if (!predicate.Check(member)) + continue; + + float const distanceSq = bot->GetExactDist2dSq(member->GetPositionX(), member->GetPositionY()); + if (distanceSq < closestDistanceSq) + { + closestDistanceSq = distanceSq; + bestTarget = member; + } + } + + return bestTarget; +} diff --git a/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h new file mode 100644 index 00000000000..d7b517e38a3 --- /dev/null +++ b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h @@ -0,0 +1,24 @@ +/* + * 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_PARTYMEMBERSNAREDTARGETVALUE_H +#define _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H + +#include "NamedObjectContext.h" +#include "PartyMemberValue.h" + +class PartyMemberSnaredTargetValue : public PartyMemberValue +{ +public: + PartyMemberSnaredTargetValue(PlayerbotAI* botAI, std::string const name = "party member snared target") + : PartyMemberValue(botAI, name) + { + } + +protected: + Unit* Calculate() override; +}; + +#endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index d1b07b88cce..14cbd185c24 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -66,6 +66,7 @@ #include "PartyMemberToDispel.h" #include "PartyMemberToHeal.h" #include "PartyMemberToResurrect.h" +#include "PartyMemberSnaredTargetValue.h" #include "PartyMemberWithoutAuraValue.h" #include "PartyMemberWithoutItemValue.h" #include "PetTargetValue.h" @@ -152,6 +153,7 @@ class ValueContext : public NamedObjectContext creators["duel target"] = &ValueContext::duel_target; creators["party member to dispel"] = &ValueContext::party_member_to_dispel; creators["party member to protect"] = &ValueContext::party_member_to_protect; + creators["party member snared target"] = &ValueContext::party_member_snared_target; creators["health"] = &ValueContext::health; creators["rage"] = &ValueContext::rage; creators["energy"] = &ValueContext::energy; @@ -450,6 +452,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); } static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); } static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); } + static UntypedValue* party_member_snared_target(PlayerbotAI* botAI) { return new PartyMemberSnaredTargetValue(botAI); } 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); } diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index 944b6e68601..38e17af1e6e 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -7,6 +7,7 @@ #include "AiFactory.h" #include "Event.h" +#include "PaladinHelper.h" #include "PlayerbotAI.h" #include "Playerbots.h" #include "SharedDefines.h" @@ -468,6 +469,39 @@ bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self ta Value* CastTurnUndeadAction::GetTargetValue() { return context->GetValue("cc target", getName()); } +Unit* CastHandOfFreedomOnPartyAction::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return CastBuffSpellAction::GetTarget(); +} + +Value* CastHandOfFreedomOnPartyAction::GetTargetValue() +{ + return context->GetValue("party member snared target"); +} + +bool CastHandOfFreedomOnPartyAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); +} + Unit* CastRighteousDefenseAction::GetTarget() { Unit* current_target = AI_VALUE(Unit*, "current target"); diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.h b/src/Ai/Class/Paladin/Action/PaladinActions.h index c58c3209d65..75b0637a4bc 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/src/Ai/Class/Paladin/Action/PaladinActions.h @@ -371,6 +371,19 @@ class CastTurnUndeadAction : public CastBuffSpellAction Value* GetTargetValue() override; }; +class CastHandOfFreedomOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport +{ +public: + CastHandOfFreedomOnPartyAction(PlayerbotAI* botAI) + : CastBuffSpellAction(botAI, "hand of freedom"), PartyMemberActionNameSupport("hand of freedom") {} + + Unit* GetTarget() override; + Value* GetTargetValue() override; + std::string const GetTargetName() override { return "party member snared target"; } + std::string const getName() override { return PartyMemberActionNameSupport::getName(); } + bool isUseful() override; +}; + PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection"); class CastDivinePleaAction : public CastBuffSpellAction diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index ed8f4931b38..3c92e57c33c 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -142,6 +142,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext creators["repentance interrupt"] = &PaladinTriggerFactoryInternal::repentance_interrupt; creators["beacon of light on main tank"] = &PaladinTriggerFactoryInternal::beacon_of_light_on_main_tank; creators["sacred shield on main tank"] = &PaladinTriggerFactoryInternal::sacred_shield_on_main_tank; + creators["hand of freedom on party"] = &PaladinTriggerFactoryInternal::hand_of_freedom_on_party; creators["blessing of kings on party"] = &PaladinTriggerFactoryInternal::blessing_of_kings_on_party; creators["blessing of wisdom on party"] = &PaladinTriggerFactoryInternal::blessing_of_wisdom_on_party; @@ -207,6 +208,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); } static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); } static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); } + static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); } static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); } static Trigger* blessing_of_wisdom_on_party(PlayerbotAI* botAI) @@ -308,6 +310,7 @@ class PaladinAiObjectContextInternal : public NamedObjectContext creators["divine illumination"] = &PaladinAiObjectContextInternal::divine_illumination; creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice; creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice; + creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party; } private: @@ -414,6 +417,7 @@ class PaladinAiObjectContextInternal : public NamedObjectContext static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); } static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); } static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); } + static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); } }; SharedNamedObjectContextList PaladinAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index 49143f9aef8..f03207216ec 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -35,6 +35,9 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "protect party member", { NextAction("blessing of protection on party", ACTION_EMERGENCY + 2) })); + triggers.push_back(new TriggerNode( + "hand of freedom on party", + { NextAction("hand of freedom on party", ACTION_HIGH + 4) })); triggers.push_back( new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); } diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index 71328c4dcf7..e3367aaef3b 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -8,6 +8,7 @@ #include "PaladinActions.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "PaladinHelper.h" bool SealTrigger::IsActive() { @@ -31,6 +32,40 @@ bool BlessingTrigger::IsActive() "blessing of kings", "blessing of sanctuary", nullptr); } +Unit* HandOfFreedomOnPartyTrigger::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return Trigger::GetTarget(); +} + +bool HandOfFreedomOnPartyTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) + return false; + + if (!botAI->CanCastSpell("hand of freedom", target)) + return false; + + return !ai::paladin::HasAnyPaladinHandFromCaster(target, bot) && botAI->IsMovementImpaired(target); +} + bool NotSensingUndeadTrigger::IsActive() { return !botAI->HasAura("sense undead", bot); diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index f33b66890ad..cc6ceddcf4c 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -242,6 +242,16 @@ class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} }; +class HandOfFreedomOnPartyTrigger : public Trigger +{ +public: + HandOfFreedomOnPartyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "hand of freedom on party", 1) {} + + Unit* GetTarget() override; + std::string const GetTargetName() override { return "party member snared target"; } + bool IsActive() override; +}; + class AvengingWrathTrigger : public BoostTrigger { public: diff --git a/src/Ai/Class/Paladin/Util/PaladinHelper.h b/src/Ai/Class/Paladin/Util/PaladinHelper.h new file mode 100644 index 00000000000..64b88b73103 --- /dev/null +++ b/src/Ai/Class/Paladin/Util/PaladinHelper.h @@ -0,0 +1,43 @@ +/* + * 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_PALADINHELPER_H +#define _PLAYERBOT_PALADINHELPER_H + +#include + +#include "Unit.h" + +class Player; + +namespace ai::paladin +{ +static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022; +static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038; +static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044; +static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940; + +inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list spellIds) +{ + if (!target || !caster) + return false; + + for (uint32 spellId : spellIds) + { + if (target->HasAura(spellId, caster->GetGUID())) + return true; + } + + return false; +} + +inline bool HasAnyPaladinHandFromCaster(Unit* target, Player* caster) +{ + return HasHandFromCaster(target, caster, + { SPELL_HAND_OF_PROTECTION, SPELL_HAND_OF_SALVATION, SPELL_HAND_OF_FREEDOM, SPELL_HAND_OF_SACRIFICE }); +} +} + +#endif diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 4a746009ac0..446ca8c40f1 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1962,6 +1962,11 @@ bool PlayerbotAI::HasAggro(Unit* unit) return false; } +bool PlayerbotAI::IsMovementImpaired(Unit* unit) +{ + return unit && (unit->HasAuraType(SPELL_AURA_MOD_ROOT) || unit->IsRooted() || unit->GetSpeedRate(MOVE_RUN) < 1.0f); +} + int32 PlayerbotAI::GetAssistTankIndex(Player* player) { Group* group = player->GetGroup(); diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 5d64bf15946..9c417561fec 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -430,6 +430,7 @@ class PlayerbotAI : public PlayerbotAIBase static bool IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); static bool IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); bool HasAggro(Unit* unit); + bool IsMovementImpaired(Unit* unit); static int32 GetAssistTankIndex(Player* player); int32 GetGroupSlotIndex(Player* player); int32 GetRangedIndex(Player* player); From 15bf0ab427a91b35e31ffd5551bef8d90c0d6e1a Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 3 Apr 2026 15:25:40 -0500 Subject: [PATCH 40/87] Fix Shaman Weapon Enchants & Cure Toxins/Cleanse Spirit (#2234) ## Pull Request Description 1. I've been having persistent issues with Enhancement Shamans sometimes applying Rockbiter to both weapons instead of MH Windfury and OH Flametongue. Rockbiter is the alternative for Flametongue and, through Flametongue, the alternative for Windfury. But there seemed to be no obvious reason why a Shaman that had all three abilities would ever use Rockbiter, which costs more mana than Windfury and Flametongue. Claude's take on it is that there is instability from ItemForSpellValue related to its poor way of distinguishing handedness, in addition to it having a 1-second cache, which can cause in some scenarios stale caches for the action running on each hand back-to-back. I still can't say I fully understand why the issue exists, but the most straightforward fix that should prevent this from happening is to just have separate mainhand and offhand actions for each enchant. So that's what this PR does. The relevant ActionNodes are now: - The MH-specific chain for Enhancement is WF -> FT -> RB. In practice, Enhancement should never apply RB because all Shamans under level 10 (when FT is learned) are considered Elemental. The FT -> RB node is just for Elemental. - The MH-specific Resto chain (not that Resto can dual-wield) is EL -> FT -> RB. Againt, FT -> RB is just for Elemental. - OH for Enhancement is only FT. You cannot be Enhancement before level 10, nor can Enhancement dual-wield before level 40, so no alternative is needed. 3. I commented out Frostbrand Weapon actions/triggers because the ability is not included in any strategy. I didn't delete the code because in the future somebody might want to implement it as I understand it can be useful for Enhancement Shamans in PvP. 4. Shamans are coded to use "cure poison" and "cure disease", which do not exist in WotLK, having been combined into Cure Toxins. Wishmaster has PR #1844 that has been open on this for a long time, but I decided to correct the abilities here anyway as he was going for a more limited approach, and I decided to rename all the actions and redo the structure to rely on ActionNode alternatives, which is pretty much the exact framework that should be used for this type of situation w/r/t bots. Now, Shamans prefer Cleanse Spirit (Resto talent, which costs the same as Cure Toxins and also dispels curses), with an alternative of Cure Toxins (for poisons and disease only). I tested this and it seems to work well. 5. I deleted empty ActionNodes. 6. I did some cleanup of formatting and such, but this is not intended to be a comprehensive refactor. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. I've followed the general intended structure of class strategies with triggers and actions. The same triggers exist, just different actions are called based on the trigger that fires, so I don't think there should be any impact on performance. ## How to Test the Changes The best way to get a grasp on if things work is probably to just do group play for a while with Shamans and make sure they apply the right enchants and properly cast Cleanse Spirit and Cure Toxins. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Shamans previously did not cure poisons or disease at all, and now they do with the default "cure" strategy applied. - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [x] Yes (**explain below**) One might say having separate actions per hand for enchants is somewhat more complex, but ultimately I think it is less confusing to keep those paths separate. ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) I had Claude try to diagnose the weapon enchant issue. It proposed and provided the separate MH/OH WF/FT actions. The other things were easy enough for me to do. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Base/Actions/GenericSpellActions.cpp | 33 +- src/Ai/Base/Actions/GenericSpellActions.h | 14 + src/Ai/Class/Shaman/Action/ShamanActions.cpp | 25 +- src/Ai/Class/Shaman/Action/ShamanActions.h | 352 ++++++++++-------- src/Ai/Class/Shaman/ShamanAiObjectContext.cpp | 194 +++++----- .../Strategy/ElementalShamanStrategy.cpp | 33 +- .../Strategy/EnhancementShamanStrategy.cpp | 17 - .../Shaman/Strategy/GenericShamanStrategy.cpp | 61 +-- .../Shaman/Strategy/RestoShamanStrategy.cpp | 97 +---- .../Strategy/ShamanNonCombatStrategy.cpp | 110 +++--- .../Shaman/Strategy/TotemsShamanStrategy.cpp | 19 +- .../Class/Shaman/Trigger/ShamanTriggers.cpp | 6 +- src/Ai/Class/Shaman/Trigger/ShamanTriggers.h | 137 +++---- 13 files changed, 519 insertions(+), 579 deletions(-) diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 82bdb74d2a2..41911f62d8e 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -151,7 +151,9 @@ bool CastMeleeSpellAction::isUseful() return CastSpellAction::isUseful(); } -CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) +CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction( + PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : + CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) { range = ATTACK_DISTANCE; } @@ -203,6 +205,35 @@ bool CastEnchantItemAction::isPossible() return spellId && AI_VALUE2(Item*, "item for spell", spellId); } +CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell) + : CastEnchantItemAction(botAI, spell) {} + +bool CastEnchantItemMainHandAction::isPossible() +{ + if (!CastEnchantItemAction::isPossible()) + return false; + + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + return item && !item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT) && + item->GetTemplate()->Class == ITEM_CLASS_WEAPON; +} + +CastEnchantItemOffHandAction::CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell) + : CastEnchantItemAction(botAI, spell) {} + +bool CastEnchantItemOffHandAction::isPossible() +{ + if (!CastEnchantItemAction::isPossible()) + return false; + + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!item || item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + return false; + + uint32 invType = item->GetTemplate()->InventoryType; + return invType == INVTYPE_WEAPON || invType == INVTYPE_WEAPONOFFHAND; +} + CastHealingSpellAction::CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount, HealingManaEfficiency manaEfficiency, bool isOwner) : CastAuraSpellAction(botAI, spell, isOwner), estAmount(estAmount), manaEfficiency(manaEfficiency) diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index b15c4894a3b..dc07857138d 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -130,6 +130,20 @@ class CastEnchantItemAction : public CastSpellAction std::string const GetTargetName() override { return "self target"; } }; +class CastEnchantItemMainHandAction : public CastEnchantItemAction +{ +public: + CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell); + bool isPossible() override; +}; + +class CastEnchantItemOffHandAction : public CastEnchantItemAction +{ +public: + CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell); + bool isPossible() override; +}; + class CastHealingSpellAction : public CastAuraSpellAction { public: diff --git a/src/Ai/Class/Shaman/Action/ShamanActions.cpp b/src/Ai/Class/Shaman/Action/ShamanActions.cpp index 3bb0ae86f4d..47634c4e070 100644 --- a/src/Ai/Class/Shaman/Action/ShamanActions.cpp +++ b/src/Ai/Class/Shaman/Action/ShamanActions.cpp @@ -57,7 +57,8 @@ bool CastLavaBurstAction::isUseful() if (!target) return false; - static const uint32 FLAME_SHOCK_SPELL_IDS[] = {8050, 8052, 8053, 10447, 10448, 29228, 25457, 49232, 49233}; + static const uint32 FLAME_SHOCK_SPELL_IDS[] = + {8050, 8052, 8053, 10447, 10448, 29228, 25457, 49232, 49233}; ObjectGuid botGuid = bot->GetGUID(); for (uint32 spellId : FLAME_SHOCK_SPELL_IDS) @@ -65,6 +66,7 @@ bool CastLavaBurstAction::isUseful() if (target->HasAura(spellId, botGuid)) return true; } + return false; } @@ -77,15 +79,13 @@ bool CastSpiritWalkAction::Execute(Event /*event*/) for (Unit* unit : bot->m_Controlled) { - if (unit->GetEntry() == SPIRIT_WOLF) + if (unit->GetEntry() == SPIRIT_WOLF && unit->HasSpell(SPIRIT_WALK_SPELL)) { - if (unit->HasSpell(SPIRIT_WALK_SPELL)) - { - unit->CastSpell(unit, SPIRIT_WALK_SPELL, false); - return true; - } + unit->CastSpell(unit, SPIRIT_WALK_SPELL, false); + return true; } } + return false; } @@ -105,16 +105,13 @@ bool SetTotemAction::Execute(Event /*event*/) } if (!totemSpell) - { return false; - } - if (const ActionButton* button = bot->GetActionButton(actionButtonId)) + if (const ActionButton* button = bot->GetActionButton(actionButtonId); + button && button->GetType() == ACTION_BUTTON_SPELL && + button->GetAction() == totemSpell) { - if (button->GetType() == ACTION_BUTTON_SPELL && button->GetAction() == totemSpell) - { - return false; - } + return false; } bot->addActionButton(actionButtonId, totemSpell, ACTION_BUTTON_SPELL); diff --git a/src/Ai/Class/Shaman/Action/ShamanActions.h b/src/Ai/Class/Shaman/Action/ShamanActions.h index 69f29f0496e..cdd661561ec 100644 --- a/src/Ai/Class/Shaman/Action/ShamanActions.h +++ b/src/Ai/Class/Shaman/Action/ShamanActions.h @@ -18,73 +18,92 @@ class PlayerbotAI; class CastWaterShieldAction : public CastBuffSpellAction { public: - CastWaterShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water shield") {} + CastWaterShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water shield") {} }; class CastLightningShieldAction : public CastBuffSpellAction { public: - CastLightningShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "lightning shield") {} + CastLightningShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "lightning shield") {} }; -class CastEarthlivingWeaponAction : public CastEnchantItemAction +class CastRockbiterWeaponMainHandAction : public CastEnchantItemMainHandAction { public: - CastEarthlivingWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "earthliving weapon") {} + CastRockbiterWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "rockbiter weapon") {} }; -class CastRockbiterWeaponAction : public CastEnchantItemAction +class CastFlametongueWeaponMainHandAction : public CastEnchantItemMainHandAction { public: - CastRockbiterWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "rockbiter weapon") {} + CastFlametongueWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "flametongue weapon") {} }; -class CastFlametongueWeaponAction : public CastEnchantItemAction +class CastFlametongueWeaponOffHandAction : public CastEnchantItemOffHandAction { public: - CastFlametongueWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "flametongue weapon") {} + CastFlametongueWeaponOffHandAction(PlayerbotAI* botAI) : + CastEnchantItemOffHandAction(botAI, "flametongue weapon") {} }; -class CastFrostbrandWeaponAction : public CastEnchantItemAction +/* class CastFrostbrandWeaponOffHandAction : public CastEnchantItemOffHandAction { public: - CastFrostbrandWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "frostbrand weapon") {} + CastFrostbrandWeaponOffHandAction(PlayerbotAI* botAI) : + CastEnchantItemOffHandAction(botAI, "frostbrand weapon") {} +}; */ + +class CastEarthlivingWeaponMainHandAction : public CastEnchantItemMainHandAction +{ +public: + CastEarthlivingWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "earthliving weapon") {} }; -class CastWindfuryWeaponAction : public CastEnchantItemAction +class CastWindfuryWeaponMainHandAction : public CastEnchantItemMainHandAction { public: - CastWindfuryWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "windfury weapon") {} + CastWindfuryWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "windfury weapon") {} }; class CastAncestralSpiritAction : public ResurrectPartyMemberAction { public: - CastAncestralSpiritAction(PlayerbotAI* botAI) : ResurrectPartyMemberAction(botAI, "ancestral spirit") {} + CastAncestralSpiritAction(PlayerbotAI* botAI) : + ResurrectPartyMemberAction(botAI, "ancestral spirit") {} }; class CastWaterBreathingAction : public CastBuffSpellAction { public: - CastWaterBreathingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water breathing") {} + CastWaterBreathingAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water breathing") {} }; class CastWaterWalkingAction : public CastBuffSpellAction { public: - CastWaterWalkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water walking") {} + CastWaterWalkingAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water walking") {} }; class CastWaterBreathingOnPartyAction : public BuffOnPartyAction { public: - CastWaterBreathingOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "water breathing") {} + CastWaterBreathingOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "water breathing") {} }; class CastWaterWalkingOnPartyAction : public BuffOnPartyAction { public: - CastWaterWalkingOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "water walking") {} + CastWaterWalkingOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "water walking") {} }; // Boost Actions @@ -92,31 +111,36 @@ class CastWaterWalkingOnPartyAction : public BuffOnPartyAction class CastHeroismAction : public CastBuffSpellAction { public: - CastHeroismAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "heroism") {} + CastHeroismAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "heroism") {} }; class CastBloodlustAction : public CastBuffSpellAction { public: - CastBloodlustAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "bloodlust") {} + CastBloodlustAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "bloodlust") {} }; class CastElementalMasteryAction : public CastBuffSpellAction { public: - CastElementalMasteryAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "elemental mastery") {} + CastElementalMasteryAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "elemental mastery") {} }; class CastShamanisticRageAction : public CastBuffSpellAction { public: - CastShamanisticRageAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "shamanistic rage") {} + CastShamanisticRageAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "shamanistic rage") {} }; class CastFeralSpiritAction : public CastSpellAction { public: - CastFeralSpiritAction(PlayerbotAI* ai) : CastSpellAction(ai, "feral spirit") {} + CastFeralSpiritAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "feral spirit") {} }; class CastSpiritWalkAction : public Action @@ -138,7 +162,8 @@ class CastWindShearAction : public CastSpellAction class CastWindShearOnEnemyHealerAction : public CastSpellOnEnemyHealerAction { public: - CastWindShearOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "wind shear") {} + CastWindShearOnEnemyHealerAction(PlayerbotAI* botAI) : + CastSpellOnEnemyHealerAction(botAI, "wind shear") {} }; class CastPurgeAction : public CastSpellAction @@ -150,16 +175,15 @@ class CastPurgeAction : public CastSpellAction class CastCleanseSpiritAction : public CastCureSpellAction { public: - CastCleanseSpiritAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cleanse spirit") {} + CastCleanseSpiritAction(PlayerbotAI* botAI) : + CastCureSpellAction(botAI, "cleanse spirit") {} }; class CastCleanseSpiritPoisonOnPartyAction : public CurePartyMemberAction { public: - CastCleanseSpiritPoisonOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_POISON) - { - } + CastCleanseSpiritPoisonOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_POISON) {} std::string const getName() override { return "cleanse spirit poison on party"; } }; @@ -167,10 +191,8 @@ class CastCleanseSpiritPoisonOnPartyAction : public CurePartyMemberAction class CastCleanseSpiritCurseOnPartyAction : public CurePartyMemberAction { public: - CastCleanseSpiritCurseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_CURSE) - { - } + CastCleanseSpiritCurseOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_CURSE) {} std::string const getName() override { return "cleanse spirit curse on party"; } }; @@ -178,42 +200,35 @@ class CastCleanseSpiritCurseOnPartyAction : public CurePartyMemberAction class CastCleanseSpiritDiseaseOnPartyAction : public CurePartyMemberAction { public: - CastCleanseSpiritDiseaseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_DISEASE) - { - } + CastCleanseSpiritDiseaseOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_DISEASE) {} std::string const getName() override { return "cleanse spirit disease on party"; } }; -class CastCurePoisonActionSham : public CastCureSpellAction +class CastCureToxinsActionSham : public CastCureSpellAction { public: - CastCurePoisonActionSham(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cure poison") {} + CastCureToxinsActionSham(PlayerbotAI* botAI) : + CastCureSpellAction(botAI, "cure toxins") {} }; -class CastCurePoisonOnPartyActionSham : public CurePartyMemberAction +class CastCureToxinsPoisonOnPartyActionSham : public CurePartyMemberAction { public: - CastCurePoisonOnPartyActionSham(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "cure poison", DISPEL_POISON) {} - - std::string const getName() override { return "cure poison on party"; } -}; + CastCureToxinsPoisonOnPartyActionSham(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cure toxins", DISPEL_POISON) {} -class CastCureDiseaseActionSham : public CastCureSpellAction -{ -public: - CastCureDiseaseActionSham(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cure disease") {} + std::string const getName() override { return "cure toxins poison on party"; } }; -class CastCureDiseaseOnPartyActionSham : public CurePartyMemberAction +class CastCureToxinsDiseaseOnPartyActionSham : public CurePartyMemberAction { public: - CastCureDiseaseOnPartyActionSham(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "cure disease", DISPEL_DISEASE) - { - } + CastCureToxinsDiseaseOnPartyActionSham(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cure toxins", DISPEL_DISEASE) {} - std::string const getName() override { return "cure disease on party"; } + std::string const getName() override { return "cure toxins disease on party"; } }; // Damage and Debuff Actions @@ -221,68 +236,77 @@ class CastCureDiseaseOnPartyActionSham : public CurePartyMemberAction class CastFireNovaAction : public CastMeleeSpellAction { public: - CastFireNovaAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "fire nova") {} + CastFireNovaAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "fire nova") {} + bool isUseful() override; }; class CastStormstrikeAction : public CastMeleeSpellAction { public: - CastStormstrikeAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "stormstrike") {} + CastStormstrikeAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "stormstrike") {} }; class CastLavaLashAction : public CastMeleeSpellAction { public: - CastLavaLashAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "lava lash") {} + CastLavaLashAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "lava lash") {} }; class CastFlameShockAction : public CastDebuffSpellAction { public: - CastFlameShockAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} - bool isUseful() override - { - // Bypass TTL check - return CastAuraSpellAction::isUseful(); - } + CastFlameShockAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} + + bool isUseful() override { return CastAuraSpellAction::isUseful(); } }; class CastEarthShockAction : public CastSpellAction { public: - CastEarthShockAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "earth shock") {} + CastEarthShockAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "earth shock") {} }; class CastFrostShockAction : public CastSnareSpellAction { public: - CastFrostShockAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "frost shock") {} + CastFrostShockAction(PlayerbotAI* botAI) : + CastSnareSpellAction(botAI, "frost shock") {} }; class CastChainLightningAction : public CastSpellAction { public: - CastChainLightningAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "chain lightning") {} + CastChainLightningAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "chain lightning") {} + ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } }; class CastLightningBoltAction : public CastSpellAction { public: - CastLightningBoltAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "lightning bolt") {} + CastLightningBoltAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "lightning bolt") {} }; class CastThunderstormAction : public CastSpellAction { public: - CastThunderstormAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "thunderstorm") {} + CastThunderstormAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "thunderstorm") {} }; class CastLavaBurstAction : public CastSpellAction { public: - CastLavaBurstAction(PlayerbotAI* ai) : CastSpellAction(ai, "lava burst") {} + CastLavaBurstAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "lava burst") {} bool isUseful() override; }; @@ -291,73 +315,71 @@ class CastLavaBurstAction : public CastSpellAction class CastLesserHealingWaveAction : public CastHealingSpellAction { public: - CastLesserHealingWaveAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "lesser healing wave") {} + CastLesserHealingWaveAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "lesser healing wave") {} }; class CastLesserHealingWaveOnPartyAction : public HealPartyMemberAction { public: - CastLesserHealingWaveOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "lesser healing wave", 25.0f, HealingManaEfficiency::LOW) - { - } + CastLesserHealingWaveOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "lesser healing wave", 25.0f, HealingManaEfficiency::LOW) {} }; class CastHealingWaveAction : public CastHealingSpellAction { public: - CastHealingWaveAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "healing wave") {} + CastHealingWaveAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "healing wave") {} }; class CastHealingWaveOnPartyAction : public HealPartyMemberAction { public: - CastHealingWaveOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "healing wave", 50.0f, HealingManaEfficiency::MEDIUM) - { - } + CastHealingWaveOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "healing wave", 50.0f, HealingManaEfficiency::MEDIUM) {} }; class CastChainHealAction : public HealPartyMemberAction { public: - CastChainHealAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) - { - } + CastChainHealAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) {} }; class CastRiptideAction : public CastHealingSpellAction { public: - CastRiptideAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "riptide") {} + CastRiptideAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "riptide") {} }; class CastRiptideOnPartyAction : public HealPartyMemberAction { public: - CastRiptideOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "riptide", 15.0f, HealingManaEfficiency::VERY_HIGH) - { - } + CastRiptideOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "riptide", 15.0f, HealingManaEfficiency::VERY_HIGH) {} }; class CastEarthShieldAction : public CastBuffSpellAction { public: - CastEarthShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "earth shield") {} + CastEarthShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "earth shield") {} }; class CastEarthShieldOnPartyAction : public BuffOnPartyAction { public: - CastEarthShieldOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "earth shield") {} + CastEarthShieldOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "earth shield") {} }; class CastEarthShieldOnMainTankAction : public BuffOnMainTankAction { public: - CastEarthShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "earth shield", false) {} + CastEarthShieldOnMainTankAction(PlayerbotAI* botAI) : + BuffOnMainTankAction(botAI, "earth shield", false) {} }; // Totem Spells @@ -365,8 +387,9 @@ class CastEarthShieldOnMainTankAction : public BuffOnMainTankAction class CastTotemAction : public CastBuffSpellAction { public: - CastTotemAction(PlayerbotAI* botAI, std::string const spell, std::string const buffName = "") - : CastBuffSpellAction(botAI, spell) + CastTotemAction( + PlayerbotAI* botAI, std::string const spell, + std::string const buffName = "") : CastBuffSpellAction(botAI, spell) { buff = (buffName == "") ? spell : buffName; } @@ -380,56 +403,66 @@ class CastTotemAction : public CastBuffSpellAction class CastCallOfTheElementsAction : public CastSpellAction { public: - CastCallOfTheElementsAction(PlayerbotAI* ai) : CastSpellAction(ai, "call of the elements") {} + CastCallOfTheElementsAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "call of the elements") {} }; class CastTotemicRecallAction : public CastSpellAction { public: - CastTotemicRecallAction(PlayerbotAI* ai) : CastSpellAction(ai, "totemic recall") {} + CastTotemicRecallAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "totemic recall") {} }; class CastStrengthOfEarthTotemAction : public CastTotemAction { public: - CastStrengthOfEarthTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "strength of earth totem", "strength of earth") {} + CastStrengthOfEarthTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "strength of earth totem", "strength of earth") {} }; class CastStoneskinTotemAction : public CastTotemAction { public: - CastStoneskinTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "stoneskin totem", "stoneskin") {} + CastStoneskinTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "stoneskin totem", "stoneskin") {} }; class CastTremorTotemAction : public CastTotemAction { public: - CastTremorTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "tremor totem", "") {} + CastTremorTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "tremor totem", "") {} }; class CastEarthbindTotemAction : public CastTotemAction { public: - CastEarthbindTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "earthbind totem", "") {} + CastEarthbindTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "earthbind totem", "") {} }; class CastStoneclawTotemAction : public CastTotemAction { public: - CastStoneclawTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "stoneclaw totem", "") {} + CastStoneclawTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "stoneclaw totem", "") {} bool isUseful() override; }; class CastSearingTotemAction : public CastTotemAction { public: - CastSearingTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "searing totem", "") {} + CastSearingTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "searing totem", "") {} }; class CastMagmaTotemAction : public CastTotemAction { public: - CastMagmaTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "magma totem", "") {} + CastMagmaTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "magma totem", "") {} + std::string const GetTargetName() override { return "self target"; } bool isUseful() override; }; @@ -437,26 +470,30 @@ class CastMagmaTotemAction : public CastTotemAction class CastFlametongueTotemAction : public CastTotemAction { public: - CastFlametongueTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "flametongue totem", "flametongue totem") {} + CastFlametongueTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "flametongue totem", "flametongue totem") {} }; class CastTotemOfWrathAction : public CastTotemAction { public: - CastTotemOfWrathAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "totem of wrath", "totem of wrath") {} + CastTotemOfWrathAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "totem of wrath", "totem of wrath") {} }; class CastFrostResistanceTotemAction : public CastTotemAction { public: - CastFrostResistanceTotemAction(PlayerbotAI* botAI) - : CastTotemAction(botAI, "frost resistance totem", "frost resistance") {} + CastFrostResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "frost resistance totem", "frost resistance") {} }; class CastFireElementalTotemAction : public CastTotemAction { public: - CastFireElementalTotemAction(PlayerbotAI* ai) : CastTotemAction(ai, "fire elemental totem", "") {} + CastFireElementalTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire elemental totem", "") {} + virtual std::string const GetTargetName() override { return "self target"; } virtual bool isUseful() override { return CastTotemAction::isUseful(); } }; @@ -464,7 +501,9 @@ class CastFireElementalTotemAction : public CastTotemAction class CastFireElementalTotemMeleeAction : public CastTotemAction { public: - CastFireElementalTotemMeleeAction(PlayerbotAI* ai) : CastTotemAction(ai, "fire elemental totem", "") {} + CastFireElementalTotemMeleeAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire elemental totem", "") {} + virtual std::string const GetTargetName() override { return "self target"; } virtual bool isUseful() override { @@ -478,51 +517,60 @@ class CastFireElementalTotemMeleeAction : public CastTotemAction class CastHealingStreamTotemAction : public CastTotemAction { public: - CastHealingStreamTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "healing stream totem", "") {} + CastHealingStreamTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "healing stream totem", "") {} }; class CastManaSpringTotemAction : public CastTotemAction { public: - CastManaSpringTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "mana spring totem", "mana spring") {} + CastManaSpringTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "mana spring totem", "mana spring") {} }; class CastCleansingTotemAction : public CastTotemAction { public: - CastCleansingTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "cleansing totem", "") {} + CastCleansingTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "cleansing totem", "") {} virtual bool isUseful(); }; class CastManaTideTotemAction : public CastTotemAction { public: - CastManaTideTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "mana tide totem", "") {} + CastManaTideTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "mana tide totem", "") {} + std::string const GetTargetName() override { return "self target"; } }; class CastFireResistanceTotemAction : public CastTotemAction { public: - CastFireResistanceTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "fire resistance totem", "fire resistance") {} + CastFireResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire resistance totem", "fire resistance") {} }; class CastWrathOfAirTotemAction : public CastTotemAction { public: - CastWrathOfAirTotemAction(PlayerbotAI* ai) : CastTotemAction(ai, "wrath of air totem", "wrath of air totem") {} + CastWrathOfAirTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "wrath of air totem", "wrath of air totem") {} }; class CastWindfuryTotemAction : public CastTotemAction { public: - CastWindfuryTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "windfury totem", "windfury totem") {} + CastWindfuryTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "windfury totem", "windfury totem") {} }; class CastNatureResistanceTotemAction : public CastTotemAction { public: - CastNatureResistanceTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "nature resistance totem", "nature resistance") {} + CastNatureResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "nature resistance totem", "nature resistance") {} }; // Set Strategy Assigned Totems @@ -532,12 +580,8 @@ class SetTotemAction : public Action public: // Template constructor: infers N (size of the id array) at compile time template - SetTotemAction(PlayerbotAI* botAI, std::string const& totemName, const uint32 (&ids)[N], int actionButtonId) - : Action(botAI, "set " + totemName) - , totemSpellIds(ids) - , totemSpellIdsCount(N) - , actionButtonId(actionButtonId) - {} + SetTotemAction(PlayerbotAI* botAI, std::string const& totemName, const uint32 (&ids)[N], int actionButtonId) : + Action(botAI, "set " + totemName), totemSpellIds(ids), totemSpellIdsCount(N), actionButtonId(actionButtonId) {} bool Execute(Event event) override; uint32 const* totemSpellIds; @@ -548,120 +592,120 @@ class SetTotemAction : public Action class SetStrengthOfEarthTotemAction : public SetTotemAction { public: - SetStrengthOfEarthTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStrengthOfEarthTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetStoneskinTotemAction : public SetTotemAction { public: - SetStoneskinTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStoneskinTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetTremorTotemAction : public SetTotemAction { public: - SetTremorTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetTremorTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetEarthbindTotemAction : public SetTotemAction { public: - SetEarthbindTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetEarthbindTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetSearingTotemAction : public SetTotemAction { public: - SetSearingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetSearingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetMagmaTotemAction : public SetTotemAction { public: - SetMagmaTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetMagmaTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetFlametongueTotemAction : public SetTotemAction { public: - SetFlametongueTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFlametongueTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetTotemOfWrathAction : public SetTotemAction { public: - SetTotemOfWrathAction(PlayerbotAI* ai) - : SetTotemAction(ai, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} + SetTotemOfWrathAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} }; class SetFrostResistanceTotemAction : public SetTotemAction { public: - SetFrostResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFrostResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetHealingStreamTotemAction : public SetTotemAction { public: - SetHealingStreamTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetHealingStreamTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetManaSpringTotemAction : public SetTotemAction { public: - SetManaSpringTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetManaSpringTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetCleansingTotemAction : public SetTotemAction { public: - SetCleansingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetCleansingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetFireResistanceTotemAction : public SetTotemAction { public: - SetFireResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetFireResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetWrathOfAirTotemAction : public SetTotemAction { public: - SetWrathOfAirTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWrathOfAirTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetWindfuryTotemAction : public SetTotemAction { public: - SetWindfuryTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWindfuryTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetNatureResistanceTotemAction : public SetTotemAction { public: - SetNatureResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetNatureResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetGroundingTotemAction : public SetTotemAction { public: - SetGroundingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetGroundingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; #endif diff --git a/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp b/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp index 08ee3638d97..73c41c4b6c7 100644 --- a/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp +++ b/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp @@ -158,10 +158,6 @@ class ShamanATriggerFactoryInternal : public NamedObjectContext creators["bloodlust"] = &ShamanATriggerFactoryInternal::bloodlust; creators["elemental mastery"] = &ShamanATriggerFactoryInternal::elemental_mastery; creators["wind shear on enemy healer"] = &ShamanATriggerFactoryInternal::wind_shear_on_enemy_healer; - creators["cure poison"] = &ShamanATriggerFactoryInternal::cure_poison; - creators["party member cure poison"] = &ShamanATriggerFactoryInternal::party_member_cure_poison; - creators["cure disease"] = &ShamanATriggerFactoryInternal::cure_disease; - creators["party member cure disease"] = &ShamanATriggerFactoryInternal::party_member_cure_disease; creators["earth shield on main tank"] = &ShamanATriggerFactoryInternal::earth_shield_on_main_tank; creators["maelstrom weapon 3"] = &ShamanATriggerFactoryInternal::maelstrom_weapon_3; creators["maelstrom weapon 4"] = &ShamanATriggerFactoryInternal::maelstrom_weapon_4; @@ -225,42 +221,38 @@ class ShamanATriggerFactoryInternal : public NamedObjectContext static Trigger* shock(PlayerbotAI* botAI) { return new ShockTrigger(botAI); } static Trigger* frost_shock_snare(PlayerbotAI* botAI) { return new FrostShockSnareTrigger(botAI); } static Trigger* wind_shear_on_enemy_healer(PlayerbotAI* botAI) { return new WindShearInterruptEnemyHealerSpellTrigger(botAI); } - static Trigger* cure_poison(PlayerbotAI* botAI) { return new CurePoisonTrigger(botAI); } - static Trigger* party_member_cure_poison(PlayerbotAI* botAI) { return new PartyMemberCurePoisonTrigger(botAI); } - static Trigger* cure_disease(PlayerbotAI* botAI) { return new CureDiseaseTrigger(botAI); } - static Trigger* party_member_cure_disease(PlayerbotAI* botAI) { return new PartyMemberCureDiseaseTrigger(botAI); } - static Trigger* earth_shield_on_main_tank(PlayerbotAI* ai) { return new EarthShieldOnMainTankTrigger(ai); } - static Trigger* flame_shock(PlayerbotAI* ai) { return new FlameShockTrigger(ai); } + static Trigger* earth_shield_on_main_tank(PlayerbotAI* botAI) { return new EarthShieldOnMainTankTrigger(botAI); } + static Trigger* flame_shock(PlayerbotAI* botAI) { return new FlameShockTrigger(botAI); } static Trigger* fire_elemental_totem(PlayerbotAI* botAI) { return new FireElementalTotemTrigger(botAI); } static Trigger* earth_shock_execute(PlayerbotAI* botAI) { return new EarthShockExecuteTrigger(botAI); } - static Trigger* spirit_walk_ready(PlayerbotAI* ai) { return new SpiritWalkTrigger(ai); } - static Trigger* chain_lightning_no_cd(PlayerbotAI* ai) { return new ChainLightningNoCdTrigger(ai); } - static Trigger* call_of_the_elements_and_enemy_within_melee(PlayerbotAI* ai) { return new CallOfTheElementsAndEnemyWithinMeleeTrigger(ai); } - static Trigger* maelstrom_weapon_5_and_medium_aoe(PlayerbotAI* ai) { return new MaelstromWeapon5AndMediumAoeTrigger(ai); } - static Trigger* maelstrom_weapon_4_and_medium_aoe(PlayerbotAI* ai) { return new MaelstromWeapon4AndMediumAoeTrigger(ai); } - static Trigger* call_of_the_elements(PlayerbotAI* ai) { return new CallOfTheElementsTrigger(ai); } - static Trigger* totemic_recall(PlayerbotAI* ai) { return new TotemicRecallTrigger(ai); } - static Trigger* no_earth_totem(PlayerbotAI* ai) { return new NoEarthTotemTrigger(ai); } - static Trigger* no_fire_totem(PlayerbotAI* ai) { return new NoFireTotemTrigger(ai); } - static Trigger* no_water_totem(PlayerbotAI* ai) { return new NoWaterTotemTrigger(ai); } - static Trigger* no_air_totem(PlayerbotAI* ai) { return new NoAirTotemTrigger(ai); } - static Trigger* set_strength_of_earth_totem(PlayerbotAI* ai) { return new SetStrengthOfEarthTotemTrigger(ai); } - static Trigger* set_stoneskin_totem(PlayerbotAI* ai) { return new SetStoneskinTotemTrigger(ai); } - static Trigger* set_tremor_totem(PlayerbotAI* ai) { return new SetTremorTotemTrigger(ai); } - static Trigger* set_earthbind_totem(PlayerbotAI* ai) { return new SetEarthbindTotemTrigger(ai); } - static Trigger* set_searing_totem(PlayerbotAI* ai) { return new SetSearingTotemTrigger(ai); } - static Trigger* set_magma_totem(PlayerbotAI* ai) { return new SetMagmaTotemTrigger(ai); } - static Trigger* set_flametongue_totem(PlayerbotAI* ai) { return new SetFlametongueTotemTrigger(ai); } - static Trigger* set_totem_of_wrath(PlayerbotAI* ai) { return new SetTotemOfWrathTrigger(ai); } - static Trigger* set_frost_resistance_totem(PlayerbotAI* ai) { return new SetFrostResistanceTotemTrigger(ai); } - static Trigger* set_healing_stream_totem(PlayerbotAI* ai) { return new SetHealingStreamTotemTrigger(ai); } - static Trigger* set_mana_spring_totem(PlayerbotAI* ai) { return new SetManaSpringTotemTrigger(ai); } - static Trigger* set_cleansing_totem(PlayerbotAI* ai) { return new SetCleansingTotemTrigger(ai); } - static Trigger* set_fire_resistance_totem(PlayerbotAI* ai) { return new SetFireResistanceTotemTrigger(ai); } - static Trigger* set_wrath_of_air_totem(PlayerbotAI* ai) { return new SetWrathOfAirTotemTrigger(ai); } - static Trigger* set_windfury_totem(PlayerbotAI* ai) { return new SetWindfuryTotemTrigger(ai); } - static Trigger* set_nature_resistance_totem(PlayerbotAI* ai) { return new SetNatureResistanceTotemTrigger(ai); } - static Trigger* set_grounding_totem(PlayerbotAI* ai) { return new SetGroundingTotemTrigger(ai); } + static Trigger* spirit_walk_ready(PlayerbotAI* botAI) { return new SpiritWalkTrigger(botAI); } + static Trigger* chain_lightning_no_cd(PlayerbotAI* botAI) { return new ChainLightningNoCdTrigger(botAI); } + static Trigger* call_of_the_elements_and_enemy_within_melee(PlayerbotAI* botAI) { return new CallOfTheElementsAndEnemyWithinMeleeTrigger(botAI); } + static Trigger* maelstrom_weapon_5_and_medium_aoe(PlayerbotAI* botAI) { return new MaelstromWeapon5AndMediumAoeTrigger(botAI); } + static Trigger* maelstrom_weapon_4_and_medium_aoe(PlayerbotAI* botAI) { return new MaelstromWeapon4AndMediumAoeTrigger(botAI); } + static Trigger* call_of_the_elements(PlayerbotAI* botAI) { return new CallOfTheElementsTrigger(botAI); } + static Trigger* totemic_recall(PlayerbotAI* botAI) { return new TotemicRecallTrigger(botAI); } + static Trigger* no_earth_totem(PlayerbotAI* botAI) { return new NoEarthTotemTrigger(botAI); } + static Trigger* no_fire_totem(PlayerbotAI* botAI) { return new NoFireTotemTrigger(botAI); } + static Trigger* no_water_totem(PlayerbotAI* botAI) { return new NoWaterTotemTrigger(botAI); } + static Trigger* no_air_totem(PlayerbotAI* botAI) { return new NoAirTotemTrigger(botAI); } + static Trigger* set_strength_of_earth_totem(PlayerbotAI* botAI) { return new SetStrengthOfEarthTotemTrigger(botAI); } + static Trigger* set_stoneskin_totem(PlayerbotAI* botAI) { return new SetStoneskinTotemTrigger(botAI); } + static Trigger* set_tremor_totem(PlayerbotAI* botAI) { return new SetTremorTotemTrigger(botAI); } + static Trigger* set_earthbind_totem(PlayerbotAI* botAI) { return new SetEarthbindTotemTrigger(botAI); } + static Trigger* set_searing_totem(PlayerbotAI* botAI) { return new SetSearingTotemTrigger(botAI); } + static Trigger* set_magma_totem(PlayerbotAI* botAI) { return new SetMagmaTotemTrigger(botAI); } + static Trigger* set_flametongue_totem(PlayerbotAI* botAI) { return new SetFlametongueTotemTrigger(botAI); } + static Trigger* set_totem_of_wrath(PlayerbotAI* botAI) { return new SetTotemOfWrathTrigger(botAI); } + static Trigger* set_frost_resistance_totem(PlayerbotAI* botAI) { return new SetFrostResistanceTotemTrigger(botAI); } + static Trigger* set_healing_stream_totem(PlayerbotAI* botAI) { return new SetHealingStreamTotemTrigger(botAI); } + static Trigger* set_mana_spring_totem(PlayerbotAI* botAI) { return new SetManaSpringTotemTrigger(botAI); } + static Trigger* set_cleansing_totem(PlayerbotAI* botAI) { return new SetCleansingTotemTrigger(botAI); } + static Trigger* set_fire_resistance_totem(PlayerbotAI* botAI) { return new SetFireResistanceTotemTrigger(botAI); } + static Trigger* set_wrath_of_air_totem(PlayerbotAI* botAI) { return new SetWrathOfAirTotemTrigger(botAI); } + static Trigger* set_windfury_totem(PlayerbotAI* botAI) { return new SetWindfuryTotemTrigger(botAI); } + static Trigger* set_nature_resistance_totem(PlayerbotAI* botAI) { return new SetNatureResistanceTotemTrigger(botAI); } + static Trigger* set_grounding_totem(PlayerbotAI* botAI) { return new SetGroundingTotemTrigger(botAI); } }; class ShamanAiObjectContextInternal : public NamedObjectContext @@ -272,11 +264,12 @@ class ShamanAiObjectContextInternal : public NamedObjectContext creators["lightning shield"] = &ShamanAiObjectContextInternal::lightning_shield; creators["wind shear"] = &ShamanAiObjectContextInternal::wind_shear; creators["wind shear on enemy healer"] = &ShamanAiObjectContextInternal::wind_shear_on_enemy_healer; - creators["rockbiter weapon"] = &ShamanAiObjectContextInternal::rockbiter_weapon; - creators["flametongue weapon"] = &ShamanAiObjectContextInternal::flametongue_weapon; - creators["frostbrand weapon"] = &ShamanAiObjectContextInternal::frostbrand_weapon; - creators["windfury weapon"] = &ShamanAiObjectContextInternal::windfury_weapon; - creators["earthliving weapon"] = &ShamanAiObjectContextInternal::earthliving_weapon; + creators["rockbiter weapon main hand"] = &ShamanAiObjectContextInternal::rockbiter_weapon_main_hand; + creators["flametongue weapon main hand"] = &ShamanAiObjectContextInternal::flametongue_weapon_main_hand; + creators["flametongue weapon off hand"] = &ShamanAiObjectContextInternal::flametongue_weapon_off_hand; + // creators["frostbrand weapon off hand"] = &ShamanAiObjectContextInternal::frostbrand_weapon_off_hand; + creators["windfury weapon main hand"] = &ShamanAiObjectContextInternal::windfury_weapon_main_hand; + creators["earthliving weapon main hand"] = &ShamanAiObjectContextInternal::earthliving_weapon_main_hand; creators["purge"] = &ShamanAiObjectContextInternal::purge; creators["healing wave"] = &ShamanAiObjectContextInternal::healing_wave; creators["lesser healing wave"] = &ShamanAiObjectContextInternal::lesser_healing_wave; @@ -308,10 +301,9 @@ class ShamanAiObjectContextInternal : public NamedObjectContext creators["heroism"] = &ShamanAiObjectContextInternal::heroism; creators["bloodlust"] = &ShamanAiObjectContextInternal::bloodlust; creators["elemental mastery"] = &ShamanAiObjectContextInternal::elemental_mastery; - creators["cure disease"] = &ShamanAiObjectContextInternal::cure_disease; - creators["cure disease on party"] = &ShamanAiObjectContextInternal::cure_disease_on_party; - creators["cure poison"] = &ShamanAiObjectContextInternal::cure_poison; - creators["cure poison on party"] = &ShamanAiObjectContextInternal::cure_poison_on_party; + creators["cure toxins"] = &ShamanAiObjectContextInternal::cure_toxins; + creators["cure toxins poison on party"] = &ShamanAiObjectContextInternal::cure_toxins_poison_on_party; + creators["cure toxins disease on party"] = &ShamanAiObjectContextInternal::cure_toxins_disease_on_party; creators["lava burst"] = &ShamanAiObjectContextInternal::lava_burst; creators["earth shield on main tank"] = &ShamanAiObjectContextInternal::earth_shield_on_main_tank; creators["shamanistic rage"] = &ShamanAiObjectContextInternal::shamanistic_rage; @@ -368,10 +360,10 @@ class ShamanAiObjectContextInternal : public NamedObjectContext static Action* frost_shock(PlayerbotAI* botAI) { return new CastFrostShockAction(botAI); } static Action* earth_shock(PlayerbotAI* botAI) { return new CastEarthShockAction(botAI); } static Action* flame_shock(PlayerbotAI* botAI) { return new CastFlameShockAction(botAI); } + static Action* cleanse_spirit(PlayerbotAI* botAI) { return new CastCleanseSpiritAction(botAI); } static Action* cleanse_spirit_poison_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritPoisonOnPartyAction(botAI); } static Action* cleanse_spirit_disease_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritDiseaseOnPartyAction(botAI); } static Action* cleanse_spirit_curse_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritCurseOnPartyAction(botAI); } - static Action* cleanse_spirit(PlayerbotAI* botAI) { return new CastCleanseSpiritAction(botAI); } static Action* water_walking(PlayerbotAI* botAI) { return new CastWaterWalkingAction(botAI); } static Action* water_breathing(PlayerbotAI* botAI) { return new CastWaterBreathingAction(botAI); } static Action* water_walking_on_party(PlayerbotAI* botAI) { return new CastWaterWalkingOnPartyAction(botAI); } @@ -380,11 +372,12 @@ class ShamanAiObjectContextInternal : public NamedObjectContext static Action* lightning_shield(PlayerbotAI* botAI) { return new CastLightningShieldAction(botAI); } static Action* fire_nova(PlayerbotAI* botAI) { return new CastFireNovaAction(botAI); } static Action* wind_shear(PlayerbotAI* botAI) { return new CastWindShearAction(botAI); } - static Action* rockbiter_weapon(PlayerbotAI* botAI) { return new CastRockbiterWeaponAction(botAI); } - static Action* flametongue_weapon(PlayerbotAI* botAI) { return new CastFlametongueWeaponAction(botAI); } - static Action* frostbrand_weapon(PlayerbotAI* botAI) { return new CastFrostbrandWeaponAction(botAI); } - static Action* windfury_weapon(PlayerbotAI* botAI) { return new CastWindfuryWeaponAction(botAI); } - static Action* earthliving_weapon(PlayerbotAI* botAI) { return new CastEarthlivingWeaponAction(botAI); } + static Action* rockbiter_weapon_main_hand(PlayerbotAI* botAI) { return new CastRockbiterWeaponMainHandAction(botAI); } + static Action* flametongue_weapon_main_hand(PlayerbotAI* botAI) { return new CastFlametongueWeaponMainHandAction(botAI); } + static Action* flametongue_weapon_off_hand(PlayerbotAI* botAI) { return new CastFlametongueWeaponOffHandAction(botAI); } + // static Action* frostbrand_weapon_off_hand(PlayerbotAI* botAI) { return new CastFrostbrandWeaponOffHandAction(botAI); } + static Action* earthliving_weapon_main_hand(PlayerbotAI* botAI) { return new CastEarthlivingWeaponMainHandAction(botAI); } + static Action* windfury_weapon_main_hand(PlayerbotAI* botAI) { return new CastWindfuryWeaponMainHandAction(botAI); } static Action* purge(PlayerbotAI* botAI) { return new CastPurgeAction(botAI); } static Action* healing_wave(PlayerbotAI* botAI) { return new CastHealingWaveAction(botAI); } static Action* lesser_healing_wave(PlayerbotAI* botAI) { return new CastLesserHealingWaveAction(botAI); } @@ -399,54 +392,53 @@ class ShamanAiObjectContextInternal : public NamedObjectContext static Action* lava_lash(PlayerbotAI* botAI) { return new CastLavaLashAction(botAI); } static Action* ancestral_spirit(PlayerbotAI* botAI) { return new CastAncestralSpiritAction(botAI); } static Action* wind_shear_on_enemy_healer(PlayerbotAI* botAI) { return new CastWindShearOnEnemyHealerAction(botAI); } - static Action* cure_poison(PlayerbotAI* botAI) { return new CastCurePoisonActionSham(botAI); } - static Action* cure_poison_on_party(PlayerbotAI* botAI) { return new CastCurePoisonOnPartyActionSham(botAI); } - static Action* cure_disease(PlayerbotAI* botAI) { return new CastCureDiseaseActionSham(botAI); } - static Action* cure_disease_on_party(PlayerbotAI* botAI) { return new CastCureDiseaseOnPartyActionSham(botAI); } - static Action* lava_burst(PlayerbotAI* ai) { return new CastLavaBurstAction(ai); } - static Action* earth_shield_on_main_tank(PlayerbotAI* ai) { return new CastEarthShieldOnMainTankAction(ai); } - static Action* shamanistic_rage(PlayerbotAI* ai) { return new CastShamanisticRageAction(ai); } - static Action* feral_spirit(PlayerbotAI* ai) { return new CastFeralSpiritAction(ai); } - static Action* spirit_walk(PlayerbotAI* ai) { return new CastSpiritWalkAction(ai); } - static Action* call_of_the_elements(PlayerbotAI* ai) { return new CastCallOfTheElementsAction(ai); } - static Action* totemic_recall(PlayerbotAI* ai) { return new CastTotemicRecallAction(ai); } - static Action* strength_of_earth_totem(PlayerbotAI* ai) { return new CastStrengthOfEarthTotemAction(ai); } - static Action* stoneskin_totem(PlayerbotAI* ai) { return new CastStoneskinTotemAction(ai); } - static Action* tremor_totem(PlayerbotAI* ai) { return new CastTremorTotemAction(ai); } - static Action* earthbind_totem(PlayerbotAI* ai) { return new CastEarthbindTotemAction(ai); } - static Action* stoneclaw_totem(PlayerbotAI* ai) { return new CastStoneclawTotemAction(ai); } - static Action* searing_totem(PlayerbotAI* ai) { return new CastSearingTotemAction(ai); } - static Action* magma_totem(PlayerbotAI* ai) { return new CastMagmaTotemAction(ai); } - static Action* flametongue_totem(PlayerbotAI* ai) { return new CastFlametongueTotemAction(ai); } - static Action* totem_of_wrath(PlayerbotAI* ai) { return new CastTotemOfWrathAction(ai); } - static Action* frost_resistance_totem(PlayerbotAI* ai) { return new CastFrostResistanceTotemAction(ai); } - static Action* fire_elemental_totem(PlayerbotAI* ai) { return new CastFireElementalTotemAction(ai); } - static Action* fire_elemental_totem_melee(PlayerbotAI* ai) { return new CastFireElementalTotemMeleeAction(ai); } - static Action* healing_stream_totem(PlayerbotAI* ai) { return new CastHealingStreamTotemAction(ai); } - static Action* mana_spring_totem(PlayerbotAI* ai) { return new CastManaSpringTotemAction(ai); } - static Action* cleansing_totem(PlayerbotAI* ai) { return new CastCleansingTotemAction(ai); } - static Action* mana_tide_totem(PlayerbotAI* ai) { return new CastManaTideTotemAction(ai); } - static Action* fire_resistance_totem(PlayerbotAI* ai) { return new CastFireResistanceTotemAction(ai); } - static Action* wrath_of_air_totem(PlayerbotAI* ai) { return new CastWrathOfAirTotemAction(ai); } - static Action* windfury_totem(PlayerbotAI* ai) { return new CastWindfuryTotemAction(ai); } - static Action* nature_resistance_totem(PlayerbotAI* ai) { return new CastNatureResistanceTotemAction(ai); } - static Action* set_strength_of_earth_totem(PlayerbotAI* ai) { return new SetStrengthOfEarthTotemAction(ai); } - static Action* set_stoneskin_totem(PlayerbotAI* ai) { return new SetStoneskinTotemAction(ai); } - static Action* set_tremor_totem(PlayerbotAI* ai) { return new SetTremorTotemAction(ai); } - static Action* set_earthbind_totem(PlayerbotAI* ai) { return new SetEarthbindTotemAction(ai); } - static Action* set_searing_totem(PlayerbotAI* ai) { return new SetSearingTotemAction(ai); } - static Action* set_magma_totem(PlayerbotAI* ai) { return new SetMagmaTotemAction(ai); } - static Action* set_flametongue_totem(PlayerbotAI* ai) { return new SetFlametongueTotemAction(ai); } - static Action* set_totem_of_wrath(PlayerbotAI* ai) { return new SetTotemOfWrathAction(ai); } - static Action* set_frost_resistance_totem(PlayerbotAI* ai) { return new SetFrostResistanceTotemAction(ai); } - static Action* set_healing_stream_totem(PlayerbotAI* ai) { return new SetHealingStreamTotemAction(ai); } - static Action* set_mana_spring_totem(PlayerbotAI* ai) { return new SetManaSpringTotemAction(ai); } - static Action* set_cleansing_totem(PlayerbotAI* ai) { return new SetCleansingTotemAction(ai); } - static Action* set_fire_resistance_totem(PlayerbotAI* ai) { return new SetFireResistanceTotemAction(ai); } - static Action* set_wrath_of_air_totem(PlayerbotAI* ai) { return new SetWrathOfAirTotemAction(ai); } - static Action* set_windfury_totem(PlayerbotAI* ai) { return new SetWindfuryTotemAction(ai); } - static Action* set_nature_resistance_totem(PlayerbotAI* ai) { return new SetNatureResistanceTotemAction(ai); } - static Action* set_grounding_totem(PlayerbotAI* ai) { return new SetGroundingTotemAction(ai); } + static Action* cure_toxins(PlayerbotAI* botAI) { return new CastCureToxinsActionSham(botAI); } + static Action* cure_toxins_poison_on_party(PlayerbotAI* botAI) { return new CastCureToxinsPoisonOnPartyActionSham(botAI); } + static Action* cure_toxins_disease_on_party(PlayerbotAI* botAI) { return new CastCureToxinsDiseaseOnPartyActionSham(botAI); } + static Action* lava_burst(PlayerbotAI* botAI) { return new CastLavaBurstAction(botAI); } + static Action* earth_shield_on_main_tank(PlayerbotAI* botAI) { return new CastEarthShieldOnMainTankAction(botAI); } + static Action* shamanistic_rage(PlayerbotAI* botAI) { return new CastShamanisticRageAction(botAI); } + static Action* feral_spirit(PlayerbotAI* botAI) { return new CastFeralSpiritAction(botAI); } + static Action* spirit_walk(PlayerbotAI* botAI) { return new CastSpiritWalkAction(botAI); } + static Action* call_of_the_elements(PlayerbotAI* botAI) { return new CastCallOfTheElementsAction(botAI); } + static Action* totemic_recall(PlayerbotAI* botAI) { return new CastTotemicRecallAction(botAI); } + static Action* strength_of_earth_totem(PlayerbotAI* botAI) { return new CastStrengthOfEarthTotemAction(botAI); } + static Action* stoneskin_totem(PlayerbotAI* botAI) { return new CastStoneskinTotemAction(botAI); } + static Action* tremor_totem(PlayerbotAI* botAI) { return new CastTremorTotemAction(botAI); } + static Action* earthbind_totem(PlayerbotAI* botAI) { return new CastEarthbindTotemAction(botAI); } + static Action* stoneclaw_totem(PlayerbotAI* botAI) { return new CastStoneclawTotemAction(botAI); } + static Action* searing_totem(PlayerbotAI* botAI) { return new CastSearingTotemAction(botAI); } + static Action* magma_totem(PlayerbotAI* botAI) { return new CastMagmaTotemAction(botAI); } + static Action* flametongue_totem(PlayerbotAI* botAI) { return new CastFlametongueTotemAction(botAI); } + static Action* totem_of_wrath(PlayerbotAI* botAI) { return new CastTotemOfWrathAction(botAI); } + static Action* frost_resistance_totem(PlayerbotAI* botAI) { return new CastFrostResistanceTotemAction(botAI); } + static Action* fire_elemental_totem(PlayerbotAI* botAI) { return new CastFireElementalTotemAction(botAI); } + static Action* fire_elemental_totem_melee(PlayerbotAI* botAI) { return new CastFireElementalTotemMeleeAction(botAI); } + static Action* healing_stream_totem(PlayerbotAI* botAI) { return new CastHealingStreamTotemAction(botAI); } + static Action* mana_spring_totem(PlayerbotAI* botAI) { return new CastManaSpringTotemAction(botAI); } + static Action* cleansing_totem(PlayerbotAI* botAI) { return new CastCleansingTotemAction(botAI); } + static Action* mana_tide_totem(PlayerbotAI* botAI) { return new CastManaTideTotemAction(botAI); } + static Action* fire_resistance_totem(PlayerbotAI* botAI) { return new CastFireResistanceTotemAction(botAI); } + static Action* wrath_of_air_totem(PlayerbotAI* botAI) { return new CastWrathOfAirTotemAction(botAI); } + static Action* windfury_totem(PlayerbotAI* botAI) { return new CastWindfuryTotemAction(botAI); } + static Action* nature_resistance_totem(PlayerbotAI* botAI) { return new CastNatureResistanceTotemAction(botAI); } + static Action* set_strength_of_earth_totem(PlayerbotAI* botAI) { return new SetStrengthOfEarthTotemAction(botAI); } + static Action* set_stoneskin_totem(PlayerbotAI* botAI) { return new SetStoneskinTotemAction(botAI); } + static Action* set_tremor_totem(PlayerbotAI* botAI) { return new SetTremorTotemAction(botAI); } + static Action* set_earthbind_totem(PlayerbotAI* botAI) { return new SetEarthbindTotemAction(botAI); } + static Action* set_searing_totem(PlayerbotAI* botAI) { return new SetSearingTotemAction(botAI); } + static Action* set_magma_totem(PlayerbotAI* botAI) { return new SetMagmaTotemAction(botAI); } + static Action* set_flametongue_totem(PlayerbotAI* botAI) { return new SetFlametongueTotemAction(botAI); } + static Action* set_totem_of_wrath(PlayerbotAI* botAI) { return new SetTotemOfWrathAction(botAI); } + static Action* set_frost_resistance_totem(PlayerbotAI* botAI) { return new SetFrostResistanceTotemAction(botAI); } + static Action* set_healing_stream_totem(PlayerbotAI* botAI) { return new SetHealingStreamTotemAction(botAI); } + static Action* set_mana_spring_totem(PlayerbotAI* botAI) { return new SetManaSpringTotemAction(botAI); } + static Action* set_cleansing_totem(PlayerbotAI* botAI) { return new SetCleansingTotemAction(botAI); } + static Action* set_fire_resistance_totem(PlayerbotAI* botAI) { return new SetFireResistanceTotemAction(botAI); } + static Action* set_wrath_of_air_totem(PlayerbotAI* botAI) { return new SetWrathOfAirTotemAction(botAI); } + static Action* set_windfury_totem(PlayerbotAI* botAI) { return new SetWindfuryTotemAction(botAI); } + static Action* set_nature_resistance_totem(PlayerbotAI* botAI) { return new SetNatureResistanceTotemAction(botAI); } + static Action* set_grounding_totem(PlayerbotAI* botAI) { return new SetGroundingTotemAction(botAI); } }; SharedNamedObjectContextList ShamanAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp index c1295687cdd..5ef0d3f55c5 100644 --- a/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp @@ -4,42 +4,11 @@ */ #include "ElementalShamanStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class ElementalShamanStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - ElementalShamanStrategyActionNodeFactory() - { - creators["flame shock"] = &flame_shock; - creators["earth shock"] = &earth_shock; - creators["lava burst"] = &lava_burst; - creators["lightning bolt"] = &lightning_bolt; - creators["call of the elements"] = &call_of_the_elements; - creators["elemental mastery"] = &elemental_mastery; - creators["stoneclaw totem"] = &stoneclaw_totem; - creators["water shield"] = &water_shield; - creators["thunderstorm"] = &thunderstorm; - } - -private: - static ActionNode* flame_shock(PlayerbotAI*) { return new ActionNode("flame shock", {}, {}, {}); } - static ActionNode* earth_shock(PlayerbotAI*) { return new ActionNode("earth shock", {}, {}, {}); } - static ActionNode* lava_burst(PlayerbotAI*) { return new ActionNode("lava burst", {}, {}, {}); } - static ActionNode* lightning_bolt(PlayerbotAI*) { return new ActionNode("lightning bolt", {}, {}, {}); } - static ActionNode* call_of_the_elements(PlayerbotAI*) { return new ActionNode("call of the elements", {}, {}, {}); } - static ActionNode* elemental_mastery(PlayerbotAI*) { return new ActionNode("elemental mastery", {}, {}, {}); } - static ActionNode* stoneclaw_totem(PlayerbotAI*) { return new ActionNode("stoneclaw totem", {}, {}, {}); } - static ActionNode* water_shield(PlayerbotAI*) { return new ActionNode("water shield", {}, {}, {}); } - static ActionNode* thunderstorm(PlayerbotAI*) { return new ActionNode("thunderstorm", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== ElementalShamanStrategy::ElementalShamanStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) { - actionNodeFactories.Add(new ElementalShamanStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp index 4b7fb7159e0..0eb548993d5 100644 --- a/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp @@ -4,7 +4,6 @@ */ #include "EnhancementShamanStrategy.h" - #include "Playerbots.h" // ===== Action Node Factory ===== @@ -13,19 +12,10 @@ class EnhancementShamanStrategyActionNodeFactory : public NamedObjectFactory& triggers) triggers.push_back(new TriggerNode("wind shear", { NextAction("wind shear", 23.0f), })); triggers.push_back(new TriggerNode("wind shear on enemy healer", { NextAction("wind shear on enemy healer", 23.0f), })); triggers.push_back(new TriggerNode("purge", { NextAction("purge", ACTION_DISPEL), })); - triggers.push_back(new TriggerNode("medium mana", { NextAction("mana potion", ACTION_DISPEL), })); triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 65.0f), })); } void ShamanCureStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("cure poison", { NextAction("cure poison", 21.0f), })); - triggers.push_back(new TriggerNode("party member cure poison", { NextAction("cure poison on party", 21.0f), })); triggers.push_back(new TriggerNode("cleanse spirit poison", { NextAction("cleanse spirit", 24.0f), })); triggers.push_back(new TriggerNode("party member cleanse spirit poison", { NextAction("cleanse spirit poison on party", 23.0f), })); - triggers.push_back(new TriggerNode("cure disease", { NextAction("cure disease", 31.0f), })); - triggers.push_back(new TriggerNode("party member cure disease", { NextAction("cure disease on party", 30.0f), })); triggers.push_back(new TriggerNode("cleanse spirit disease", { NextAction("cleanse spirit", 24.0f), })); triggers.push_back(new TriggerNode("party member cleanse spirit disease", { NextAction("cleanse spirit disease on party", 23.0f), })); triggers.push_back(new TriggerNode("cleanse spirit curse", { NextAction("cleanse spirit", 24.0f), })); @@ -133,11 +138,11 @@ void ShamanBoostStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { triggers.push_back(new TriggerNode("fire elemental totem", { NextAction("fire elemental totem", 23.0f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { triggers.push_back(new TriggerNode("fire elemental totem", { NextAction("fire elemental totem melee", 24.0f), })); } @@ -149,23 +154,19 @@ void ShamanAoeStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { triggers.push_back(new TriggerNode("medium aoe",{ NextAction("fire nova", 23.0f), })); triggers.push_back(new TriggerNode("chain lightning no cd", { NextAction("chain lightning", 5.6f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { - triggers.push_back(new TriggerNode("medium aoe",{ - NextAction("magma totem", 24.0f), - NextAction("fire nova", 23.0f), })); + triggers.push_back(new TriggerNode("medium aoe",{ NextAction("magma totem", 24.0f), + NextAction("fire nova", 23.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 5 and medium aoe", { NextAction("chain lightning", 22.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 4 and medium aoe", { NextAction("chain lightning", 21.0f), })); triggers.push_back(new TriggerNode("enemy within melee", { NextAction("fire nova", 5.1f), })); } - else if (tab == 2) // Restoration - { - // Handled by "Healer DPS" Strategy - } + // Resto AoE handled by "Healer DPS" Strategy } diff --git a/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp index 698531a858c..37f55c6c6c8 100644 --- a/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp @@ -4,64 +4,11 @@ */ #include "RestoShamanStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class RestoShamanStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - RestoShamanStrategyActionNodeFactory() - { - creators["mana tide totem"] = &mana_tide_totem; - creators["call of the elements"] = &call_of_the_elements; - creators["stoneclaw totem"] = &stoneclaw_totem; - creators["riptide on party"] = &riptide_on_party; - creators["chain heal on party"] = &chain_heal_on_party; - creators["healing wave on party"] = &healing_wave_on_party; - creators["lesser healing wave on party"] = &lesser_healing_wave_on_party; - creators["earth shield on main tank"] = &earth_shield_on_main_tank; - creators["cleanse spirit poison on party"] = &cleanse_spirit_poison_on_party; - creators["cleanse spirit disease on party"] = &cleanse_spirit_disease_on_party; - creators["cleanse spirit curse on party"] = &cleanse_spirit_curse_on_party; - creators["cleansing totem"] = &cleansing_totem; - creators["water shield"] = &water_shield; - creators["flame shock"] = &flame_shock; - creators["lava burst"] = &lava_burst; - creators["lightning bolt"] = &lightning_bolt; - creators["chain lightning"] = &chain_lightning; - } - -private: - static ActionNode* mana_tide_totem([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("mana tide totem", - /*P*/ {}, - /*A*/ { NextAction("mana potion") }, - /*C*/ {}); - } - static ActionNode* call_of_the_elements(PlayerbotAI*) { return new ActionNode("call of the elements", {}, {}, {}); } - static ActionNode* stoneclaw_totem(PlayerbotAI*) { return new ActionNode("stoneclaw totem", {}, {}, {}); } - static ActionNode* riptide_on_party(PlayerbotAI*) { return new ActionNode("riptide on party", {}, {}, {}); } - static ActionNode* chain_heal_on_party(PlayerbotAI*) { return new ActionNode("chain heal on party", {}, {}, {}); } - static ActionNode* healing_wave_on_party(PlayerbotAI*) { return new ActionNode("healing wave on party", {}, {}, {}); } - static ActionNode* lesser_healing_wave_on_party(PlayerbotAI*) { return new ActionNode("lesser healing wave on party", {}, {}, {}); } - static ActionNode* earth_shield_on_main_tank(PlayerbotAI*) { return new ActionNode("earth shield on main tank", {}, {}, {}); } - static ActionNode* cleanse_spirit_poison_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit poison on party", {}, {}, {}); } - static ActionNode* cleanse_spirit_disease_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit disease on party", {}, {}, {}); } - static ActionNode* cleanse_spirit_curse_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit curse on party", {}, {}, {}); } - static ActionNode* cleansing_totem(PlayerbotAI*) { return new ActionNode("cleansing totem", {}, {}, {}); } - static ActionNode* water_shield(PlayerbotAI*) { return new ActionNode("water shield", {}, {}, {}); } - static ActionNode* flame_shock(PlayerbotAI*) { return new ActionNode("flame shock", {}, {}, {}); } - static ActionNode* lava_burst(PlayerbotAI*) { return new ActionNode("lava burst", {}, {}, {}); } - static ActionNode* lightning_bolt(PlayerbotAI*) { return new ActionNode("lightning bolt", {}, {}, {}); } - static ActionNode* chain_lightning(PlayerbotAI*) { return new ActionNode("chain lightning", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== RestoShamanStrategy::RestoShamanStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) { - actionNodeFactories.Add(new RestoShamanStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Trigger Initialization === @@ -75,28 +22,23 @@ void RestoShamanStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("medium mana", { NextAction("mana tide totem", ACTION_HIGH + 5) })); // Healing Triggers - triggers.push_back(new TriggerNode("group heal setting", { - NextAction("riptide on party", 27.0f), - NextAction("chain heal on party", 26.0f) })); + triggers.push_back(new TriggerNode("group heal setting", { NextAction("riptide on party", 27.0f), + NextAction("chain heal on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member critical health", { - NextAction("riptide on party", 25.0f), - NextAction("healing wave on party", 24.0f), - NextAction("lesser healing wave on party", 23.0f) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("riptide on party", 25.0f), + NextAction("healing wave on party", 24.0f), + NextAction("lesser healing wave on party", 23.0f) })); - triggers.push_back(new TriggerNode("party member low health", { - NextAction("riptide on party", 19.0f), - NextAction("healing wave on party", 18.0f), - NextAction("lesser healing wave on party", 17.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("riptide on party", 19.0f), + NextAction("healing wave on party", 18.0f), + NextAction("lesser healing wave on party", 17.0f) })); - triggers.push_back(new TriggerNode("party member medium health", { - NextAction("riptide on party", 16.0f), - NextAction("healing wave on party", 15.0f), - NextAction("lesser healing wave on party", 14.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("riptide on party", 16.0f), + NextAction("healing wave on party", 15.0f), + NextAction("lesser healing wave on party", 14.0f) })); - triggers.push_back(new TriggerNode("party member almost full health", { - NextAction("riptide on party", 12.0f), - NextAction("lesser healing wave on party", 11.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("riptide on party", 12.0f), + NextAction("lesser healing wave on party", 11.0f) })); triggers.push_back(new TriggerNode("earth shield on main tank", { NextAction("earth shield on main tank", ACTION_HIGH + 7) })); @@ -113,12 +55,9 @@ void RestoShamanStrategy::InitTriggers(std::vector& triggers) void ShamanHealerDpsStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("healer should attack", - { NextAction("flame shock", ACTION_DEFAULT + 0.2f), - NextAction("lava burst", ACTION_DEFAULT + 0.1f), - NextAction("lightning bolt", ACTION_DEFAULT) })); + triggers.push_back(new TriggerNode("healer should attack", { NextAction("flame shock", ACTION_DEFAULT + 0.2f), + NextAction("lava burst", ACTION_DEFAULT + 0.1f), + NextAction("lightning bolt", ACTION_DEFAULT) })); - triggers.push_back( - new TriggerNode("medium aoe and healer should attack", - { NextAction("chain lightning", ACTION_DEFAULT + 0.3f) })); + triggers.push_back( new TriggerNode("medium aoe and healer should attack", { NextAction("chain lightning", ACTION_DEFAULT + 0.3f) })); } diff --git a/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp b/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp index c72000539c7..1e10b46c9b7 100644 --- a/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp @@ -13,45 +13,65 @@ class ShamanNonCombatStrategyActionNodeFactory : public NamedObjectFactory& triggers) NonCombatStrategy::InitTriggers(triggers); // Totemic Recall - triggers.push_back(new TriggerNode("totemic recall", { NextAction("totemic recall", 60.0f), })); + triggers.push_back(new TriggerNode("totemic recall", { NextAction("totemic recall", 60.0f) })); // Healing/Resurrect Triggers - triggers.push_back(new TriggerNode("party member dead", { NextAction("ancestral spirit", ACTION_CRITICAL_HEAL + 10), })); - triggers.push_back(new TriggerNode("party member critical health", { - NextAction("riptide on party", 31.0f), - NextAction("healing wave on party", 30.0f) })); - triggers.push_back(new TriggerNode("party member low health",{ - NextAction("riptide on party", 29.0f), - NextAction("healing wave on party", 28.0f) })); - triggers.push_back(new TriggerNode("party member medium health",{ - NextAction("riptide on party", 27.0f), - NextAction("healing wave on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member almost full health",{ - NextAction("riptide on party", 25.0f), - NextAction("lesser healing wave on party", 24.0f) })); - triggers.push_back(new TriggerNode("group heal setting",{ NextAction("chain heal on party", 27.0f) })); + triggers.push_back(new TriggerNode("party member dead", { NextAction("ancestral spirit", ACTION_CRITICAL_HEAL + 10) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("riptide on party", 31.0f), + NextAction("healing wave on party", 30.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("riptide on party", 29.0f), + NextAction("healing wave on party", 28.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("riptide on party", 27.0f), + NextAction("healing wave on party", 26.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("riptide on party", 25.0f), + NextAction("lesser healing wave on party", 24.0f) })); + triggers.push_back(new TriggerNode("group heal setting", { NextAction("chain heal on party", 27.0f) })); // Cure Triggers - triggers.push_back(new TriggerNode("cure poison", { NextAction("cure poison", 21.0f), })); - triggers.push_back(new TriggerNode("party member cure poison", { NextAction("cure poison on party", 21.0f), })); - triggers.push_back(new TriggerNode("cure disease", { NextAction("cure disease", 31.0f), })); - triggers.push_back(new TriggerNode("party member cure disease", { NextAction("cure disease on party", 30.0f), })); + triggers.push_back(new TriggerNode("cleanse spirit poison", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit poison", { NextAction("cleanse spirit poison on party", 23.0f) })); + triggers.push_back(new TriggerNode("cleanse spirit disease", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit disease", { NextAction("cleanse spirit disease on party", 23.0f) })); + triggers.push_back(new TriggerNode("cleanse spirit curse", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit curse", { NextAction("cleanse spirit curse on party", 23.0f) })); // Out of Combat Buff Triggers Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { - triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("flametongue weapon", 22.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("flametongue weapon main hand", 22.0f), })); triggers.push_back(new TriggerNode("water shield", { NextAction("water shield", 21.0f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { - triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("windfury weapon", 22.0f), })); - triggers.push_back(new TriggerNode("off hand weapon no imbue", { NextAction("flametongue weapon", 21.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("windfury weapon main hand", 22.0f), })); + triggers.push_back(new TriggerNode("off hand weapon no imbue", { NextAction("flametongue weapon off hand", 21.0f), })); triggers.push_back(new TriggerNode("lightning shield", { NextAction("lightning shield", 20.0f), })); } - else if (tab == 2) // Restoration + else if (tab == SHAMAN_TAB_RESTORATION) { - triggers.push_back(new TriggerNode("main hand weapon no imbue",{ NextAction("earthliving weapon", 22.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("earthliving weapon main hand", 22.0f), })); triggers.push_back(new TriggerNode("water shield", { NextAction("water shield", 20.0f), })); } diff --git a/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp b/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp index d00cc5e6cb7..c5ae222c16f 100644 --- a/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp +++ b/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp @@ -75,13 +75,9 @@ void TotemOfWrathStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Totem of Wrath yet, set Flametongue Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(30706)) - { triggers.push_back(new TriggerNode("set totem of wrath", { NextAction("set totem of wrath", 60.0f) })); - } else if (bot->HasSpell(8227)) - { triggers.push_back(new TriggerNode("set flametongue totem", { NextAction("set flametongue totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no fire totem", { NextAction("totem of wrath", 55.0f) })); } @@ -117,13 +113,9 @@ void CleansingTotemStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Cleansing Totem yet, set Mana Spring Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(8170)) - { triggers.push_back(new TriggerNode("set cleansing totem", { NextAction("set cleansing totem", 60.0f) })); - } else if (bot->HasSpell(5675)) - { triggers.push_back(new TriggerNode("set mana spring totem", { NextAction("set mana spring totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no water totem", { NextAction("cleansing totem", 55.0f) })); } @@ -143,15 +135,10 @@ void WrathOfAirTotemStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Wrath of Air Totem yet, set Grounding Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(3738)) - { triggers.push_back(new TriggerNode("set wrath of air totem", { NextAction("set wrath of air totem", 60.0f) })); - } else if (bot->HasSpell(8177)) - { triggers.push_back(new TriggerNode("set grounding totem", { NextAction("set grounding totem", 60.0f) })); - } - triggers.push_back( - new TriggerNode("no air totem", { NextAction("wrath of air totem", 55.0f) })); + triggers.push_back( new TriggerNode("no air totem", { NextAction("wrath of air totem", 55.0f) })); } WindfuryTotemStrategy::WindfuryTotemStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) {} @@ -161,13 +148,9 @@ void WindfuryTotemStrategy::InitTriggers(std::vector& triggers) // If the bot hasn't learned Windfury Totem yet, set Grounding Totem instead. Player* bot = botAI->GetBot(); if (bot->HasSpell(8512)) - { triggers.push_back(new TriggerNode("set windfury totem", { NextAction("set windfury totem", 60.0f) })); - } else if (bot->HasSpell(8177)) - { triggers.push_back(new TriggerNode("set grounding totem", { NextAction("set grounding totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no air totem", { NextAction("windfury totem", 55.0f) })); } diff --git a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp index c8b36135a97..e7bcbfe4cf2 100644 --- a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp +++ b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp @@ -261,13 +261,13 @@ bool TotemicRecallTrigger::IsActive() } // Find the active totem strategy for this slot, and return the highest-rank spellId the bot knows for it -static uint32 GetRequiredTotemSpellId(PlayerbotAI* ai, const char* strategies[], +static uint32 GetRequiredTotemSpellId(PlayerbotAI* botAI, const char* strategies[], const uint32* spellList[], const size_t spellCounts[], size_t numStrategies) { - Player* bot = ai->GetBot(); + Player* bot = botAI->GetBot(); for (size_t i = 0; i < numStrategies; ++i) { - if (ai->HasStrategy(strategies[i], BOT_STATE_COMBAT)) + if (botAI->HasStrategy(strategies[i], BOT_STATE_COMBAT)) { // Find the highest-rank spell the bot knows for (size_t j = 0; j < spellCounts[i]; ++j) diff --git a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h index 9e1a86aace0..800dc834269 100644 --- a/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h +++ b/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h @@ -41,14 +41,14 @@ const uint32 SPELL_CALL_OF_THE_ELEMENTS = 66842; class MainHandWeaponNoImbueTrigger : public BuffTrigger { public: - MainHandWeaponNoImbueTrigger(PlayerbotAI* ai) : BuffTrigger(ai, "main hand", 1) {} + MainHandWeaponNoImbueTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "main hand", 1) {} virtual bool IsActive(); }; class OffHandWeaponNoImbueTrigger : public BuffTrigger { public: - OffHandWeaponNoImbueTrigger(PlayerbotAI* ai) : BuffTrigger(ai, "off hand", 1) {} + OffHandWeaponNoImbueTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "off hand", 1) {} virtual bool IsActive(); }; @@ -121,7 +121,7 @@ class ElementalMasteryTrigger : public BuffTrigger class SpiritWalkTrigger : public Trigger { public: - SpiritWalkTrigger(PlayerbotAI* ai) : Trigger(ai, "spirit walk ready") {} + SpiritWalkTrigger(PlayerbotAI* botAI) : Trigger(botAI, "spirit walk ready") {} bool IsActive() override; @@ -165,9 +165,7 @@ class PartyMemberCleanseSpiritPoisonTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritPoisonTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_POISON) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_POISON) {} }; class CleanseSpiritCurseTrigger : public NeedCureTrigger @@ -180,9 +178,7 @@ class PartyMemberCleanseSpiritCurseTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritCurseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_CURSE) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_CURSE) {} }; class CleanseSpiritDiseaseTrigger : public NeedCureTrigger @@ -195,34 +191,7 @@ class PartyMemberCleanseSpiritDiseaseTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_DISEASE) - { - } -}; - -class CurePoisonTrigger : public NeedCureTrigger -{ -public: - CurePoisonTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class PartyMemberCurePoisonTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCurePoisonTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class CureDiseaseTrigger : public NeedCureTrigger -{ -public: - CureDiseaseTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure disease", DISPEL_DISEASE) {} -}; - -class PartyMemberCureDiseaseTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCureDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cure disease", DISPEL_DISEASE) {} + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_DISEASE) {} }; // Damage and Debuff Triggers @@ -250,7 +219,7 @@ class MaelstromWeaponTrigger : public HasAuraStackTrigger class FlameShockTrigger : public DebuffTrigger { public: - FlameShockTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "flame shock", 1, true, 6.0f) {} + FlameShockTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "flame shock", 1, true, 6.0f) {} bool IsActive() override { return BuffTrigger::IsActive(); } }; @@ -265,19 +234,19 @@ class EarthShockExecuteTrigger : public Trigger class MaelstromWeapon5AndMediumAoeTrigger : public TwoTriggers { public: - MaelstromWeapon5AndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "maelstrom weapon 5", "medium aoe") {} + MaelstromWeapon5AndMediumAoeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "maelstrom weapon 5", "medium aoe") {} }; class MaelstromWeapon4AndMediumAoeTrigger : public TwoTriggers { public: - MaelstromWeapon4AndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "maelstrom weapon 4", "medium aoe") {} + MaelstromWeapon4AndMediumAoeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "maelstrom weapon 4", "medium aoe") {} }; class ChainLightningNoCdTrigger : public SpellNoCooldownTrigger { public: - ChainLightningNoCdTrigger(PlayerbotAI* ai) : SpellNoCooldownTrigger(ai, "chain lightning") {} + ChainLightningNoCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "chain lightning") {} }; // Healing Triggers @@ -307,49 +276,49 @@ class TotemTrigger : public Trigger class CallOfTheElementsTrigger : public Trigger { public: - CallOfTheElementsTrigger(PlayerbotAI* ai) : Trigger(ai, "call of the elements") {} + CallOfTheElementsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "call of the elements") {} bool IsActive() override; }; class TotemicRecallTrigger : public Trigger { public: - TotemicRecallTrigger(PlayerbotAI* ai) : Trigger(ai, "totemic recall") {} + TotemicRecallTrigger(PlayerbotAI* botAI) : Trigger(botAI, "totemic recall") {} bool IsActive() override; }; class NoEarthTotemTrigger : public Trigger { public: - NoEarthTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no earth totem") {} + NoEarthTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no earth totem") {} bool IsActive() override; }; class NoFireTotemTrigger : public Trigger { public: - NoFireTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no fire totem") {} + NoFireTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no fire totem") {} bool IsActive() override; }; class NoWaterTotemTrigger : public Trigger { public: - NoWaterTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no water totem") {} + NoWaterTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no water totem") {} bool IsActive() override; }; class NoAirTotemTrigger : public Trigger { public: - NoAirTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no air totem") {} + NoAirTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no air totem") {} bool IsActive() override; }; class CallOfTheElementsAndEnemyWithinMeleeTrigger : public TwoTriggers { public: - CallOfTheElementsAndEnemyWithinMeleeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "call of the elements", "enemy within melee") {} + CallOfTheElementsAndEnemyWithinMeleeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "call of the elements", "enemy within melee") {} }; // Set Strategy Assigned Totems @@ -359,8 +328,8 @@ class SetTotemTrigger : public Trigger public: // Template constructor: infers N (size of the id array) at compile time template - SetTotemTrigger(PlayerbotAI* ai, std::string const& spellName, const uint32 (&ids)[N], int actionButtonId) - : Trigger(ai, "set " + spellName) + SetTotemTrigger(PlayerbotAI* botAI, std::string const& spellName, const uint32 (&ids)[N], int actionButtonId) + : Trigger(botAI, "set " + spellName) , totemSpellIds(ids) , totemSpellIdsCount(N) , actionButtonId(actionButtonId) @@ -376,120 +345,120 @@ class SetTotemTrigger : public Trigger class SetStrengthOfEarthTotemTrigger : public SetTotemTrigger { public: - SetStrengthOfEarthTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStrengthOfEarthTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetStoneskinTotemTrigger : public SetTotemTrigger { public: - SetStoneskinTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStoneskinTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetTremorTotemTrigger : public SetTotemTrigger { public: - SetTremorTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetTremorTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetEarthbindTotemTrigger : public SetTotemTrigger { public: - SetEarthbindTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetEarthbindTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetSearingTotemTrigger : public SetTotemTrigger { public: - SetSearingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetSearingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetMagmaTotemTrigger : public SetTotemTrigger { public: - SetMagmaTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetMagmaTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetFlametongueTotemTrigger : public SetTotemTrigger { public: - SetFlametongueTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFlametongueTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetTotemOfWrathTrigger : public SetTotemTrigger { public: - SetTotemOfWrathTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} + SetTotemOfWrathTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} }; class SetFrostResistanceTotemTrigger : public SetTotemTrigger { public: - SetFrostResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFrostResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetHealingStreamTotemTrigger : public SetTotemTrigger { public: - SetHealingStreamTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetHealingStreamTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetManaSpringTotemTrigger : public SetTotemTrigger { public: - SetManaSpringTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetManaSpringTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetCleansingTotemTrigger : public SetTotemTrigger { public: - SetCleansingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetCleansingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetFireResistanceTotemTrigger : public SetTotemTrigger { public: - SetFireResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetFireResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetWrathOfAirTotemTrigger : public SetTotemTrigger { public: - SetWrathOfAirTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWrathOfAirTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetWindfuryTotemTrigger : public SetTotemTrigger { public: - SetWindfuryTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWindfuryTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetNatureResistanceTotemTrigger : public SetTotemTrigger { public: - SetNatureResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetNatureResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetGroundingTotemTrigger : public SetTotemTrigger { public: - SetGroundingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetGroundingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; #endif From 579f9726665385825278879b65f504c8d523592f Mon Sep 17 00:00:00 2001 From: Chris Lacy <129199071+SI-ChrisL@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:29:23 -0500 Subject: [PATCH 41/87] fix: invert NoRtiTrigger condition so mark rti strategy works (#2256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - `NoRtiTrigger::IsActive()` returns `target != nullptr`, meaning it only fires when a raid icon is **already** assigned — creating a chicken-and-egg problem where the bot never places the first mark - Invert to `target == nullptr` so the trigger fires when **no** mark exists, prompting `MarkRtiAction` to mark the lowest-HP unmarked attacker - Once a mark is placed, the trigger stops firing until the marked target dies ## Test plan - [ ] Add `mark rti` strategy to a tank bot via `co +mark rti` - [ ] Enter combat with multiple mobs — bot should auto-mark lowest HP target with skull - [ ] Verify mark persists until target dies, then bot marks next target - [ ] Verify no marking occurs out of combat 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar Co-authored-by: Claude Opus 4.6 (1M context) --- src/Ai/Base/Trigger/RtiTriggers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ai/Base/Trigger/RtiTriggers.cpp b/src/Ai/Base/Trigger/RtiTriggers.cpp index 6158bc4d600..c7dc737cf33 100644 --- a/src/Ai/Base/Trigger/RtiTriggers.cpp +++ b/src/Ai/Base/Trigger/RtiTriggers.cpp @@ -16,5 +16,5 @@ bool NoRtiTrigger::IsActive() return false; Unit* target = AI_VALUE(Unit*, "rti target"); - return target != nullptr; + return target == nullptr; } From f76c286353c41661c47e095a14a2836725c636a8 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 3 Apr 2026 15:31:30 -0500 Subject: [PATCH 42/87] Fix Destruction Warlock Glyphs, Take Two (#2278) ## Pull Request Description When I previously "fixed" the default glyphs for destro pve, I accidentally put Life Tap twice. This PR replaces the second Life Tap with Incinerate (which is what I intended). I also fixed a typo in the config. ## How to Test the Changes Use maintenance on a level 80 Warlock with destro pve spec. Their major glyphs should be, in order, Life Tap, Conflagrate, and Incinerate. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) The default glyphs are wrong. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index c1c06b8b717..5df2b0a9784 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -582,7 +582,7 @@ AiPlayerbot.AutoGearScoreLimit = 0 AiPlayerbot.BotCheats = "food,taxi,raid" # List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. -# While mod-playerbots does not restore removed attunement requirements, although other mods, such as mod-individual-progression, may do so. +# While mod-playerbots does not restore removed attunement requirements, other mods, such as mod-individual-progression, may do so. # This is meant to exclude bots from such requirements. # # Default: @@ -1687,7 +1687,7 @@ AiPlayerbot.PremadeSpecLink.9.1.60 = -003203301135112530135201051 AiPlayerbot.PremadeSpecLink.9.1.70 = -003203301135112530135201051-55 AiPlayerbot.PremadeSpecLink.9.1.80 = -003203301135112530135221351-55000005 AiPlayerbot.PremadeSpecName.9.2 = destro pve -AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,45785 +AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,42453 AiPlayerbot.PremadeSpecLink.9.2.60 = --05203215200231051305031151 AiPlayerbot.PremadeSpecLink.9.2.80 = 23-0302-05203215220331051335231351 AiPlayerbot.PremadeSpecName.9.3 = affli pvp From c0390a24fd9b0ab622d7ea246b57b16c3cc0f5d8 Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:54:46 +0200 Subject: [PATCH 43/87] feat(Performance): BotActiveAlone activity interval fixes and default settings for avg player (#2250) ## Pull Request Description - Bugfix the jittering on/off of botAlone activity - BotActiveAlone activity duration configurable - Updated the default config values for general user for a smoother experience - Added offset jittering for the check allowedActivity and check next AI delay to prevent cpu spikes (disabled WhenIsFriend can cause race conditions) ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [ ] Minimal impact (**explain below**) - - [x] Moderate impact (**explain below**) In a positive way, bots in your zone and 150 radius will always be active, meanwhile other bots will be active 40% of the time with intervals for 60 seconds per bot. With much lower latencies. All configurable without say. 40% and 60 seconds for more balance for those who seek bots create world feel more natural and live vs bots leveling without killing the server performance. Why not 50 due activity of bots itself 40% will result more into 45-50% like behavior and 50% prolly more 55%-60%. This it not something we want incorporate when calculating the value since it depends on various config and situation. But 40% is good base with default config. - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 28 +++++++---- src/Bot/Engine/PlayerbotAIBase.cpp | 10 ++-- src/Bot/Engine/PlayerbotAIBase.h | 3 +- src/Bot/PlayerbotAI.cpp | 76 +++++++++++++++--------------- src/Bot/PlayerbotAI.h | 3 +- src/PlayerbotAIConfig.cpp | 5 +- src/PlayerbotAIConfig.h | 1 + 7 files changed, 72 insertions(+), 54 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 5df2b0a9784..ff1b393b58b 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -866,22 +866,32 @@ AiPlayerbot.ExcludedHunterPetFamilies = "" # #################################################################################################### - #################################################################################################### -# ACTIVITIES +# ACTIVITY # +# BotActiveAlone (%) +# - Determines the percentage of bots that remain active when no real players are nearby. +# - Default is 40% (which is practise kinda translates into 45-50%). +# - If `botActiveAloneSmartScale` is enabled, automatically temporarily down scale activity based on latency. +# - 40% will be activated in a random rotation for the amount of seconds specified in . +# - There are multiple conditions when bots will forced to be active e.g. when in BG/instance/attacked, some are configurable below. +#- When 100$ all bots will be active without any rotation or logic applied but comes with performance hit. # -# Specify percent of active bots -# The default is 100% but will be automatically adjusted if botActiveAloneSmartScale -# is enabled. Regardless, this value is only applied to inactive areas where no real players -# are detected. When real players are nearby, the value is always enforced to 100% -AiPlayerbot.BotActiveAlone = 100 +AiPlayerbot.BotActiveAlone = 10 +AiPlayerbot.BotActiveAloneDurationSeconds = 30 -# Force botActiveAlone when bot is within the specified distance of a real player +# Some additional rules that enforces the bot to be active +# +# - bot is within this distance from a real player. +# - bot is in the same zone as a real player. +# - bot is in the same continent as a real player. +# - bot is a real player's friend. +# - bot is in a real player's guild. +# AiPlayerbot.BotActiveAloneForceWhenInRadius = 150 AiPlayerbot.BotActiveAloneForceWhenInZone = 1 AiPlayerbot.BotActiveAloneForceWhenInMap = 0 -AiPlayerbot.BotActiveAloneForceWhenIsFriend = 1 +AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0 AiPlayerbot.BotActiveAloneForceWhenInGuild = 1 # SmartScale (automatic scaling of percentage of active bots based on latency) diff --git a/src/Bot/Engine/PlayerbotAIBase.cpp b/src/Bot/Engine/PlayerbotAIBase.cpp index cf4ad172cc9..46e8c0bf508 100644 --- a/src/Bot/Engine/PlayerbotAIBase.cpp +++ b/src/Bot/Engine/PlayerbotAIBase.cpp @@ -25,7 +25,7 @@ void PlayerbotAIBase::UpdateAI(uint32 elapsed, bool minimal) return; UpdateAIInternal(elapsed, minimal); - YieldThread(); + YieldThread(nullptr); } void PlayerbotAIBase::SetNextCheckDelay(uint32 const delay) @@ -49,10 +49,14 @@ void PlayerbotAIBase::IncreaseNextCheckDelay(uint32 delay) bool PlayerbotAIBase::CanUpdateAI() { return nextAICheckDelay == 0; } -void PlayerbotAIBase::YieldThread(uint32 delay) +void PlayerbotAIBase::YieldThread(Player* bot, uint32 delay) { if (nextAICheckDelay < delay) - nextAICheckDelay = delay; + { + // Adding a deterministic per-bot slight offset (0–200 ms) to stagger updates and prevent cpu spikes. + uint32 offset = bot ? (bot->GetGUID().GetCounter() % 201) : 0; + nextAICheckDelay = delay + offset; + } } bool PlayerbotAIBase::IsActive() { return nextAICheckDelay < sPlayerbotAIConfig.maxWaitForMove; } diff --git a/src/Bot/Engine/PlayerbotAIBase.h b/src/Bot/Engine/PlayerbotAIBase.h index d0e0b775b20..2d6ab31ce41 100644 --- a/src/Bot/Engine/PlayerbotAIBase.h +++ b/src/Bot/Engine/PlayerbotAIBase.h @@ -8,6 +8,7 @@ #include "Define.h" #include "PlayerbotAIConfig.h" +#include "Player.h" class PlayerbotAIBase { @@ -17,7 +18,7 @@ class PlayerbotAIBase bool CanUpdateAI(); void SetNextCheckDelay(uint32 const delay); void IncreaseNextCheckDelay(uint32 delay); - void YieldThread(uint32 delay = sPlayerbotAIConfig.reactDelay); + void YieldThread(Player* bot, uint32 delay = sPlayerbotAIConfig.reactDelay); virtual void UpdateAI(uint32 elapsed, bool minimal = false); virtual void UpdateAIInternal(uint32 elapsed, bool minimal = false) = 0; bool IsActive(); diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 446ca8c40f1..5ea8b3323f8 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -279,7 +279,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -288,7 +288,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (goSpellTarget && !goSpellTarget->isSpawned()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -320,7 +320,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) { InterruptSpell(); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } @@ -332,7 +332,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Wait for spell cast - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } } @@ -368,7 +368,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) // Update internal AI UpdateAIInternal(elapsed, minimal); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); } // Helper function for UpdateAI to check group membership and handle removal if necessary @@ -4377,21 +4377,27 @@ Player* PlayerbotAI::GetGroupLeader() return master; } -uint32 PlayerbotAI::GetFixedBotNumer(uint32 maxNum, float cyclePerMin) +uint32 PlayerbotAI::GetFixedBotNumber(uint32 maxNum) { - uint32 randseed = rand32(); // Seed random number - uint32 randnum = bot->GetGUID().GetCounter() + randseed; // Semi-random but fixed number for each bot. + if (maxNum == 0) + return 0; - if (cyclePerMin > 0) - { - uint32 cycle = floor(getMSTime() / (1000)); // Semi-random number adds 1 each second. - cycle = cycle * cyclePerMin / 60; // Cycles cyclePerMin per minute. - randnum += cycle; // Make the random number cylce. - } + // Deterministic pseudo-random hash based on the bot GUID evenly distributed across active slots + uint32 id = bot->GetGUID().GetCounter(); + uint32 h = id; + h ^= h >> 16; + h *= 0x7feb352d; + h ^= h >> 15; + h *= 0x846ca68b; + h ^= h >> 16; - randnum = - (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. - return randnum; // Now we have a number unique for each bot between 0 and maxNum that increases by cyclePerMin. + // Current time slot + uint32 timeSlot = (getMSTime() / 1000) / sPlayerbotAIConfig.BotActiveAloneDurationSeconds; + + // Mix timeSlot into the hash to reshuffle every rotation window + uint32 mixed = h ^ (timeSlot * 0x9e3779b9); // with multiplicative constant + + return mixed % maxNum; } /* @@ -4408,7 +4414,7 @@ enum GrouperType GrouperType PlayerbotAI::GetGrouperType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GrouperType::SOLO; @@ -4430,7 +4436,7 @@ GrouperType PlayerbotAI::GetGrouperType() GuilderType PlayerbotAI::GetGuilderType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GuilderType::SOLO; @@ -4754,44 +4760,40 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) // situations are usable for scaling when enabled. // ####################################################################################### - // Below is code to have a specified % of bots active at all times. - // The default is 100%. With 1% of all bots going active or inactive each minute. + // Base percentage of bots to be active uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; + + // Apply SmartScale if enabled if (sPlayerbotAIConfig.botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) { - mod = AutoScaleActivity(mod); + mod = AutoScaleActivity(mod); // mod reflects on latency throttling } - uint32 ActivityNumber = - GetFixedBotNumer(100, sPlayerbotAIConfig.botActiveAlone * static_cast(mod) / 100 * 0.01f); + // Get deterministic bucket + timeSlot + uint32 ActivityNumber = GetFixedBotNumber(100); - return ActivityNumber <= - (sPlayerbotAIConfig.botActiveAlone * mod) / - 100; // The given percentage of bots should be active and rotate 1% of those active bots each minute. + // Check if this bot is in the active set + return ActivityNumber < mod; // mod is directly the number of bots active (0–100) } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) { const int activityIndex = static_cast(activityType); - // Unknown/out-of-range avoid blocking, added logging for further analysing should not happen in the first place. - if (activityIndex <= 0 || activityIndex >= MAX_ACTIVITY_TYPE) - { - LOG_ERROR("playerbots", "AllowActivity received invalid activity type value: {}", activityIndex); - return true; - } - if (!allowActiveCheckTimer[activityIndex]) - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); + + // 4500ms base + 0–499ms per-bot offset = 4500–4999ms, capping at just under 5 seconds + uint32 offset = bot->GetGUID().GetCounter() % 500; - if (!checkNow && time(nullptr) < (allowActiveCheckTimer[activityIndex] + 5)) + if (!checkNow && getMSTime() < (allowActiveCheckTimer[activityIndex] + 4500 + offset)) return allowActive[activityIndex]; const bool allowed = AllowActive(activityType); allowActive[activityIndex] = allowed; - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); return allowed; } diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 9c417561fec..1829e9175d4 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -541,8 +541,7 @@ class PlayerbotAI : public PlayerbotAIBase // Checks if the bot is summoned as alt of a player bool IsAlt(); Player* GetGroupLeader(); - // Returns a semi-random (cycling) number that is fixed for each bot. - uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); + uint32 GetFixedBotNumber(uint32 maxNum = 100); GrouperType GetGrouperType(); GuilderType GetGuilderType(); bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 6a8d60129e2..f150f7af198 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -595,11 +595,12 @@ bool PlayerbotAIConfig::Initialize() randomBotHordeRatio = sConfigMgr->GetOption("AiPlayerbot.RandomBotHordeRatio", 50); disableDeathKnightLogin = sConfigMgr->GetOption("AiPlayerbot.DisableDeathKnightLogin", 0); limitTalentsExpansion = sConfigMgr->GetOption("AiPlayerbot.LimitTalentsExpansion", 0); - botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 100); + botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 10); + BotActiveAloneDurationSeconds = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneDurationSeconds", 30); BotActiveAloneForceWhenInRadius = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInRadius", 150); BotActiveAloneForceWhenInZone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInZone", 1); BotActiveAloneForceWhenInMap = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInMap", 0); - BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 1); + BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 0); BotActiveAloneForceWhenInGuild = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInGuild", 1); botActiveAloneSmartScale = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScale", 1); botActiveAloneSmartScaleDiffLimitfloor = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor", 50); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 7b9b2fe8112..7b6c1eb6f57 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -334,6 +334,7 @@ class PlayerbotAIConfig bool disableDeathKnightLogin; bool limitTalentsExpansion; uint32 botActiveAlone; + uint32 BotActiveAloneDurationSeconds; uint32 BotActiveAloneForceWhenInRadius; bool BotActiveAloneForceWhenInZone; bool BotActiveAloneForceWhenInMap; From d07ddb14d091111ae01157149c8f2997fc52a304 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:31:17 +0200 Subject: [PATCH 44/87] Paladin use bubble heal strategy (#2244) ## Pull Request Description Added support for bubble heal strategy. Adjusted emergency action order ## How to Test the Changes - invite paladin bot to party - check that bot dont have `healer dps` strategy - start combat (with for example dummy) - use `.damage 20000` where 20000 is near max health of bot - use `.unaura 25771` until bot use Divine Shield - bot should heal himself ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Paladin bot heal himself without `healer dps` while Divine Shield - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers obraz --- .../Class/Paladin/PaladinAiObjectContext.cpp | 2 ++ .../Strategy/GenericPaladinStrategy.cpp | 22 +++++++++++-------- .../Class/Paladin/Trigger/PaladinTriggers.cpp | 5 +++++ .../Class/Paladin/Trigger/PaladinTriggers.h | 8 +++++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 3c92e57c33c..7edbf5c8f49 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -134,6 +134,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext &PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target; creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead; creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor; + creators["divine shield low health"] = &PaladinTriggerFactoryInternal::divine_shield_low_health; creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead; creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield; creators["consecration"] = &PaladinTriggerFactoryInternal::consecration; @@ -156,6 +157,7 @@ class PaladinTriggerFactoryInternal : public NamedObjectContext static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); } static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); } static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); } + static Trigger* divine_shield_low_health(PlayerbotAI* botAI) { return new DivineShieldLowHealthTrigger(botAI); } static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); } static Trigger* righteous_fury(PlayerbotAI* botAI) { return new RighteousFuryTrigger(botAI); } static Trigger* judgement(PlayerbotAI* botAI) { return new JudgementTrigger(botAI); } diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index f03207216ec..9a197c601c6 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -16,17 +16,21 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) { CombatStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("critical health", { NextAction("divine shield", - ACTION_HIGH + 5) })); - triggers.push_back( - new TriggerNode("hammer of justice interrupt", - { NextAction("hammer of justice", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "hammer of justice on enemy healer", + triggers.push_back(new TriggerNode("hammer of justice interrupt", + { NextAction("hammer of justice", ACTION_INTERRUPT) })); + triggers.push_back(new TriggerNode("hammer of justice on enemy healer", { NextAction("hammer of justice on enemy healer", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "hammer of justice on snare target", + triggers.push_back(new TriggerNode("hammer of justice on snare target", { NextAction("hammer of justice on snare target", ACTION_INTERRUPT) })); + triggers.push_back(new TriggerNode("critical health", { NextAction("divine shield", ACTION_EMERGENCY) })); + triggers.push_back(new TriggerNode("critical health", { NextAction("lay on hands", ACTION_EMERGENCY + 1) })); + triggers.push_back(new TriggerNode("party member critical health", + { NextAction("lay on hands on party", ACTION_EMERGENCY + 2) })); + triggers.push_back(new TriggerNode("divine shield low health", + { NextAction("flash of light", ACTION_EMERGENCY + 3), NextAction("holy light", ACTION_EMERGENCY + 2)})); + triggers.push_back(new TriggerNode("protect party member", + { NextAction("blessing of protection on party", ACTION_EMERGENCY + 3) })); + triggers.push_back(new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); triggers.push_back(new TriggerNode( "critical health", { NextAction("lay on hands", ACTION_EMERGENCY) })); triggers.push_back( diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index e3367aaef3b..46a2d8a9427 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -32,6 +32,11 @@ bool BlessingTrigger::IsActive() "blessing of kings", "blessing of sanctuary", nullptr); } +bool DivineShieldLowHealthTrigger::IsActive() +{ + return botAI->HasAura("divine shield", bot) && AI_VALUE2(uint8, "health", "self target") < 80; +} + Unit* HandOfFreedomOnPartyTrigger::GetTarget() { bool const selfImpaired = botAI->IsMovementImpaired(bot); diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index cc6ceddcf4c..d11c8024fca 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -185,6 +185,14 @@ class DivineFavorTrigger : public BuffTrigger DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} }; +class DivineShieldLowHealthTrigger : public Trigger +{ +public: + DivineShieldLowHealthTrigger(PlayerbotAI* botAI) : Trigger(botAI, "divine shield low health") {} + + bool IsActive() override; +}; + class NotSensingUndeadTrigger : public BuffTrigger { public: From 30c142aacab881335e1e717996a6a5ef2995fe82 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:31:42 +0200 Subject: [PATCH 45/87] Challenging Roar support (#2238) ## Pull Request Description Added Challenging Roar support to druid taunt spell Related with: #2002 ## How to Test the Changes - run dungeon with druid tank and monitor spell usage ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Druid also use Challenging Roar as taunt spell - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Class/Druid/Action/DruidBearActions.h | 6 ++++++ src/Ai/Class/Druid/DruidAiObjectContext.cpp | 2 ++ src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp | 1 + 3 files changed, 9 insertions(+) diff --git a/src/Ai/Class/Druid/Action/DruidBearActions.h b/src/Ai/Class/Druid/Action/DruidBearActions.h index caf369be339..d5354b7e6c5 100644 --- a/src/Ai/Class/Druid/Action/DruidBearActions.h +++ b/src/Ai/Class/Druid/Action/DruidBearActions.h @@ -23,6 +23,12 @@ class CastGrowlAction : public CastSpellAction CastGrowlAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "growl") {} }; +class CastChallengingRoarAction : public CastMeleeDebuffSpellAction +{ +public: + CastChallengingRoarAction(PlayerbotAI* botAI) : CastMeleeDebuffSpellAction(botAI, "challenging roar") {} +}; + class CastMaulAction : public CastMeleeSpellAction { public: diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 3a638eedc31..17b561d19f4 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -183,6 +183,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext creators["bash"] = &DruidAiObjectContextInternal::bash; creators["swipe"] = &DruidAiObjectContextInternal::swipe; creators["growl"] = &DruidAiObjectContextInternal::growl; + creators["challenging roar"] = &DruidAiObjectContextInternal::challenging_roar; creators["demoralizing roar"] = &DruidAiObjectContextInternal::demoralizing_roar; creators["hibernate"] = &DruidAiObjectContextInternal::hibernate; creators["entangling roots"] = &DruidAiObjectContextInternal::entangling_roots; @@ -277,6 +278,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } static Action* swipe(PlayerbotAI* botAI) { return new CastSwipeAction(botAI); } static Action* growl(PlayerbotAI* botAI) { return new CastGrowlAction(botAI); } + static Action* challenging_roar(PlayerbotAI* botAI) { return new CastChallengingRoarAction(botAI); } static Action* demoralizing_roar(PlayerbotAI* botAI) { return new CastDemoralizingRoarAction(botAI); } static Action* moonkin_form(PlayerbotAI* botAI) { return new CastMoonkinFormAction(botAI); } static Action* hibernate(PlayerbotAI* botAI) { return new CastHibernateAction(botAI); } diff --git a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp index 4a60fbd3eac..13af635c3d3 100644 --- a/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp @@ -212,6 +212,7 @@ void BearTankDruidStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back(new TriggerNode("high aoe", {NextAction("challenging roar", ACTION_HIGH + 8)})); triggers.push_back( new TriggerNode( "lose aggro", From a87999bef55c213ea093af842ff1213a2eefe9f8 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 07:32:32 +0200 Subject: [PATCH 46/87] Berserker Rage support (#2261) ## Pull Request Description Added Berserker Rage usage (in combat and outside combat) for Warrior (all specs) Related with: #1755 ## How to Test the Changes - invite warrior to party - [optional] start combat (for example with dummy) - use command `.aura 6215` - bot should cast Berserker Rage ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Warriors use Berserker Rage when they got fear, sleep or sap - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Copilot CLI to review ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers obraz [Berserker Rage performance test.txt](https://github.com/user-attachments/files/26333365/Berserker.Rage.performance.test.txt) --- src/Ai/Base/Trigger/GenericTriggers.cpp | 7 +++++ src/Ai/Base/Trigger/GenericTriggers.h | 8 ++++++ src/Ai/Base/TriggerContext.h | 2 ++ .../Class/Warrior/Action/WarriorActions.cpp | 27 +++++++++++++++++++ src/Ai/Class/Warrior/Action/WarriorActions.h | 10 ++++++- .../GenericWarriorNonCombatStrategy.cpp | 24 +++++++++++++++++ .../GenericWarriorNonCombatStrategy.h | 2 +- .../Strategy/GenericWarriorStrategy.cpp | 21 ++++++++++++++- 8 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 68f5a523909..27dd5999cbf 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -481,6 +481,13 @@ bool FearCharmSleepTrigger::IsActive() bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP); } +bool FearSleepSapTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) || + bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED); +} + bool HasAuraStackTrigger::IsActive() { Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index f78728cd5e8..7a44112f36f 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -762,6 +762,14 @@ class FearCharmSleepTrigger : public Trigger bool IsActive() override; }; +class FearSleepSapTrigger : public Trigger +{ +public: + FearSleepSapTrigger(PlayerbotAI* botAI) : Trigger(botAI, "fear sleep sap", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index 63f9be4049f..c77df3a31ba 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -62,6 +62,7 @@ class TriggerContext : public NamedObjectContext creators["generic boost"] = &TriggerContext::generic_boost; creators["loss of control"] = &TriggerContext::loss_of_control; creators["fear charm sleep"] = &TriggerContext::fear_charm_sleep; + creators["fear sleep sap"] = &TriggerContext::fear_sleep_sap; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -369,6 +370,7 @@ class TriggerContext : public NamedObjectContext static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); } static Trigger* fear_charm_sleep(PlayerbotAI* botAI) { return new FearCharmSleepTrigger(botAI); } + static Trigger* fear_sleep_sap(PlayerbotAI* botAI) { return new FearSleepSapTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI); diff --git a/src/Ai/Class/Warrior/Action/WarriorActions.cpp b/src/Ai/Class/Warrior/Action/WarriorActions.cpp index 9f15cf76731..20a42c21997 100644 --- a/src/Ai/Class/Warrior/Action/WarriorActions.cpp +++ b/src/Ai/Class/Warrior/Action/WarriorActions.cpp @@ -7,6 +7,33 @@ #include "Playerbots.h" +bool CastBerserkerRageAction::isPossible() +{ + if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true)) + return false; + + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + if (!spellId) + return false; + + if (!bot->HasSpell(spellId)) + return false; + + if (bot->HasSpellCooldown(spellId)) + return false; + + return true; +} + +bool CastBerserkerRageAction::isUseful() +{ + return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) || + bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED)) + && !botAI->HasAura("berserker rage", bot) + && CastSpellAction::isUseful(); +} + bool CastSunderArmorAction::isUseful() { Aura* aura = botAI->GetAura("sunder armor", GetTarget(), false, true); diff --git a/src/Ai/Class/Warrior/Action/WarriorActions.h b/src/Ai/Class/Warrior/Action/WarriorActions.h index 7910fc0d8ed..da004fc70a9 100644 --- a/src/Ai/Class/Warrior/Action/WarriorActions.h +++ b/src/Ai/Class/Warrior/Action/WarriorActions.h @@ -78,7 +78,15 @@ REACH_ACTION(CastInterceptAction, "intercept", 8.0f); ENEMY_HEALER_ACTION(CastInterceptOnEnemyHealerAction, "intercept"); SNARE_ACTION(CastInterceptOnSnareTargetAction, "intercept"); MELEE_ACTION(CastSlamAction, "slam"); -BUFF_ACTION(CastBerserkerRageAction, "berserker rage"); +class CastBerserkerRageAction : public CastSpellAction +{ +public: + CastBerserkerRageAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "berserker rage") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; + bool isUseful() override; +}; MELEE_ACTION(CastWhirlwindAction, "whirlwind"); MELEE_ACTION(CastPummelAction, "pummel"); ENEMY_HEALER_ACTION(CastPummelOnEnemyHealerAction, "pummel"); diff --git a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp index 091aed2b84c..05a6c2c63ca 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp @@ -7,9 +7,33 @@ #include "Playerbots.h" +class GenericWarriorNonCombatStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + GenericWarriorNonCombatStrategyActionNodeFactory() { creators["berserker rage"] = &berserker_rage; } + +private: + static ActionNode* berserker_rage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "berserker rage", + /*P*/ { NextAction("berserker stance") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + +GenericWarriorNonCombatStrategy::GenericWarriorNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) +{ + actionNodeFactories.Add(new GenericWarriorNonCombatStrategyActionNodeFactory()); +} + void GenericWarriorNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + triggers.push_back(new TriggerNode( + "fear sleep sap", { NextAction("berserker rage", ACTION_EMERGENCY + 1) })); } diff --git a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h index 27643258356..160d8df8fcc 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h @@ -13,7 +13,7 @@ class PlayerbotAI; class GenericWarriorNonCombatStrategy : public NonCombatStrategy { public: - GenericWarriorNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + GenericWarriorNonCombatStrategy(PlayerbotAI* botAI); std::string const getName() override { return "nc"; } void InitTriggers(std::vector& triggers) override; diff --git a/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp b/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp index 178984de9cb..18781ef31f8 100644 --- a/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp @@ -7,9 +7,26 @@ #include "Playerbots.h" -GenericWarriorStrategy::GenericWarriorStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) +class GenericWarriorStrategyActionNodeFactory : public NamedObjectFactory { +public: + GenericWarriorStrategyActionNodeFactory() { creators["berserker rage"] = &berserker_rage; } + +private: + static ActionNode* berserker_rage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "berserker rage", + /*P*/ { NextAction("berserker stance") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; +GenericWarriorStrategy::GenericWarriorStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) +{ + actionNodeFactories.Add(new GenericWarriorStrategyActionNodeFactory()); } void GenericWarriorStrategy::InitTriggers(std::vector& triggers) @@ -17,6 +34,8 @@ void GenericWarriorStrategy::InitTriggers(std::vector& triggers) CombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode( "enemy out of melee", { NextAction("reach melee", ACTION_HIGH + 1) })); + triggers.push_back(new TriggerNode( + "fear sleep sap", { NextAction("berserker rage", ACTION_EMERGENCY + 1) })); } class WarrirorAoeStrategyActionNodeFactory : public NamedObjectFactory From f5363c94655e293d530e0e05ba28a73bdcb2729e Mon Sep 17 00:00:00 2001 From: Crow Date: Sat, 4 Apr 2026 05:59:45 -0500 Subject: [PATCH 47/87] Fix Warrior Battle Shout Spam (#2259) ## Pull Request Description There is a known problem that Warriors will repeatedly spam Battle Shout (BS) if there is a stronger Blessing of Might (BoM) present that they cannot override. It has been long known that the issue arises from the lack of a BS trigger. It was not a problem in the past because BS and BoM used to (erroneously) stack, and that was eliminated when AC ported TC's stacking rules. This PR adds a trigger for BS that prevents the bot from attempting to cast it if a stronger BoM (or Greater BoM) is present. To do this, it compares the AP bonus of any BoM/GBoM applied to the Warrior bot, and then will attempt BS only if the Warrior has a stronger one. I also did some minor reformatting of the other Warrior triggers, mostly just deleting comments and extra braces. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. The trigger is fairly complicated because it uses an AP comparison and takes into account spell ranks and the talent Commanding Presence (which increases the strength of BS). The bug could be solved by a very simple aura check for BoM/GBoM. That's suboptimal though because BS will be stronger if the Paladin has a lower rank of BoM or the Warrior has Commanding Presence and the Paladin does not have Improved BoM. At the end of the day, this is a new trigger that runs every tick, but it's not really more expensive than generic BuffTriggers, which all do this. And it is needed to solve a very annoying bug that essentially renders Warriors useless in any optimized raid unless BS is disabled. I don't believe this more robust trigger (as opposed to just calling BuffTrigger and additionally checking for any BoM/GBoM aura) has any further impact on performance--any added logic is practically free AFAIK (e.g., checking if the bot has a particular talent is just an integer lookup). ## How to Test the Changes Form a group with a Warrior and Paladin bot, with the Warrior bot having a weaker BS than the Paladin's BoM (this is naturally the case at max level, unless the Warrior has Commanding Presence and the Paladin does not have Improved BoM). Have the Paladin cast BoM on the Warrior if it has not done so automatically. Enter combat and see if the Warrior uses BS. Try again with the Warrior having a stronger BS. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [x] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) As noted above, this is a new BuffTrigger that runs every tick. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Default behavior was broken. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) This is, with respect to the code, a pretty complex trigger. But that should not impact other logic, and if need be, there is a very simple version that could be implemented that would at least fix the bug, as noted above. ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) I used Claude Sonnet 4.6. This is essentially the first PR that I entirely vibe coded. I knew what the problem was and how to fix it, including the conditions I wanted in the trigger, and the LLM did the rest. I can tell you what each block does and why that's needed, but I can't tell you how they all work technically. I tested this ingame in scenarios where the available BS was stronger and not as strong, and in each case, the correct behavior was observed. I've since run several raids without BS disabled on my Warriors, and they are correctly not using it (as my Ret Paladin is applying a stronger BoM). The trigger does not show up as an expensive one with pmon (unlike, say, VIgilance, which is probably the most expensive class trigger in the mod (excluding any DK triggers just because I've never used one so I have no idea)). ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- .../Class/Warrior/Trigger/WarriorTriggers.cpp | 114 ++++++++++++------ .../Class/Warrior/Trigger/WarriorTriggers.h | 9 +- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp index 5aa419c9276..f6e5bd2345b 100644 --- a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp +++ b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp @@ -4,7 +4,6 @@ */ #include "WarriorTriggers.h" - #include "Playerbots.h" bool BloodrageBuffTrigger::IsActive() @@ -16,15 +15,11 @@ bool BloodrageBuffTrigger::IsActive() bool VigilanceTrigger::IsActive() { if (!bot->HasSpell(50720)) - { return false; - } Group* group = bot->GetGroup(); if (!group) - { return false; - } Player* currentVigilanceTarget = nullptr; Player* mainTank = nullptr; @@ -33,37 +28,23 @@ bool VigilanceTrigger::IsActive() Player* highestGearScorePlayer = nullptr; uint32 highestGearScore = 0; - // Iterate once through the group to gather all necessary information for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member || member == bot || !member->IsAlive()) continue; - // Check if member has Vigilance applied by the bot if (!currentVigilanceTarget && botAI->HasAura("vigilance", member, false, true)) - { currentVigilanceTarget = member; - } - // Identify Main Tank if (!mainTank && botAI->IsMainTank(member)) - { mainTank = member; - } - - // Identify Assist Tanks - if (assistTank1 == nullptr && botAI->IsAssistTankOfIndex(member, 0)) - { + else if (!assistTank1 && botAI->IsAssistTankOfIndex(member, 0)) assistTank1 = member; - } - else if (assistTank2 == nullptr && botAI->IsAssistTankOfIndex(member, 1)) - { + else if (!assistTank2 && botAI->IsAssistTankOfIndex(member, 1)) assistTank2 = member; - } - // Determine Highest Gear Score - uint32 gearScore = botAI->GetEquipGearScore(member/*, false, false*/); + uint32 gearScore = botAI->GetEquipGearScore(member); if (gearScore > highestGearScore) { highestGearScore = gearScore; @@ -71,33 +52,20 @@ bool VigilanceTrigger::IsActive() } } - // Determine the highest-priority target Player* highestPriorityTarget = mainTank ? mainTank : (assistTank1 ? assistTank1 : (assistTank2 ? assistTank2 : highestGearScorePlayer)); - // Trigger if no Vigilance is active or the current target is not the highest-priority target if (!currentVigilanceTarget || currentVigilanceTarget != highestPriorityTarget) - { return true; - } - return false; // No need to reassign Vigilance + return false; } bool ShatteringThrowTrigger::IsActive() { - // Spell cooldown check - if (!bot->HasSpell(64382)) - { + if (!bot->HasSpell(64382) || bot->HasSpellCooldown(64382)) return false; - } - - // Spell cooldown check - if (bot->HasSpellCooldown(64382)) - { - return false; - } GuidVector enemies = AI_VALUE(GuidVector, "possible targets"); @@ -107,7 +75,6 @@ bool ShatteringThrowTrigger::IsActive() if (!enemy || !enemy->IsAlive() || enemy->IsFriendlyTo(bot)) continue; - // Check if the enemy is within 25 yards and has the specific auras if (bot->IsWithinDistInMap(enemy, 25.0f) && (enemy->HasAura(642) || // Divine Shield enemy->HasAura(45438) || // Ice Block @@ -117,5 +84,74 @@ bool ShatteringThrowTrigger::IsActive() } } - return false; // No valid targets within range + return false; +} + +bool BattleShoutTrigger::IsActive() +{ + if (!BuffTrigger::IsActive()) + return false; + + uint32 battleShoutSpellId = AI_VALUE2(uint32, "spell id", "battle shout"); + if (!battleShoutSpellId) + return false; + + SpellInfo const* bsInfo = sSpellMgr->GetSpellInfo(battleShoutSpellId); + if (!bsInfo) + return false; + + int32 bsApValue = 0; + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (bsInfo->Effects[eff].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + bsApValue = bsInfo->Effects[eff].BasePoints + 1; + break; + } + } + if (!bsApValue) + return false; + + static const uint32 commandingPresenceSpells[] = { + 12318, 12857, 12858, 12860, 12861 }; + static const float commandingPresenceBonus[] = { + 0.05f, 0.10f, 0.15f, 0.20f, 0.25f }; + + float cpBonus = 0.0f; + for (int rank = 4; rank >= 0; --rank) + { + if (bot->HasAura(commandingPresenceSpells[rank])) + { + cpBonus = commandingPresenceBonus[rank]; + break; + } + } + int32 effectiveBsAp = int32(bsApValue * (1.0f + cpBonus)); + + static const char* blessingNames[] = { + "blessing of might", "greater blessing of might", nullptr + }; + for (int i = 0; blessingNames[i] != nullptr; ++i) + { + Aura* bom = botAI->GetAura(blessingNames[i], bot); + if (!bom) + continue; + + SpellInfo const* bomInfo = bom->GetSpellInfo(); + if (!bomInfo) + continue; + + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (bomInfo->Effects[eff].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + int32 bomApValue = bomInfo->Effects[eff].BasePoints + 1; + if (bomApValue >= effectiveBsAp) + return false; + break; + } + } + } + + return true; } diff --git a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h index 563d7076987..8a9ed924837 100644 --- a/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h +++ b/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h @@ -9,7 +9,13 @@ #include "GenericTriggers.h" #include "PlayerbotAI.h" -BUFF_TRIGGER(BattleShoutTrigger, "battle shout"); +class BattleShoutTrigger : public BuffTrigger +{ +public: + BattleShoutTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "battle shout") {} + bool IsActive() override; +}; + BUFF_TRIGGER(BattleStanceTrigger, "battle stance"); BUFF_TRIGGER(DefensiveStanceTrigger, "defensive stance"); BUFF_TRIGGER(BerserkerStanceTrigger, "berserker stance"); @@ -85,4 +91,5 @@ class ShatteringThrowTrigger : public Trigger // public: // SlamTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "slam!") {} // }; + #endif From ca54cff6f522bd70eba5078f3ea804abf4309e94 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 4 Apr 2026 04:00:01 -0700 Subject: [PATCH 48/87] Bug fix. Edge case where bots would get stuck in cities. (#2269) ## Pull Request Description When I refactored flight destinations, I wanted to make where bots go more intentional. so I made it dependent on the allianceHubsPerLevelCache and hodeHubsPerLevelCache. This system relied on there being an innkeeper in each area that the bots would fly to. However, not every zone has an innkeeper, and so there was an odd situation where bots had nowhere to fly to. (Most notably at level 53.) This solves that by hardcoding the flightmasters in those areas into the cache. I also put back in the city teleport probability check which was forcing every bot to teleport to a city on level up. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Debugging and comments. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Bot/RandomPlayerbotMgr.cpp | 13 ++++++++----- src/Mgr/Travel/TravelMgr.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 3fea369a52b..a772136e771 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -1768,13 +1768,16 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->InBattleground()) return; - std::vector locs = sTravelMgr.GetCityLocations(bot); - if (!locs.empty()) + if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { - RandomTeleport(bot, locs, true); - return; + std::vector locs = sTravelMgr.GetCityLocations(bot); + if (!locs.empty()) + { + RandomTeleport(bot, locs, true); + return; + } } - locs = sTravelMgr.GetTeleportLocations(bot); + std::vector locs = sTravelMgr.GetTeleportLocations(bot); if (!locs.empty()) { RandomTeleport(bot, locs, false); diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 7a5ac4f2e47..1868bc2e386 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -4419,6 +4419,7 @@ std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot->GetTeamId()); if (!fromNode) return validDestinations; + std::vector candidateLocations; if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) candidateLocations = GetCityLocations(bot); @@ -4673,6 +4674,31 @@ void TravelMgr::PrepareDestinationCache() if (forAlliance) allianceFlightMasterCache[guid] = pos; flightMastersCount++; + + // Zones that have flight masters but no innkeepers — use flight master as hub + static const std::set zonesWithoutInnkeeper = { + 4, // Blasted Lands (52-57) + 16, // Azshara (45-52) + 28, // Western Plaguelands (50-60) + 46, // Burning Steppes (51-60) + 51, // Searing Gorge (45-51) + 361, // Felwood (47-57) + 490, // Un'Goro Crater (49-56) + 2817, // Crystalsong Forest (77-80) + 4197 // Wintergrasp (79-80) + }; + if (zonesWithoutInnkeeper.count(areaId)) + { + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (int i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + } + } } else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) { From 4ba05962c9c85400020bf8ab74f9cf23feb5da10 Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:00:26 -0400 Subject: [PATCH 49/87] Add fireball fallback for frostfire bolt for frost mage (#2271) ## Pull Request Description Low level frost mages who do not yet have Frostfire Bolt never use Brain Freeze procs, since it is trying to use Frostfire Bolt. This adds Fireball as the fallback for Frostfire Bolt, so frost mages without frostfire bolt will still use Fireball when they get Brain Freeze ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes Get a frost mage without Frostfire Bolt who has the Brain Freeze talent. A level 60 frost mage works for example. Have them attack something until Brain Freeze procs. They should now use Fireball instead of ignoring it. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp b/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp index 34ed81dba1f..fe703f354d2 100644 --- a/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp +++ b/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp @@ -35,7 +35,7 @@ class FrostMageStrategyActionNodeFactory : public NamedObjectFactory static ActionNode* ice_lance(PlayerbotAI*) { return new ActionNode("ice lance", {}, {}, {}); } static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); } static ActionNode* fireball(PlayerbotAI*) { return new ActionNode("fireball", {}, {}, {}); } - static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", {}, {}, {}); } + static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", {}, { NextAction("fireball") }, {}); } }; // ===== Single Target Strategy ===== From 4c9b0adb727e6e03288a340ed924cd3dd7d0e9ba Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 13:00:51 +0200 Subject: [PATCH 50/87] Fire mage cc (#2281) ## Pull Request Description Added support for fire mage cc spells like: Dragon's Breath (disorient) and Blast Wave (knockback) ## How to Test the Changes 1. Invite fire mage to party 2. Add strategy `nc +duel` 3. Start duel and go near bot 4. Bot should use frost nova/dragon's breath/blast wave depends of cooldowns ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Mages cc by default - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) OpenCode, for research differences between CcStrategy implementation between cmangos and ac playerbots ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp | 8 ++++++++ src/Bot/Factory/AiFactory.cpp | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp b/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp index 75a63efa473..0e26c692d95 100644 --- a/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp +++ b/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp @@ -237,6 +237,14 @@ void MageBoostStrategy::InitTriggers(std::vector& triggers) void MageCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("polymorph", { NextAction("polymorph", 30.0f) })); + + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + if (tab == MAGE_TAB_FIRE) + { + triggers.push_back(new TriggerNode("enemy too close for spell", {NextAction("dragon's breath", ACTION_INTERRUPT + 1)})); + triggers.push_back(new TriggerNode("enemy is close", {NextAction("blast wave", ACTION_INTERRUPT)})); + } } void MageAoeStrategy::InitTriggers(std::vector& triggers) diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index 6121789a940..a821886f97e 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -311,7 +311,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa else engine->addStrategiesNoInit("frost", nullptr); - engine->addStrategiesNoInit("dps", "dps assist", "cure", "aoe", nullptr); + engine->addStrategiesNoInit("dps", "dps assist", "cure", "cc", "aoe", nullptr); break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) From 7b04c569562c29f82662fe2b7a56210607946d0a Mon Sep 17 00:00:00 2001 From: Benjamin Jackson <38561765+heyitsbench@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:22:27 -0400 Subject: [PATCH 51/87] Add default case to mount initialization for bots. (#2276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description This PR adds a default case for the mount initialization function for player bots, allowing custom race additions to not crash when added to an AzerothCore server (such as [`mod-worgoblin`](https://github.com/heyitsbench/mod-worgoblin)). ## Feature Evaluation This feature ideally does not get touched in standard `mod-playerbots` usage, as it only adds a case to a switch statement, and would not be getting taken with Blizzlike data (which the vast majority of users likely use). ## How to Test the Changes 1. Add custom races to your AzerothCore instance (most easily accomplished with the above linked module). 2. Start the server and configure it to create random player bots (that fall under the added custom races). 3. Observe no crash. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [X] No, not at all - Does this change modify default bot behavior? - - [X] No - Does this change add new decision branches or increase maintenance complexity? - - [X] Yes (**explain below**) This does add another path in a switch statement. It also creates another set of data for that case. This could have been avoided by consolidating the human/orc cases into the default case, but I elected not to do that in case players make their own changes. If preferred, I can definitely consolidate the cases to not have those redundant data points. ## Messages to Translate - Does this change add bot messages to translate? - - [X] No ## AI Assistance - Was AI assistance used while working on this change? - - [X] No ## Final Checklist - - [X] Stability is not compromised. - - [X] Performance impact is understood, tested, and acceptable. - - [X] Added logic complexity is justified and explained. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Pretty please? 🥹 --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> --- src/Bot/Factory/PlayerbotFactory.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index b66623c5825..11f301feb4f 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -3032,6 +3032,17 @@ void PlayerbotFactory::InitMounts() slow = {33660, 35020, 35022, 35018}; fast = {35025, 35025, 35027}; break; + default: + if (bot->GetTeamId() == TEAM_HORDE) + { // Orc mounts + slow = {470, 6648, 458, 472}; + fast = {23228, 23227, 23229}; + } + else // Human mounts + { + slow = {6654, 6653, 580}; + fast = {23250, 23252, 23251}; + } } switch (bot->GetTeamId()) From a4a3a3d964aa87605c534336ef847da797000279 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 4 Apr 2026 22:10:14 +0200 Subject: [PATCH 52/87] Fix for github action translation labeling (#2285) Maintenance PR --- .github/workflows/label_translation-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/label_translation-pr.yml b/.github/workflows/label_translation-pr.yml index 5dbe92fc5c6..cadb0a46e7e 100644 --- a/.github/workflows/label_translation-pr.yml +++ b/.github/workflows/label_translation-pr.yml @@ -35,4 +35,5 @@ jobs: GH_TOKEN: ${{ github.token }} run: | gh pr edit ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ --add-label "Added translation" From 9ebccc23a2df0e0a56932c7b74f3d0f81b67ccbb Mon Sep 17 00:00:00 2001 From: bash Date: Sun, 5 Apr 2026 11:25:53 +0200 Subject: [PATCH 53/87] clearified the ac --- conf/playerbots.conf.dist | 67 +++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index ff1b393b58b..ccb392e1aef 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -865,28 +865,43 @@ AiPlayerbot.ExcludedHunterPetFamilies = "" # # #################################################################################################### - #################################################################################################### # ACTIVITY # -# BotActiveAlone (%) -# - Determines the percentage of bots that remain active when no real players are nearby. -# - Default is 40% (which is practise kinda translates into 45-50%). -# - If `botActiveAloneSmartScale` is enabled, automatically temporarily down scale activity based on latency. -# - 40% will be activated in a random rotation for the amount of seconds specified in . -# - There are multiple conditions when bots will forced to be active e.g. when in BG/instance/attacked, some are configurable below. -#- When 100$ all bots will be active without any rotation or logic applied but comes with performance hit. +# BotActiveAlone +# - Controls how many bots are active when no real players are nearby. +# - Think of it as a rough percentage: 10 means approximately 10% of bots will be active. +# Not exact — the actual number may vary slightly per rotation cycle. +# - The active bots rotate: every a different set of bots takes a turn. +# - The real number of active bots will always be higher than this value, because bots in +# combat, dungeons, battlegrounds, LFG queue, groups with real players, etc. are always +# forced active on top of this (see force rules below). +# - Set to 100 (with SmartScale off) = all bots always active. Maximum server load. +# - Set to 0 = only bots that match a force rule below will be active. +# +# BotActiveAloneDurationSeconds +# - How often the active roster rotates (in seconds). A different group of bots wakes up +# and the previous group may go idle. +# - This is a minimum, not exact. If a bot is in combat or meets any force rule when the +# rotation happens, it stays active until those conditions end — it won't be cut off +# mid-fight just because its turn expired. # AiPlayerbot.BotActiveAlone = 10 AiPlayerbot.BotActiveAloneDurationSeconds = 30 -# Some additional rules that enforces the bot to be active # -# - bot is within this distance from a real player. -# - bot is in the same zone as a real player. -# - bot is in the same continent as a real player. -# - bot is a real player's friend. -# - bot is in a real player's guild. +# Force-active rules (1 = on, 0 = off) +# These override the percentage above. If any of these conditions is true, the bot stays active. +# +# InRadius - A real player is within this many yards (set to 0 to disable). +# InZone - A real player is in the same zone (e.g. Elwynn Forest). +# InMap - A real player is on the same continent (e.g. Eastern Kingdoms). +# IsFriend - A real player has this bot on their friends list. +# InGuild - This bot is in a guild that has a real player in it. +# +# Bots are also always forced active (not configurable) when: +# in combat, inside a dungeon/raid/BG, in a BG or LFG queue, +# grouped with a real player, or controlled by a real player. # AiPlayerbot.BotActiveAloneForceWhenInRadius = 150 AiPlayerbot.BotActiveAloneForceWhenInZone = 1 @@ -894,15 +909,21 @@ AiPlayerbot.BotActiveAloneForceWhenInMap = 0 AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0 AiPlayerbot.BotActiveAloneForceWhenInGuild = 1 -# SmartScale (automatic scaling of percentage of active bots based on latency) -# The default is 1. When enabled (smart) scales the 'BotActiveAlone' value. -# (The scaling will be overruled by the BotActiveAloneForceWhen...rules) -# -# Limitfloor - when DIFF (latency) is above floor, activity scaling begins -# LimitCeiling - when DIFF (latency) is above ceiling, activity is 0% -# -# MinLevel - only apply scaling when level is above or equal to min(bot)Level -# MaxLevel - only apply scaling when level is lower or equal of max(bot)Level +# SmartScale — automatically reduces active bots when the server is struggling. +# Monitors the server's update time (how long each server tick takes in milliseconds). +# When the server slows down, fewer bots are kept active to reduce load. +# +# Floor (default 50ms) - Below this, no reduction. Server is running fine. +# Ceiling (default 200ms) - At or above this, all non-forced bots are paused. +# Between floor and ceiling, activity scales down gradually. +# Example: BotActiveAlone=10, floor=50, ceiling=200 +# Server at 50ms → ~10% active (no reduction) +# Server at 125ms → ~5% active (half reduction) +# Server at 200ms → 0% active (only forced bots remain) +# +# MinLevel/MaxLevel — only bots within this level range are affected by SmartScale. +# Bots outside the range always use the full BotActiveAlone value. +# Force rules always win over SmartScale. # AiPlayerbot.botActiveAloneSmartScale = 1 AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor = 50 From 4bcf8fd2c479e3a5d23d9afcb19d2279193ae534 Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:35:18 +0200 Subject: [PATCH 54/87] Performance(Core): Some activity sec to ms init fixes, global activity loop check and some additional minor fixes (#2288) ## Pull Request Description 1. Corrected the init activity times; Since ive changed (previous) the calc from seconds to ms to increase more scope for the offset execute jitter (removed timer(), 0 is correct way todo now with MS) (also broke self-bot) 2. Global loop checks in activityAllowed instead vs multiple loops, this function is called very often so we better optimize it. 3 Fixed the broken 'HasManyPlayersNearby' function and then deleted it :O To fragile due various edge cases and rather expensive call, besides lets activeAlone deal with this situation which is way more controlled. 4. Some additional small fixes that where unnoticed overtime. 5. Added/Changed inline comments which makes more sense and explains what it does. 6. Removed dead code freeze bots during init. 7. self-bot fix ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [ ] Minimal impact (**explain below**) - - [x] Moderate impact (**explain below**) When playing with larger amount of real players makes the allowed activity perform better. - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Bot/PlayerbotAI.cpp | 278 ++++++++++++---------------------------- src/Bot/PlayerbotAI.h | 2 - 2 files changed, 83 insertions(+), 197 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 5ea8b3323f8..1a74b8b2f7d 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -119,7 +119,7 @@ PlayerbotAI::PlayerbotAI() for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { - allowActiveCheckTimer[i] = time(nullptr); + allowActiveCheckTimer[i] = 0; allowActive[i] = false; } } @@ -137,19 +137,20 @@ PlayerbotAI::PlayerbotAI(Player* bot) for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { - allowActiveCheckTimer[i] = time(nullptr); + allowActiveCheckTimer[i] = 0; allowActive[i] = false; } accountId = bot->GetSession()->GetAccountId(); - aiObjectContext = AiFactory::createAiObjectContext(bot, this); engines[BOT_STATE_COMBAT] = AiFactory::createCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_NON_COMBAT] = AiFactory::createNonCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_DEAD] = AiFactory::createDeadEngine(bot, this, aiObjectContext); + if (sPlayerbotAIConfig.applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId()); + currentEngine = engines[BOT_STATE_NON_COMBAT]; currentState = BOT_STATE_NON_COMBAT; @@ -445,9 +446,11 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal if (!bot->GetMap()) return; // instances are created and destroyed on demand + // kinda expensive call to make on every single updateAI, do we really need this information? std::string const mapString = WorldPosition(bot).isOverworld() ? std::to_string(bot->GetMapId()) : "I"; PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_TOTAL, "PlayerbotAI::UpdateAIInternal " + mapString); + ExternalEventHelper helper(aiObjectContext); // chat replies @@ -1202,23 +1205,18 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) return; + auto itemIds = GetChatHelper()->ExtractAllItemIds(message); if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && - (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || - GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && + (itemIds.size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && sPlayerbotAIConfig.toxicLinksRepliesChance) { if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance) - { return; - } } - else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && - sPlayerbotAIConfig.thunderfuryRepliesChance)) + else if (itemIds.count(19019) && sPlayerbotAIConfig.thunderfuryRepliesChance) { if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance) - { return; - } } else { @@ -4459,7 +4457,6 @@ GuilderType PlayerbotAI::GetGuilderType() bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) { float sqRange = range * range; - bool nearPlayer = false; for (auto& player : sRandomPlayerbotMgr.GetPlayers()) { if (!player->IsGameMaster() || player->isGMVisible()) @@ -4468,19 +4465,18 @@ bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) continue; if (pos->sqDistance(WorldPosition(player)) < sqRange) - nearPlayer = true; + return true; - // if player is far check farsight/cinematic camera WorldObject* viewObj = player->GetViewpoint(); if (viewObj && viewObj != player) { if (pos->sqDistance(WorldPosition(viewObj)) < sqRange) - nearPlayer = true; + return true; } } } - return nearPlayer; + return false; } bool PlayerbotAI::HasPlayerNearby(float range) @@ -4489,173 +4485,97 @@ bool PlayerbotAI::HasPlayerNearby(float range) return HasPlayerNearby(&botPos, range); }; -bool PlayerbotAI::HasManyPlayersNearby(uint32 trigerrValue, float range) -{ - float sqRange = range * range; - uint32 found = 0; - - for (auto& player : sRandomPlayerbotMgr.GetPlayers()) - { - if ((!player->IsGameMaster() || player->isGMVisible()) && ServerFacade::instance().GetDistance2d(player, bot) < sqRange) - { - found++; - - if (found >= trigerrValue) - return true; - } - } - - return false; -} - -inline bool HasRealPlayers(Map* map) -{ - Map::PlayerList const& players = map->GetPlayers(); - if (players.IsEmpty()) - { - return false; - } - - for (auto const& itr : players) - { - Player* player = itr.GetSource(); - if (!player || !player->IsVisible()) - { - continue; - } - - PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); - if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) - { - return true; - } - } - - return false; -} - -inline bool ZoneHasRealPlayers(Player* bot) -{ - Map* map = bot->GetMap(); - if (!bot || !map) - { - return false; - } - - for (Player* player : sRandomPlayerbotMgr.GetPlayers()) - { - if (player->GetMapId() != bot->GetMapId()) - continue; - - if (player->IsGameMaster() && !player->IsVisible()) - { - continue; - } - - if (player->GetZoneId() == bot->GetZoneId()) - { - PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); - if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) - { - return true; - } - } - } - - return false; -} - bool PlayerbotAI::AllowActive(ActivityType activityType) { - // Early return if bot is in invalid state + // bot is in an invalid state, not safe to process if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) return false; - // when botActiveAlone is 100% and smartScale disabled + // always allow packet handling (e.g. group invites, trade, loot, friend requests etc) + if (activityType == PACKET_ACTIVITY) + return true; + + // all bots forced active, no rotation or scaling needed if (sPlayerbotAIConfig.botActiveAlone >= 100 && !sPlayerbotAIConfig.botActiveAloneSmartScale) - { return true; - } - // Is in combat. Always defend yourself. + // bot is in combat, always defend yourself if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) { if (bot->IsInCombat()) - { return true; - } - } - - // only keep updating till initializing time has completed, - // which prevents unneeded expensive GameTime calls. - if (_isBotInitializing) - { - _isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig.maxRandomBots * 0.11; - - // no activity allowed during bot initialization - if (_isBotInitializing) - { - return false; - } } - // General exceptions - if (activityType == PACKET_ACTIVITY) - { + // bot is inside a BG, dungeon, or raid — always active + if (!WorldPosition(bot).isOverworld()) return true; - } - // bg, raid, dungeon - if (!WorldPosition(bot).isOverworld()) - { + // bot is waiting in a BG queue — stay active to speed up join + if (bot->InBattlegroundQueue()) return true; - } - // bot map has active players. - if (sPlayerbotAIConfig.BotActiveAloneForceWhenInMap) + // bot is in a guild that contains a real player + if (sPlayerbotAIConfig.BotActiveAloneForceWhenInGuild) { - if (HasRealPlayers(bot->GetMap())) - { + if (IsInRealGuild()) // checks cache list return true; - } } - // bot zone has active players. - if (sPlayerbotAIConfig.BotActiveAloneForceWhenInZone) + // a real player is in the same zone (e.g. Elwynn Forest), same continent or within configured yard radius + // combined into a single loop to multiple iterations since this function is called so often + bool checkMap = sPlayerbotAIConfig.BotActiveAloneForceWhenInMap; + bool checkZone = sPlayerbotAIConfig.BotActiveAloneForceWhenInZone; + bool checkRadius = sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius > 0; + if (checkMap || checkZone || checkRadius) { - if (ZoneHasRealPlayers(bot)) + uint32 botMapId = bot->GetMapId(); + uint32 botZoneId = checkZone ? bot->GetZoneId() : 0; + float sqRange = 0.0f; + WorldPosition botPos(bot); + if (checkRadius) { - return true; + float range = static_cast(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius); + sqRange = range * range; } - } - // when in real guild - if (sPlayerbotAIConfig.BotActiveAloneForceWhenInGuild) - { - if (IsInRealGuild()) + for (auto& player : sRandomPlayerbotMgr.GetPlayers()) { - return true; - } - } + if (!player || player->GetMapId() != botMapId) + continue; - // Player is near. Always active. - if (HasPlayerNearby(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius)) - { - return true; + bool isGM = player->IsGameMaster(); + + // map check + if (checkMap && !(isGM && !player->IsVisible())) + return true; + + // zone check + if (checkZone && !(isGM && !player->IsVisible()) && player->GetZoneId() == botZoneId) + return true; + + // radius check + if (checkRadius && (!isGM || player->isGMVisible())) + { + if (botPos.sqDistance(WorldPosition(player)) < sqRange) + return true; + + WorldObject* viewObj = player->GetViewpoint(); + if (viewObj && viewObj != player && botPos.sqDistance(WorldPosition(viewObj)) < sqRange) + return true; + } + } } - // Has player master. Always active. + // bot has a real player master (not another bot) if (GetMaster()) { PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster()); if (!masterBotAI || masterBotAI->IsRealPlayer()) - { return true; - } } - // if grouped up + // bot is grouped with a real player (or a bot owned by one) Group* group = bot->GetGroup(); if (group) { @@ -4666,52 +4586,37 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) continue; if (member == bot) - { continue; - } PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); - { - if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) - { - return true; - } - } + // group member is a real player or owned by one — stay active + if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) + return true; + + // if group leader (bot) is inactive, follow suit if (group->IsLeader(member->GetGUID())) { if (!memberBotAI->AllowActivity(PARTY_ACTIVITY)) - { return false; - } } } } - // In bg queue. Speed up bg queue/join. - if (bot->InBattlegroundQueue()) - { - return true; - } - + // bot is in LFG queue — stay active bool isLFG = false; if (group) { if (sLFGMgr->GetState(group->GetGUID()) != lfg::LFG_STATE_NONE) - { isLFG = true; - } } if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE) - { isLFG = true; - } + if (isLFG) - { return true; - } - // HasFriend + // a real player has this bot on their friends list if (sPlayerbotAIConfig.BotActiveAloneForceWhenIsFriend) { // shouldnt be needed analyse in future @@ -4728,54 +4633,37 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) if (!playerAI || !playerAI->IsRealPlayer()) continue; - // if a real player has the bot as a friend PlayerSocial* social = player->GetSocial(); if (social && social->HasFriend(bot->GetGUID())) return true; } } - // Force the bots to spread - if (activityType == OUT_OF_PARTY_ACTIVITY || activityType == GRIND_ACTIVITY) - { - if (HasManyPlayersNearby(10, 40)) - { - return true; - } - } - - // Bots don't need react to PathGenerator activities + // pathfinding only runs for bots forced active by the rules above — + // skip it for bots that would only be active via random rotation if (activityType == DETAILED_MOVE_ACTIVITY) - { return false; - } - - if (sPlayerbotAIConfig.botActiveAlone <= 0) - { - return false; - } // ####################################################################################### - // All mandatory conditations are checked to be active or not, from here the remaining - // situations are usable for scaling when enabled. + // Acitivity throttling logic // ####################################################################################### + if (sPlayerbotAIConfig.botActiveAlone <= 0) + return false; - // Base percentage of bots to be active + // base threshold capped at 100 uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; - // Apply SmartScale if enabled + // reduce threshold based on server tick time when SmartScale is enabled if (sPlayerbotAIConfig.botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) { - mod = AutoScaleActivity(mod); // mod reflects on latency throttling + mod = AutoScaleActivity(mod); } - // Get deterministic bucket + timeSlot + // deterministic rotation — bot is active if its hash falls below the threshold uint32 ActivityNumber = GetFixedBotNumber(100); - - // Check if this bot is in the active set - return ActivityNumber < mod; // mod is directly the number of bots active (0–100) + return ActivityNumber < mod; } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 1829e9175d4..cfa27ed4e88 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -546,7 +546,6 @@ class PlayerbotAI : public PlayerbotAIBase GuilderType GetGuilderType(); bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance); bool HasPlayerNearby(float range = sPlayerbotAIConfig.reactDistance); - bool HasManyPlayersNearby(uint32 trigerrValue = 20, float range = sPlayerbotAIConfig.sightDistance); bool AllowActive(ActivityType activityType); bool AllowActivity(ActivityType activityType = ALL_ACTIVITY, bool checkNow = false); uint32 AutoScaleActivity(uint32 mod); @@ -614,7 +613,6 @@ class PlayerbotAI : public PlayerbotAIBase Item* FindItemInInventory(std::function checkItem) const; void HandleCommands(); void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL); - bool _isBotInitializing = false; inline bool IsValidUnit(const Unit* unit) const { return unit && unit->IsInWorld() && !unit->IsDuringRemoveFromWorld(); From cd16f6baf198cbd05f74bb6b693078a78e04727f Mon Sep 17 00:00:00 2001 From: bash Date: Fri, 10 Apr 2026 12:47:49 +0200 Subject: [PATCH 55/87] z-axe clamping to prevent clipping throught the map --- src/Ai/Base/Actions/MovementActions.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 4f4a110a305..85855f60dc9 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -101,6 +101,11 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior float x = target->GetPositionX() + cos(angle) * distance; float y = target->GetPositionY() + sin(angle) * distance; float z = target->GetPositionZ(); + // Clamp Z to the terrain under the offset point so we don't + // hand PointMovementGenerator a Z that matches the target's + // floor but not the sampled (x,y) — avoids straight-line + // fallbacks through geometry. + bot->UpdateAllowedPositionZ(x, y, z); if (!bot->IsWithinLOS(x, y, z)) continue; @@ -250,7 +255,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, // bot->CastStop(); // botAI->InterruptSpell(); // } - DoMovePoint(bot, x, y, z, generatePath, backwards); + DoMovePoint(bot, x, y, modifiedZ, generatePath, backwards); float delay = 1000.0f * MoveDelay(distance, backwards); if (lessDelay) { @@ -258,7 +263,8 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, } delay = std::max(.0f, delay); delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay); - AI_VALUE(LastMovement&, "last movement").Set(mapId, x, y, z, bot->GetOrientation(), delay, priority); + AI_VALUE(LastMovement&, "last movement") + .Set(mapId, x, y, modifiedZ, bot->GetOrientation(), delay, priority); return true; } } @@ -778,15 +784,17 @@ bool MovementAction::MoveTo(WorldObject* target, float distance, MovementPriorit float dx = cos(angle) * needToGo + bx; float dy = sin(angle) * needToGo + by; - float dz; // = std::max(bz, tz); // calc accurate z position to avoid stuck + // Start from a seed Z between bot and target, then clamp to the + // terrain under (dx,dy). Linear interpolation alone ignores hills + // between the two units and fed PointMovementGenerator a Z that + // could be well above/below ground, triggering straight-line + // fallbacks through walls. + float dz; if (distanceToTarget > CONTACT_DISTANCE) - { dz = bz + (tz - bz) * (needToGo / distanceToTarget); - } else - { dz = tz; - } + bot->UpdateAllowedPositionZ(dx, dy, dz); return MoveTo(target->GetMapId(), dx, dy, dz, false, false, false, false, priority); } From 03db0c34b2386ea99195c6d93576f5413ba84568 Mon Sep 17 00:00:00 2001 From: bash Date: Fri, 10 Apr 2026 12:49:49 +0200 Subject: [PATCH 56/87] improving RPG traveland minimize wierd path selections but still happen --- .../Base/Actions/MoveToTravelTargetAction.cpp | 17 ++- src/Ai/World/Rpg/Action/NewRpgAction.cpp | 43 +++++++- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 104 +++++++++++++++--- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 9 +- 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp b/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp index f238135d9f3..37cf064d7ec 100644 --- a/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp +++ b/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp @@ -74,8 +74,18 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) float maxDistance = target->getDestination()->getRadiusMin(); - // Evenly distribute around the target. - float angle = 2 * M_PI * urand(0, 100) / 100.0; + // Spread bots around the target but keep the offset stable per + // (bot, destination) pair. Previously the angle and radius were + // re-rolled every time the action re-entered (i.e. every tick the + // bot wasn't already moving), which made bots oscillate between + // two random points around the same quest POI instead of + // committing to one approach. + uint32 botLow = bot->GetGUID().GetCounter(); + int32 destSeed = static_cast(location.GetPositionX()) * 73856093 ^ + static_cast(location.GetPositionY()) * 19349663; + uint32 seed = botLow ^ static_cast(destSeed); + float angle = 2.0f * static_cast(M_PI) * static_cast(seed % 1000) / 1000.0f; + float mod = 0.5f + static_cast((seed / 1000) % 1000) / 2000.0f; // [0.5, 1.0] if (target->getMaxTravelTime() > target->getTimeLeft()) // The bot is late. Speed it up. { @@ -89,9 +99,6 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) float z = location.GetPositionZ(); float mapId = location.GetMapId(); - // Move between 0.5 and 1.0 times the maxDistance. - float mod = frand(50.f, 100.f) / 100.0f; - x += cos(angle) * maxDistance * mod; y += sin(angle) * maxDistance * mod; diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 58846b949aa..ddd1240da22 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -151,7 +151,14 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/) if (SearchQuestGiverAndAcceptOrReward()) return true; if (auto* data = std::get_if(&botAI->rpgInfo.data)) - return MoveFarTo(data->pos); + { + if (MoveFarTo(data->pos)) + return true; + // Small nudge so the next tick's MoveFarTo starts from a + // slightly different position. Kept small so it doesn't look + // like the bot is abandoning its destination. + return MoveRandomNear(10.0f); + } return false; } @@ -162,7 +169,11 @@ bool NewRpgGoCampAction::Execute(Event /*event*/) return true; if (auto* data = std::get_if(&botAI->rpgInfo.data)) - return MoveFarTo(data->pos); + { + if (MoveFarTo(data->pos)) + return true; + return MoveRandomNear(10.0f); + } return false; } @@ -215,7 +226,14 @@ bool NewRpgWanderNpcAction::Execute(Event /*event*/) data.lastReach = 0; } else - return MoveWorldObjectTo(data.npcOrGo); + { + if (MoveWorldObjectTo(data.npcOrGo)) + return true; + // NPC pathing failed (random offset in a wall, mmap hiccup, etc). + // Take a small random step so the next tick retries from a + // different spot instead of staring at the NPC from afar. + return MoveRandomNear(15.0f); + } return true; } @@ -305,7 +323,12 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) { - return MoveFarTo(data.pos); + if (MoveFarTo(data.pos)) + return true; + // Long-range sampler couldn't land a candidate — nudge the + // bot a short distance so the next tick retries from a + // different position instead of sitting idle. + return MoveRandomNear(10.0f); } // Now we are near the quest objective // kill mobs and looting quest should be done automatically by grind strategy @@ -352,7 +375,11 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) return true; } - return MoveRandomNear(20.0f); + // At the POI: keep the bot actively placed but avoid large + // random 20yd hops that look like pacing back and forth. A small + // ~8yd wander reads as the bot looking around while grind/loot + // strategies do their work. + return MoveRandomNear(8.0f); } bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) @@ -392,7 +419,11 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) return false; if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) - return MoveFarTo(data.pos); + { + if (MoveFarTo(data.pos)) + return true; + return MoveRandomNear(10.0f); + } // Now we are near the qoi of reward // the quest should be rewarded by SearchQuestGiverAndAcceptOrReward diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index b5156d6c153..986b2f0f543 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -46,17 +46,51 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) return false; } + // Let previously committed movement finish before recomputing. + // + // MoveTo internally caps its stored delay at maxWaitForMove + // (default 5s), but a long path (200+ yd routed around a + // mountain) takes 30+ seconds to walk. After 5s + // IsWaitingForLastMove returns false and MoveFarTo re-enters. + // Without this gate, DoMovePoint would call mm->Clear() and + // reissue MovePoint from the new bot position — and from a new + // position mmap's partial-path endpoint often differs, so the + // bot gets clobbered mid-walk and ends up oscillating (e.g. + // cave entrance -> inside cave -> cave entrance -> mountain + // base -> cave entrance...) around an unreachable destination. + // + // If the bot is still actively walking toward its last + // committed point on the same map, just let the current spline + // finish. The stuck counter below continues to track real + // progress toward dest and triggers teleport recovery if the + // committed paths genuinely aren't closing the gap. + { + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId()) + { + float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ); + if (remaining > 10.0f) + return true; + } + } + // stuck check float disToDest = bot->GetDistance(dest); - if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis) + // Require a meaningful improvement (5yd) to reset the stuck counter. + // The old 1yd threshold was small enough that bots oscillating back + // and forth around an obstacle would keep "making progress" forever + // and never trigger the teleport recovery below. + if (disToDest + 5.0f < botAI->rpgInfo.nearestMoveFarDis) { botAI->rpgInfo.nearestMoveFarDis = disToDest; botAI->rpgInfo.stuckTs = getMSTime(); botAI->rpgInfo.stuckAttempts = 0; } - else if (++botAI->rpgInfo.stuckAttempts >= 10 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) + else if (++botAI->rpgInfo.stuckAttempts >= 5 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) { - // Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination + // No meaningful progress toward dest for `stuckTime`: fall + // back to teleporting directly so the bot can get on with + // its RPG objective instead of oscillating indefinitely. botAI->rpgInfo.stuckTs = getMSTime(); botAI->rpgInfo.stuckAttempts = 0; const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); @@ -78,26 +112,62 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) false, true); } + const uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; + + // Primary strategy: ask mmap for a route to the TRUE destination. + // If mmap can reach it directly (PATHFIND_NORMAL) or partially + // (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap + // of ~296 yards, or where local geometry blocks the final step), + // walk to the furthest reachable waypoint mmap computed. This + // lets bots follow the real route around obstacles (mountains, + // cave walls, cliffs) instead of trying to cut straight through. + // The spline system walks the whole returned path smoothly, so + // subsequent ticks early-out via IsWaitingForLastMove and no + // further PathGenerator calls fire until the bot arrives. + { + PathGenerator path(bot); + path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); + PathType type = path.GetPathType(); + bool canReach = !(type & (~typeOk)); + if (canReach) + { + const G3D::Vector3& endPos = path.GetActualEndPosition(); + // Only commit if the mmap endpoint actually makes progress + // toward the destination. For pathological INCOMPLETE + // results (e.g. disconnected polys that still report + // INCOMPLETE) the endpoint can land right under the bot; + // fall through to cone sampling in that case. + float endDistToDest = dest.GetExactDist(endPos.x, endPos.y, endPos.z); + if (endDistToDest + 5.0f < disToDest) + { + return MoveTo(bot->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, true); + } + } + } + + // Fallback: mmap couldn't route to the destination. Sample the + // forward cone for a reachable stepping stone so the bot keeps + // moving and can try again from a new vantage point. Cap at 2 + // samples — we already spent one PathGenerator call above and at + // 3000 bots every extra CalculatePath matters. float minDelta = M_PI; const float x = bot->GetPositionX(); const float y = bot->GetPositionY(); const float z = bot->GetPositionZ(); + const float baseAngle = bot->GetAngle(&dest); float rx, ry, rz; bool found = false; - int attempt = 3; - while (attempt--) + for (int attempt = 0; attempt < 2; ++attempt) { - float angle = bot->GetAngle(&dest); - float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2; - angle += delta; - float dis = rand_norm() * pathFinderDis; - float dx = x + cos(angle) * dis; - float dy = y + sin(angle) * dis; + float delta = (rand_norm() - 0.5f) * static_cast(M_PI); // ±π/2, forward cone + float sampleDis = (0.5f + rand_norm() * 0.5f) * pathFinderDis; + float angle = baseAngle + delta; + float dx = x + cos(angle) * sampleDis; + float dy = y + sin(angle) * sampleDis; float dz = z + 0.5f; PathGenerator path(bot); path.CalculatePath(dx, dy, dz); PathType type = path.GetPathType(); - uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; bool canReach = !(type & (~typeOk)); if (canReach && fabs(delta) <= minDelta) @@ -159,14 +229,18 @@ bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority) return false; } - float distance = rand_norm() * moveStep; Map* map = bot->GetMap(); const float x = bot->GetPositionX(); const float y = bot->GetPositionY(); const float z = bot->GetPositionZ(); - int attempts = 1; - while (attempts--) + // Previously: attempts = 1. A single random sample often landed in + // water / blocked geometry / unreachable poly, the function returned + // false, and the caller had no fallback — bot stood still. Retry a + // handful of times with a fresh distance each loop so a bad roll + // doesn't lock the bot in place. + for (int attempt = 0; attempt < 8; ++attempt) { + float distance = (0.4f + rand_norm() * 0.6f) * moveStep; float angle = (float)rand_norm() * 2 * static_cast(M_PI); float dx = x + distance * cos(angle); float dy = y + distance * sin(angle); diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 9cd939eb772..f17891ffc65 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -61,7 +61,14 @@ class NewRpgBaseAction : public MovementAction protected: /* FOR MOVE FAR */ const float pathFinderDis = 70.0f; - const uint32 stuckTime = 5 * 60 * 1000; + // Time without real progress toward dest before MoveFarTo + // falls back to teleport recovery. Kept short enough that a + // bot truly oscillating around an unreachable destination + // (mmap returning non-progressing partial paths, or NOPATH + + // cone fallback wandering) doesn't spin for 5 minutes before + // the teleport fires, but long enough that a genuine long + // walk that is slowly making progress never triggers it. + const uint32 stuckTime = 90 * 1000; }; #endif From 5e2f2823ec487a226588358375568f0231296680 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:17:03 -0700 Subject: [PATCH 57/87] fix(Core/Paladin): Remove duplicate trigger registrations (#2301) ## Pull Request Description The "lay on hands", "lay on hands on party", "blessing of protection on party", and "divine plea" triggers were registered twice with conflicting priorities, causing double-firing. Removes the old block while preserving the new "hand of freedom on party" trigger. @kadeshar Can you confirm this matches your intent? ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [X] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [X] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [X] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [X] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [X] Yes (**explain below**) Claude spotted the error on review and ran ahead with a fix, but I confirmed that it is infact duplicate. Co-Authored-By: Claude Opus 4.6 (1M context) ## Final Checklist - - [X] Stability is not compromised. - - [X] Performance impact is understood, tested, and acceptable. - - [X] Added logic complexity is justified and explained. - - [X] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Co-authored-by: Claude Opus 4.6 (1M context) --- .../Paladin/Strategy/GenericPaladinStrategy.cpp | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index 9a197c601c6..c4edc28fd73 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -31,19 +31,8 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("protect party member", { NextAction("blessing of protection on party", ACTION_EMERGENCY + 3) })); triggers.push_back(new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); - triggers.push_back(new TriggerNode( - "critical health", { NextAction("lay on hands", ACTION_EMERGENCY) })); - triggers.push_back( - new TriggerNode("party member critical health", - { NextAction("lay on hands on party", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode( - "protect party member", - { NextAction("blessing of protection on party", ACTION_EMERGENCY + 2) })); - triggers.push_back(new TriggerNode( - "hand of freedom on party", + triggers.push_back(new TriggerNode("hand of freedom on party", { NextAction("hand of freedom on party", ACTION_HIGH + 4) })); - triggers.push_back( - new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); } void PaladinCureStrategy::InitTriggers(std::vector& triggers) From 51a0d643b6c7004086055dc199a05b2f0301f4bb Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 11 Apr 2026 00:17:13 +0200 Subject: [PATCH 58/87] Crashfix for wait for attack (#2303) ## Pull Request Description Fixed crash related with setting height for new best safe spot. ## How to Test the Changes 1. Create raid group 2. Go to Molten Core 3. Add wait for attack strategy to bot and set time 4. Attack mob 5. If bot/bots will wait set time and server dont crash then is ok ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) To find existing method which safetly get height for specific point. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Actions/WaitForAttackAction.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Ai/Base/Actions/WaitForAttackAction.cpp b/src/Ai/Base/Actions/WaitForAttackAction.cpp index 4fb8918c62f..737e891daec 100644 --- a/src/Ai/Base/Actions/WaitForAttackAction.cpp +++ b/src/Ai/Base/Actions/WaitForAttackAction.cpp @@ -44,7 +44,12 @@ WorldPosition GetBestPoint(AiObjectContext* context, Player* bot, Unit* target, float z = targetPosition.GetPositionZ() + 1.0f; WorldPosition point(targetPosition.GetMapId(), x, y, z); - point.setZ(point.getHeight()); + + float groundZ = bot->GetMapHeight(x, y, z); + if (groundZ == INVALID_HEIGHT || groundZ == VMAP_INVALID_HEIGHT_VALUE) + continue; + + point.setZ(groundZ); // Check line of sight to target if (!target->IsWithinLOS(point.GetPositionX(), point.GetPositionY(), @@ -88,8 +93,11 @@ bool WaitForAttackKeepSafeDistanceAction::Execute(Event /*event*/) { Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) + return false; + // If our target is moving towards a stationary unit, use that unit as anchor - if (target && !target->IsStopped()) + if (!target->IsStopped()) { ObjectGuid targetGuid = target->GetTarget(); if (targetGuid) @@ -100,7 +108,7 @@ bool WaitForAttackKeepSafeDistanceAction::Execute(Event /*event*/) } } - if (target && target->IsAlive()) + if (target->IsAlive()) { float safeDistance = std::max( target->GetCombatReach() + ATTACK_DISTANCE, From 7cc00e6283ff1145aea5423d175bcb6048203e83 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 11 Apr 2026 07:15:47 +0200 Subject: [PATCH 59/87] Focus heal targets strategy migration (#2254) ## Pull Request Description Migration for "focus heal targets" strategy which order healer to focus only on specified targets Commands: `nc +focus heal targets` `co +focus heal targets` `focus heal +botName` `focus heal -botName` `focus heal ?` ## How to Test the Changes - create bots party with at least one healer - apply to healer strategy - order healer focus on single bot - start fight (for example with dummy) - use `.damage 10000` to bots a watch which are healed directly and which one only via aoe healing ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [ ] No - - [x] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | focus_heal_not_healer | I''m not a healer or offhealer (please change my strats to heal or offheal) focus_heal_provide_names | Please provide one or more player names focus_heal_no_targets | I don''t have any focus heal targets focus_heal_current_targets | My focus heal targets are %targets focus_heal_cleared | Removed focus heal targets focus_heal_add_remove_syntax | Please specify a + for add or - to remove a target focus_heal_not_in_group | I''m not in a group focus_heal_not_in_group_with | I''m not in a group with %player_name focus_heal_added | Added %player_name to focus heal targets focus_heal_removed | Removed %player_name from focus heal targets ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) OpenCode, to help migrate strategy ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers From tests: obraz Its optional strategy mostly for raiders then performance is not a issue. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision --- ...03_28_00_ai_playerbot_focus_heal_texts.sql | 240 ++++++++++++++++++ .../Actions/SetFocusHealTargetsAction.cpp | 219 ++++++++++++++++ .../Base/Actions/SetFocusHealTargetsAction.h | 21 ++ src/Ai/Base/ChatActionContext.h | 3 + src/Ai/Base/ChatTriggerContext.h | 2 + .../Strategy/ChatCommandHandlerStrategy.cpp | 2 + src/Ai/Base/Strategy/FocusTargetStrategy.h | 20 ++ src/Ai/Base/StrategyContext.h | 3 + src/Ai/Base/Value/PartyMemberToHeal.cpp | 50 +++- src/Ai/Base/Value/TargetValue.h | 7 + src/Ai/Base/ValueContext.h | 2 + 11 files changed, 559 insertions(+), 10 deletions(-) create mode 100644 data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql create mode 100644 src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp create mode 100644 src/Ai/Base/Actions/SetFocusHealTargetsAction.h create mode 100644 src/Ai/Base/Strategy/FocusTargetStrategy.h diff --git a/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql b/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql new file mode 100644 index 00000000000..3d356a2a2ba --- /dev/null +++ b/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql @@ -0,0 +1,240 @@ +-- ######################################################### +-- Playerbots - Add focus heal command texts +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'focus_heal_not_healer', + 'focus_heal_provide_names', + 'focus_heal_no_targets', + 'focus_heal_current_targets', + 'focus_heal_cleared', + 'focus_heal_add_remove_syntax', + 'focus_heal_not_in_group', + 'focus_heal_not_in_group_with', + 'focus_heal_added', + 'focus_heal_removed' +); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'focus_heal_not_healer', + 'focus_heal_provide_names', + 'focus_heal_no_targets', + 'focus_heal_current_targets', + 'focus_heal_cleared', + 'focus_heal_add_remove_syntax', + 'focus_heal_not_in_group', + 'focus_heal_not_in_group_with', + 'focus_heal_added', + 'focus_heal_removed' +); + +-- focus_heal_not_healer +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 ( + 1745, + 'focus_heal_not_healer', + 'I''m not a healer or offhealer (please change my strats to heal or offheal)', + 0, 0, + '저는 힐러나 오프힐러가 아닙니다 (전략을 heal 또는 offheal로 변경해주세요)', + 'Je ne suis pas un soigneur ou un soigneur secondaire (veuillez changer mes strats en heal ou offheal)', + 'Ich bin kein Heiler oder Nebenheiler (bitte ändere meine Strategien auf heal oder offheal)', + '我不是治疗者或副治疗者(请将我的策略更改为 heal 或 offheal)', + '我不是治療者或副治療者(請將我的策略更改為 heal 或 offheal)', + 'No soy un sanador ni un sanador secundario (por favor cambia mis estrategias a heal o offheal)', + 'No soy un sanador ni un sanador secundario (por favor cambia mis estrategias a heal o offheal)', + 'Я не лекарь и не побочный лекарь (пожалуйста, измените мои стратегии на heal или offheal)'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_healer', 100); + +-- focus_heal_provide_names +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 ( + 1746, + 'focus_heal_provide_names', + 'Please provide one or more player names', + 0, 0, + '하나 이상의 플레이어 이름을 제공해주세요', + 'Veuillez fournir un ou plusieurs noms de joueurs', + 'Bitte geben Sie einen oder mehrere Spielernamen an', + '请提供一个或多个玩家名称', + '請提供一個或多個玩家名稱', + 'Por favor proporciona uno o más nombres de jugadores', + 'Por favor proporciona uno o más nombres de jugadores', + 'Пожалуйста, укажите одно или несколько имён игроков'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_provide_names', 100); + +-- focus_heal_no_targets +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 ( + 1747, + 'focus_heal_no_targets', + 'I don''t have any focus heal targets', + 0, 0, + '지정된 집중 치유 대상이 없습니다', + 'Je n''ai aucune cible de soin prioritaire', + 'Ich habe keine fokussierten Heilziele', + '我没有任何集中治疗目标', + '我沒有任何集中治療目標', + 'No tengo ningún objetivo de sanación prioritario', + 'No tengo ningún objetivo de sanación prioritario', + 'У меня нет целей приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_no_targets', 100); + +-- focus_heal_current_targets: %targets is replaced with comma-separated player names +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 ( + 1748, + 'focus_heal_current_targets', + 'My focus heal targets are %targets', + 0, 0, + '나의 집중 치유 대상: %targets', + 'Mes cibles de soin prioritaire sont %targets', + 'Meine fokussierten Heilziele sind %targets', + '我的集中治疗目标是 %targets', + '我的集中治療目標是 %targets', + 'Mis objetivos de sanación prioritarios son %targets', + 'Mis objetivos de sanación prioritarios son %targets', + 'Мои цели приоритетного лечения: %targets'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_current_targets', 100); + +-- focus_heal_cleared +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 ( + 1749, + 'focus_heal_cleared', + 'Removed focus heal targets', + 0, 0, + '집중 치유 대상을 제거했습니다', + 'Cibles de soin prioritaire supprimées', + 'Fokussierte Heilziele entfernt', + '已移除集中治疗目标', + '已移除集中治療目標', + 'Objetivos de sanación prioritarios eliminados', + 'Objetivos de sanación prioritarios eliminados', + 'Цели приоритетного лечения удалены'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_cleared', 100); + +-- focus_heal_add_remove_syntax +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 ( + 1750, + 'focus_heal_add_remove_syntax', + 'Please specify a + for add or - to remove a target', + 0, 0, + '대상을 추가하려면 +, 제거하려면 -를 지정해주세요', + 'Veuillez spécifier + pour ajouter ou - pour retirer une cible', + 'Bitte geben Sie + zum Hinzufügen oder - zum Entfernen eines Ziels an', + '请指定 + 添加或 - 移除目标', + '請指定 + 添加或 - 移除目標', + 'Por favor especifica + para agregar o - para eliminar un objetivo', + 'Por favor especifica + para agregar o - para eliminar un objetivo', + 'Пожалуйста, укажите + для добавления или - для удаления цели'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_add_remove_syntax', 100); + +-- focus_heal_not_in_group +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 ( + 1751, + 'focus_heal_not_in_group', + 'I''m not in a group', + 0, 0, + '저는 파티에 속해있지 않습니다', + 'Je ne suis pas dans un groupe', + 'Ich bin in keiner Gruppe', + '我不在队伍中', + '我不在隊伍中', + 'No estoy en un grupo', + 'No estoy en un grupo', + 'Я не в группе'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_in_group', 100); + +-- focus_heal_not_in_group_with: %player_name is replaced with the target player's name +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 ( + 1752, + 'focus_heal_not_in_group_with', + 'I''m not in a group with %player_name', + 0, 0, + '%player_name 와(과) 같은 파티에 없습니다', + 'Je ne suis pas dans un groupe avec %player_name', + 'Ich bin nicht in einer Gruppe mit %player_name', + '我与 %player_name 不在同一队伍中', + '我與 %player_name 不在同一隊伍中', + 'No estoy en un grupo con %player_name', + 'No estoy en un grupo con %player_name', + 'Я не в группе с %player_name'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_in_group_with', 100); + +-- focus_heal_added: %player_name is replaced with the added player's name +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 ( + 1753, + 'focus_heal_added', + 'Added %player_name to focus heal targets', + 0, 0, + '%player_name 을(를) 집중 치유 대상에 추가했습니다', + '%player_name ajouté aux cibles de soin prioritaire', + '%player_name zu den fokussierten Heilzielen hinzugefügt', + '已将 %player_name 添加到集中治疗目标', + '已將 %player_name 添加到集中治療目標', + '%player_name agregado a los objetivos de sanación prioritarios', + '%player_name agregado a los objetivos de sanación prioritarios', + '%player_name добавлен в цели приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_added', 100); + +-- focus_heal_removed: %player_name is replaced with the removed player's name +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 ( + 1754, + 'focus_heal_removed', + 'Removed %player_name from focus heal targets', + 0, 0, + '%player_name 을(를) 집중 치유 대상에서 제거했습니다', + '%player_name retiré des cibles de soin prioritaire', + '%player_name aus den fokussierten Heilzielen entfernt', + '已将 %player_name 从集中治疗目标中移除', + '已將 %player_name 從集中治療目標中移除', + '%player_name eliminado de los objetivos de sanación prioritarios', + '%player_name eliminado de los objetivos de sanación prioritarios', + '%player_name удалён из целей приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_removed', 100); diff --git a/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp b/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp new file mode 100644 index 00000000000..d987bdd7385 --- /dev/null +++ b/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp @@ -0,0 +1,219 @@ +/* + * 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 "SetFocusHealTargetsAction.h" + +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "PlayerbotTextMgr.h" + +#include +#include + +static std::string LowercaseString(std::string const& str) +{ + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +static Player* FindGroupPlayerByName(Player* player, std::string const& playerName) +{ + if (!player) + return nullptr; + + Group* group = player->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (member) + { + std::string memberName = member->GetName(); + if (LowercaseString(memberName) == playerName) + return member; + } + } + + return nullptr; +} + +bool SetFocusHealTargetsAction::Execute(Event event) +{ + if (!botAI->IsHeal(bot) && !botAI->HasStrategy("offheal", BOT_STATE_COMBAT)) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_healer", + "I'm not a healer or offhealer (please change my strats to heal or offheal)", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + std::string const param = LowercaseString(event.getParam()); + if (param.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_provide_names", + "Please provide one or more player names", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + std::list focusHealTargets = + AI_VALUE(std::list, "focus heal targets"); + + // Query current focus targets + if (param.find('?') != std::string::npos) + { + if (focusHealTargets.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_no_targets", + "I don't have any focus heal targets", + {}); + botAI->TellMasterNoFacing(text); + } + else + { + std::stringstream targetNames; + for (auto it = focusHealTargets.begin(); it != focusHealTargets.end(); ++it) + { + Unit* target = botAI->GetUnit(*it); + if (target) + { + if (it != focusHealTargets.begin()) + targetNames << ", "; + targetNames << target->GetName(); + } + } + + std::map placeholders; + placeholders["%targets"] = targetNames.str(); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_current_targets", + "My focus heal targets are %targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + + return true; + } + + // Clear all targets + if (param == "none" || param == "unset" || param == "clear") + { + focusHealTargets.clear(); + SET_AI_VALUE(std::list, "focus heal targets", focusHealTargets); + botAI->ChangeStrategy("-focus heal targets", BOT_STATE_COMBAT); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_cleared", + "Removed focus heal targets", + {}); + botAI->TellMasterNoFacing(text); + return true; + } + + // Parse multiple targets separated by commas + std::vector targetNames; + if (param.find(',') != std::string::npos) + { + std::string targetName; + std::stringstream ss(param); + while (std::getline(ss, targetName, ',')) + targetNames.push_back(targetName); + } + else + targetNames.push_back(param); + + if (targetNames.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_provide_names", + "Please provide one or more player names", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + if (!bot->GetGroup()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_in_group", + "I'm not in a group", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + for (std::string const& targetName : targetNames) + { + bool const add = targetName.find("+") != std::string::npos; + bool const remove = targetName.find("-") != std::string::npos; + if (!add && !remove) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_add_remove_syntax", + "Please specify a + for add or - to remove a target", + {}); + botAI->TellMasterNoFacing(text); + continue; + } + + std::string const playerName = targetName.substr(1); + Player* target = FindGroupPlayerByName(bot, playerName); + if (!target) + { + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_in_group_with", + "I'm not in a group with %player_name", + placeholders); + botAI->TellMasterNoFacing(text); + continue; + } + + ObjectGuid const& targetGuid = target->GetGUID(); + if (add) + { + if (std::find(focusHealTargets.begin(), focusHealTargets.end(), targetGuid) == + focusHealTargets.end()) + focusHealTargets.push_back(targetGuid); + + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_added", + "Added %player_name to focus heal targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + else + { + focusHealTargets.remove(targetGuid); + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_removed", + "Removed %player_name from focus heal targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + } + + SET_AI_VALUE(std::list, "focus heal targets", focusHealTargets); + + if (focusHealTargets.empty()) + botAI->ChangeStrategy("-focus heal targets", BOT_STATE_COMBAT); + else + botAI->ChangeStrategy("+focus heal targets", BOT_STATE_COMBAT); + + return true; +} diff --git a/src/Ai/Base/Actions/SetFocusHealTargetsAction.h b/src/Ai/Base/Actions/SetFocusHealTargetsAction.h new file mode 100644 index 00000000000..92d5c0b1983 --- /dev/null +++ b/src/Ai/Base/Actions/SetFocusHealTargetsAction.h @@ -0,0 +1,21 @@ +/* + * 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_SETFOCUSHEALTARGETSACTION_H +#define _PLAYERBOT_SETFOCUSHEALTARGETSACTION_H + +#include "Action.h" + +class PlayerbotAI; + +class SetFocusHealTargetsAction : public Action +{ +public: + SetFocusHealTargetsAction(PlayerbotAI* botAI) : Action(botAI, "focus heal targets") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index c3a6b3d0219..6f11fb33c64 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -37,6 +37,7 @@ #include "LogLevelAction.h" #include "LootStrategyAction.h" #include "LootRollAction.h" +#include "SetFocusHealTargetsAction.h" #include "MailAction.h" #include "NamedObjectContext.h" #include "NewRpgAction.h" @@ -201,6 +202,7 @@ class ChatActionContext : public NamedObjectContext creators["pet attack"] = &ChatActionContext::pet_attack; creators["roll"] = &ChatActionContext::roll_action; creators["wait for attack time"] = &ChatActionContext::wait_for_attack_time; + creators["focus heal targets"] = &ChatActionContext::focus_heal_targets; } private: @@ -314,6 +316,7 @@ class ChatActionContext : public NamedObjectContext static Action* pet_attack(PlayerbotAI* botAI) { return new PetsAction(botAI, "attack"); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } static Action* wait_for_attack_time(PlayerbotAI* botAI) { return new SetWaitForAttackTimeAction(botAI); } + static Action* focus_heal_targets(PlayerbotAI* botAI) { return new SetFocusHealTargetsAction(botAI); } }; #endif diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 6bdfcdc4134..40316bd62a3 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -146,6 +146,7 @@ class ChatTriggerContext : public NamedObjectContext creators["pet attack"] = &ChatTriggerContext::pet_attack; creators["roll"] = &ChatTriggerContext::roll_action; creators["wait for attack time"] = &ChatTriggerContext::wait_for_attack_time; + creators["focus heal"] = &ChatTriggerContext::focus_heal; } private: @@ -271,6 +272,7 @@ class ChatTriggerContext : public NamedObjectContext static Trigger* pet_attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet attack"); } static Trigger* roll_action(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "roll"); } static Trigger* wait_for_attack_time(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "wait for attack time"); } + static Trigger* focus_heal(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "focus heal"); } }; #endif diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index e13b57dd10c..77a8d9d0b2d 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -107,6 +107,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back(new TriggerNode("pet", { NextAction("pet", relevance) })); triggers.push_back(new TriggerNode("pet attack", { NextAction("pet attack", relevance) })); triggers.push_back(new TriggerNode("roll", { NextAction("roll", relevance) })); + triggers.push_back(new TriggerNode("focus heal", { NextAction("focus heal targets", relevance) })); } ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) @@ -200,4 +201,5 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("pet"); supported.push_back("pet attack"); supported.push_back("wait for attack time"); + supported.push_back("focus heal"); } diff --git a/src/Ai/Base/Strategy/FocusTargetStrategy.h b/src/Ai/Base/Strategy/FocusTargetStrategy.h new file mode 100644 index 00000000000..9af35c66653 --- /dev/null +++ b/src/Ai/Base/Strategy/FocusTargetStrategy.h @@ -0,0 +1,20 @@ +/* + * 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_FOCUSTARGETSTRATEGY_H +#define _PLAYERBOT_FOCUSTARGETSTRATEGY_H + +#include "Strategy.h" + +class PlayerbotAI; + +class FocusHealTargetsStrategy : public Strategy +{ +public: + FocusHealTargetsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + std::string const getName() override { return "focus heal targets"; } +}; + +#endif diff --git a/src/Ai/Base/StrategyContext.h b/src/Ai/Base/StrategyContext.h index c16573fa071..5386e872bfd 100644 --- a/src/Ai/Base/StrategyContext.h +++ b/src/Ai/Base/StrategyContext.h @@ -19,6 +19,7 @@ #include "DuelStrategy.h" #include "EmoteStrategy.h" #include "FleeStrategy.h" +#include "FocusTargetStrategy.h" #include "FollowMasterStrategy.h" #include "GrindingStrategy.h" #include "GroupStrategy.h" @@ -126,6 +127,7 @@ class StrategyContext : public NamedObjectContext creators["use bobber"] = &StrategyContext::bobber_strategy; creators["master fishing"] = &StrategyContext::master_fishing; creators["wait for attack"] = &StrategyContext::wait_for_attack; + creators["focus heal targets"] = &StrategyContext::focus_heal_targets; } private: @@ -198,6 +200,7 @@ class StrategyContext : public NamedObjectContext static Strategy* bobber_strategy(PlayerbotAI* botAI) { return new UseBobberStrategy(botAI); } static Strategy* master_fishing(PlayerbotAI* botAI) { return new MasterFishingStrategy(botAI); } static Strategy* wait_for_attack(PlayerbotAI* botAI) { return new WaitForAttackStrategy(botAI); } + static Strategy* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetsStrategy(botAI); } }; class MovementStrategyContext : public NamedObjectContext diff --git a/src/Ai/Base/Value/PartyMemberToHeal.cpp b/src/Ai/Base/Value/PartyMemberToHeal.cpp index b49c1cd4196..9845bc11912 100644 --- a/src/Ai/Base/Value/PartyMemberToHeal.cpp +++ b/src/Ai/Base/Value/PartyMemberToHeal.cpp @@ -38,6 +38,36 @@ Unit* PartyMemberToHeal::Calculate() bool isRaid = bot->GetGroup()->isRaidGroup(); MinValueCalculator calc(100); + // If focus heal targets strategy is active, only heal those targets + if (botAI->HasStrategy("focus heal targets", BOT_STATE_COMBAT)) + { + std::list const focusHealTargets = + AI_VALUE(std::list, "focus heal targets"); + + for (ObjectGuid const& focusHealTarget : focusHealTargets) + { + Player* player = ObjectAccessor::FindPlayer(focusHealTarget); + if (!player || !player->IsInWorld() || !player->IsAlive() || !player->IsInSameGroupWith(bot)) + continue; + + float health = player->GetHealthPct(); + if (isRaid || health < sPlayerbotAIConfig.mediumHealth || + !IsTargetOfSpellCast(player, predicate)) + { + float probeValue = 100.0f; + if (player->GetDistance2d(bot) > sPlayerbotAIConfig.healDistance) + probeValue = health + 30.0f; + else + probeValue = health + player->GetDistance2d(bot) / 10.0f; + + if (probeValue < calc.minValue && Check(player)) + calc.probe(probeValue, player); + } + } + + return (Unit*)calc.param; + } + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* player = gref->GetSource(); @@ -45,17 +75,17 @@ Unit* PartyMemberToHeal::Calculate() continue; if (player && player->IsAlive()) { - uint8 health = player->GetHealthPct(); + float health = player->GetHealthPct(); if (isRaid || health < sPlayerbotAIConfig.mediumHealth || !IsTargetOfSpellCast(player, predicate)) { - uint32 probeValue = 100; + float probeValue = 100.0f; if (player->GetDistance2d(bot) > sPlayerbotAIConfig.healDistance) { - probeValue = health + 30; + probeValue = health + 30.0f; } else { - probeValue = health + player->GetDistance2d(bot) / 10; + probeValue = health + player->GetDistance2d(bot) / 10.0f; } // delay Check player to here for better performance if (probeValue < calc.minValue && Check(player)) @@ -68,10 +98,10 @@ Unit* PartyMemberToHeal::Calculate() Pet* pet = player->GetPet(); if (pet && pet->IsAlive()) { - uint8 health = ((Unit*)pet)->GetHealthPct(); - uint32 probeValue = 100; + float health = ((Unit*)pet)->GetHealthPct(); + float probeValue = 100.0f; if (isRaid || health < sPlayerbotAIConfig.mediumHealth) - probeValue = health + 30; + probeValue = health + 30.0f; // delay Check pet to here for better performance if (probeValue < calc.minValue && Check(pet)) { @@ -82,10 +112,10 @@ Unit* PartyMemberToHeal::Calculate() Unit* charm = player->GetCharm(); if (charm && charm->IsAlive()) { - uint8 health = charm->GetHealthPct(); - uint32 probeValue = 100; + float health = charm->GetHealthPct(); + float probeValue = 100.0f; if (isRaid || health < sPlayerbotAIConfig.mediumHealth) - probeValue = health + 30; + probeValue = health + 30.0f; // delay Check charm to here for better performance if (probeValue < calc.minValue && Check(charm)) { diff --git a/src/Ai/Base/Value/TargetValue.h b/src/Ai/Base/Value/TargetValue.h index e9e6cdea4db..7d766578a53 100644 --- a/src/Ai/Base/Value/TargetValue.h +++ b/src/Ai/Base/Value/TargetValue.h @@ -140,4 +140,11 @@ class BossTargetValue : public TargetValue, public Qualified public: Unit* Calculate(); }; + +class FocusHealTargetValue : public ManualSetValue> +{ +public: + FocusHealTargetValue(PlayerbotAI* botAI) : ManualSetValue>(botAI, {}, "focus heal targets") {} +}; + #endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index 14cbd185c24..77d25e060ff 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -241,6 +241,7 @@ class ValueContext : public NamedObjectContext creators["travel target"] = &ValueContext::travel_target; creators["talk target"] = &ValueContext::talk_target; creators["pull target"] = &ValueContext::pull_target; + creators["focus heal targets"] = &ValueContext::focus_heal_targets; creators["group"] = &ValueContext::group; creators["range"] = &ValueContext::range; creators["inside target"] = &ValueContext::inside_target; @@ -497,6 +498,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* next_rpg_action(PlayerbotAI* botAI) { return new NextRpgActionValue(botAI); } static UntypedValue* travel_target(PlayerbotAI* botAI) { return new TravelTargetValue(botAI); } static UntypedValue* pull_target(PlayerbotAI* botAI) { return new PullTargetValue(botAI); } + static UntypedValue* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetValue(botAI); } static UntypedValue* bg_master(PlayerbotAI* botAI) { return new BgMasterValue(botAI); } static UntypedValue* bg_role(PlayerbotAI* botAI) { return new BgRoleValue(botAI); } From e13aa7d2f633a818da3a7b6664af6caf1ab1d08b Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 11 Apr 2026 07:15:57 +0200 Subject: [PATCH 60/87] IsTank fix for Death Knight (#2296) ## Pull Request Description Death Knights with Frost Presents are recognized by bots as tank Related with: #2290 ## How to Test the Changes 1. Create Death Knight and apply Frost Presence 2. Invite Restoration Druid bot to party 3. Bot should apply "Thorns" on you ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Bots now identify Death Knight with Frost Presence as tank - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Example of non Blood Death Knight tank build https://forum.warmane.com/showthread.php?325582 --- src/Bot/PlayerbotAI.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 1a74b8b2f7d..e9de581dead 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -54,6 +54,7 @@ #include "Unit.h" #include "UpdateTime.h" #include "Vehicle.h" +#include "../../../../src/server/scripts/Spells/spell_dk.cpp" const int SPELL_TITAN_GRIP = 49152; @@ -2163,7 +2164,7 @@ bool PlayerbotAI::IsTank(Player* player, bool bySpec) switch (player->getClass()) { case CLASS_DEATH_KNIGHT: - if (tab == DEATH_KNIGHT_TAB_BLOOD) + if (tab == DEATH_KNIGHT_TAB_BLOOD || player->HasAura(SPELL_DK_FROST_PRESENCE)) { return true; } From ae9b76aaa5f5ded7d06947bc26f319be039c2e3b Mon Sep 17 00:00:00 2001 From: Boidl <273966711+Boidl1337@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:16:15 +0200 Subject: [PATCH 61/87] Fix Undead/Draenei bots stuck in starting zones (#2298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description Undead and Draenei bots get stuck in an idle/rest loop in their starting zones because the default NPC scan range (150f) and quest giver filter (80f) are too small and not enough. NPCs fall within range for `WANDER_NPC` to activate (requires >= 3). This adds a configurable area-based override that increases both ranges to 200f only in affected areas. All other zones remain at default values. ## Feature Evaluation - **Minimum logic**: One `std::set::count()` lookup per `Calculate()` call to check if the bot's current area is in the override list. If yes, scan range is 200f instead of 150f. - **Processing cost**: `GetAreaId()` is a cached uint32 read (~1ns). `std::set::count()` on a 2-element set is O(log n) ≈ 1 comparison. Negligible compared to the grid scan itself (~33,000-53,000ns). ## How to Test the Changes 1. Create Undead or Draenei bots (level 1) 2. Observe that they pick up quests and start moving in Deathknell/Ammen Vale 3. Without this fix, they sit in REST status indefinitely 4. Optionally add/remove area IDs via `AiPlayerbot.RpgScanRangeOverrideAreaIds` in playerbots.conf ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] Minimal impact - pmon data (500 bots, extended run): - Default 150f: 0.033ms avg - Global 200f: 0.053ms avg - Area check (this PR): 0.042ms avg - The 0.009ms increase over default is caused by bots currently in starting zones scanning at 200f. Bots outside override areas are unaffected. - Does this change modify default bot behavior? - [x] No - Does this change add new decision branches or increase maintenance complexity? - [x] No - Uses the existing `LoadSet`/`std::set` config pattern already used throughout the codebase. ## AI Assistance - [x] Yes - Used AI to speed up understanding the codebase, locate relevant functions, and compare with the cmangos playerbots implementation. All code was reviewed and tested manually. ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Any new bot dialogue lines are translated. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers The root cause is that `WANDER_NPC` requires `possibleTargets.size() >= 3`, but sparse starting zones have fewer than 3 NPC-flagged units within 150f. Increasing the scan range to 200f brings enough NPCs into range for the status check to pass. The override is configurable via `AiPlayerbot.RpgScanRangeOverrideAreaIds` so server admins can add more areas without code changes. --- src/Ai/Base/Value/PossibleRpgTargetsValue.cpp | 11 ++++++++++- src/Ai/Base/Value/PossibleRpgTargetsValue.h | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp b/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp index f2b6aef105d..bf9e9d68950 100644 --- a/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp +++ b/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp @@ -13,6 +13,7 @@ #include "ServerFacade.h" #include "SharedDefines.h" #include "NearestGameObjects.h" +#include std::vector PossibleRpgTargetsValue::allowedNpcFlags; @@ -88,8 +89,11 @@ bool PossibleRpgTargetsValue::AcceptUnit(Unit* unit) std::vector PossibleNewRpgTargetsValue::allowedNpcFlags; +// Sparse starting zones where the default scan range is insufficient for WANDER_NPC (requires >= 3 NPCs) +static const std::unordered_set rpgRangeOverrideAreaIds = { 3526 /* Ammen Vale */, 2117 /* Deathknell */ }; + PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float range) - : NearestUnitsValue(botAI, "possible new rpg targets", range, true) + : NearestUnitsValue(botAI, "possible new rpg targets", range, true), defaultRange(range) { if (allowedNpcFlags.empty()) { @@ -119,6 +123,11 @@ PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float GuidVector PossibleNewRpgTargetsValue::Calculate() { + if (rpgRangeOverrideAreaIds.count(bot->GetAreaId()) && defaultRange < 200.0f) + range = 200.0f; + else + range = defaultRange; + std::list targets; FindUnits(targets); diff --git a/src/Ai/Base/Value/PossibleRpgTargetsValue.h b/src/Ai/Base/Value/PossibleRpgTargetsValue.h index ae5b596a0d8..00e5f801293 100644 --- a/src/Ai/Base/Value/PossibleRpgTargetsValue.h +++ b/src/Ai/Base/Value/PossibleRpgTargetsValue.h @@ -35,6 +35,8 @@ class PossibleNewRpgTargetsValue : public NearestUnitsValue protected: void FindUnits(std::list& targets) override; bool AcceptUnit(Unit* unit) override; +private: + float defaultRange; }; class PossibleNewRpgGameObjectsValue : public ObjectGuidListCalculatedValue From 74ccc6fbe99cffe4bc1c860ede0997af6eb1e4ee Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 11 Apr 2026 07:16:28 +0200 Subject: [PATCH 62/87] Mage additional stat weight (#2299) ## Pull Request Description Mage before getting Molten Armor dont prioritize Spirit before Intellect ## How to Test the Changes 1. Invite mage which dont have Molten Armor (level < 62) 2. Give him 2 items for same slot one with spirit one with intellect and unequip item on this slot and destroy 3. Bot should equip this with intellect (if other stats are same) ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Mage before getting Molten Armor dont prioritize Spirit before Intellect - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) To find best spot to change stat weights depending of class and HasSpell. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Example stat weights when using Molten Armor (Fire P1 Preset) obraz Example stat weight when using Mage Armor (Fire P1 Preset) obraz --- src/Mgr/Item/StatsWeightCalculator.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Mgr/Item/StatsWeightCalculator.cpp b/src/Mgr/Item/StatsWeightCalculator.cpp index 1b3f3dcfcb5..361e4a5f722 100644 --- a/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/src/Mgr/Item/StatsWeightCalculator.cpp @@ -20,6 +20,13 @@ #include "StatsCollector.h" #include "Unit.h" +namespace +{ +constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_1 = 30482; +constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_2 = 43045; +constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_3 = 43046; +} + StatsWeightCalculator::StatsWeightCalculator(Player* player) : player_(player) { if (PlayerbotAI::IsHeal(player)) @@ -454,6 +461,16 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player) if (player->HasAura(51885)) stats_weights_[STATS_TYPE_INTELLECT] += 1.1f; } + else if (cls == CLASS_MAGE) + { + if (!player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_1) + && !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_2) + && !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_3)) + { + stats_weights_[STATS_TYPE_INTELLECT] += 0.2f; + stats_weights_[STATS_TYPE_SPIRIT] -= 0.0f; + } + } } void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto) From 53a607e1478b0bbea2c73a6676231621bdb3e9b5 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:16:58 -0700 Subject: [PATCH 63/87] Enable bots to do Outdoor pvp (#2217) ## Pull Request Description Bots will now engage with outdoor pvp targets when in an area with them. I carved this out of the guildrpg system Im working on since it should work just fine as a standalone. Note this requires a core update https://github.com/azerothcore/azerothcore-wotlk/pull/25103 ## Feature Evaluation Its not expensive. the status checks are fairly light and simple. Should be on par with current rpg system actions ## How to Test the Changes You can try to use selfbot to enable this while in EPL, or set the probability of all other rpg actions to 0. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [ ] No, not at all - [x] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) There is some impact, but should be minimal overall. - Does this change modify default bot behavior? - [ ] No - [x] Yes (**explain why**) It will activate automatically based on default config. - Does this change add new decision branches or increase maintenance complexity? - [ ] No - [x] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - [ ] No - [x] Yes (**explain below**) Nothing beyond search functionality and autocomplete. ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 2 + src/Ai/Base/ActionContext.h | 3 + src/Ai/Base/TriggerContext.h | 2 + src/Ai/World/Rpg/Action/NewRpgAction.cpp | 11 +- src/Ai/World/Rpg/Action/NewRpgAction.h | 9 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 21 ++- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 2 +- src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp | 128 +++++++++++++++++++ src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h | 19 +++ src/Ai/World/Rpg/NewRpgInfo.cpp | 17 +++ src/Ai/World/Rpg/NewRpgInfo.h | 9 +- src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp | 8 ++ src/Bot/RandomPlayerbotMgr.cpp | 4 +- src/PlayerbotAIConfig.cpp | 1 + src/PlayerbotAIConfig.h | 3 +- 15 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp create mode 100644 src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index ccb392e1aef..88920a05041 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1038,6 +1038,7 @@ AiPlayerbot.EnableNewRpgStrategy = 1 # DoQuest (Default: 60 Select quest from the quest log and head to the location to attempt completion​) # TravelFlight (Default: 15 Go to the nearest flightmaster and fly to a level-appropriate area) # Rest (Default: 5 Take a break for a while and do nothing) +# OutdoorPvp (Default: 10 Participate in outdoor PvP capture points if already in an outdoor PvP zone) AiPlayerbot.RpgStatusProbWeight.WanderRandom = 15 AiPlayerbot.RpgStatusProbWeight.WanderNpc = 20 AiPlayerbot.RpgStatusProbWeight.GoGrind = 15 @@ -1045,6 +1046,7 @@ AiPlayerbot.RpgStatusProbWeight.GoCamp = 10 AiPlayerbot.RpgStatusProbWeight.DoQuest = 60 AiPlayerbot.RpgStatusProbWeight.TravelFlight = 15 AiPlayerbot.RpgStatusProbWeight.Rest = 5 +AiPlayerbot.RpgStatusProbWeight.OutdoorPvp = 10 # Bots' minimum and maximum level when teleporting in and out of a zone, according to the new RPG strategy # Format: AiPlayerbot.ZoneBracket.zoneID = minLevel,maxLevel diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 41763be90f2..79b5b498581 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -63,6 +63,7 @@ #include "WorldBuffAction.h" #include "XpGainAction.h" #include "NewRpgAction.h" +#include "NewRpgOutdoorPvP.h" #include "FishingAction.h" #include "CancelChannelAction.h" #include "WaitForAttackAction.h" @@ -265,6 +266,7 @@ class ActionContext : public NamedObjectContext creators["new rpg wander npc"] = &ActionContext::new_rpg_wander_npc; creators["new rpg do quest"] = &ActionContext::new_rpg_do_quest; creators["new rpg travel flight"] = &ActionContext::new_rpg_travel_flight; + creators["new rpg outdoor pvp"] = &ActionContext::new_rpg_outdoor_pvp; creators["wait for attack keep safe distance"] = &ActionContext::wait_for_attack_keep_safe_distance; } @@ -462,6 +464,7 @@ class ActionContext : public NamedObjectContext static Action* new_rpg_wander_npc(PlayerbotAI* ai) { return new NewRpgWanderNpcAction(ai); } static Action* new_rpg_do_quest(PlayerbotAI* ai) { return new NewRpgDoQuestAction(ai); } static Action* new_rpg_travel_flight(PlayerbotAI* ai) { return new NewRpgTravelFlightAction(ai); } + static Action* new_rpg_outdoor_pvp(PlayerbotAI* ai) { return new NewRpgOutdoorPvpAction(ai); } static Action* wait_for_attack_keep_safe_distance(PlayerbotAI* ai) { return new WaitForAttackKeepSafeDistanceAction(ai); } }; diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index c77df3a31ba..bfdddecf7a3 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -230,6 +230,7 @@ class TriggerContext : public NamedObjectContext creators["wander npc status"] = &TriggerContext::wander_npc_status; creators["do quest status"] = &TriggerContext::do_quest_status; creators["travel flight status"] = &TriggerContext::travel_flight_status; + creators["outdoor pvp status"] = &TriggerContext::outdoor_pvp_status; creators["can self resurrect"] = &TriggerContext::can_self_resurrect; creators["can fish"] = &TriggerContext::can_fish; creators["can use fishing bobber"] = &TriggerContext::can_use_fishing_bobber; @@ -436,6 +437,7 @@ class TriggerContext : public NamedObjectContext static Trigger* wander_npc_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_WANDER_NPC); } static Trigger* do_quest_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_DO_QUEST); } static Trigger* travel_flight_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_TRAVEL_FLIGHT); } + static Trigger* outdoor_pvp_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_OUTDOOR_PVP); } static Trigger* can_self_resurrect(PlayerbotAI* ai) { return new SelfResurrectTrigger(ai); } static Trigger* can_fish(PlayerbotAI* ai) { return new CanFishTrigger(ai); } static Trigger* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberTrigger(ai); } diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index ddd1240da22..ca0ca243360 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -62,7 +62,7 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) { case RPG_IDLE: return RandomChangeStatus({RPG_GO_CAMP, RPG_GO_GRIND, RPG_WANDER_RANDOM, RPG_WANDER_NPC, RPG_DO_QUEST, - RPG_TRAVEL_FLIGHT, RPG_REST}); + RPG_TRAVEL_FLIGHT, RPG_REST, RPG_OUTDOOR_PVP}); case RPG_GO_GRIND: { @@ -140,6 +140,15 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) } break; } + case RPG_OUTDOOR_PVP: + { + if (info.HasStatusPersisted(statusOutDoorPvPDuration)) + { + info.ChangeToIdle(); + return true; + } + break; + } default: break; } diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.h b/src/Ai/World/Rpg/Action/NewRpgAction.h index a8cb7a2bcef..83594204b64 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgAction.h @@ -47,10 +47,11 @@ class NewRpgStatusUpdateAction : public NewRpgBaseAction protected: // static NewRpgStatusTransitionProb transitionMat; - const int32 statusWanderNpcDuration = 5 * 60 * 1000; - const int32 statusWanderRandomDuration = 5 * 60 * 1000; - const int32 statusRestDuration = 30 * 1000; - const int32 statusDoQuestDuration = 30 * 60 * 1000; + const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ; + const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ; + const int32 statusRestDuration = 30 * IN_MILLISECONDS ; + const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ; + const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ; }; class NewRpgGoGrindAction : public NewRpgBaseAction diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 986b2f0f543..092b115387f 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -12,6 +12,7 @@ #include "NewRpgStrategy.h" #include "Object.h" #include "ObjectAccessor.h" +#include "OutdoorPvPMgr.h" #include "ObjectDefines.h" #include "ObjectGuid.h" #include "ObjectMgr.h" @@ -222,12 +223,10 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) return MoveTo(mapId, x, y, z, false, false, false, true); } -bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority) +bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, WorldObject* center) { if (IsWaitingForLastMove(priority)) - { return false; - } Map* map = bot->GetMap(); const float x = bot->GetPositionX(); @@ -1160,6 +1159,11 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta bot->SetStandState(UNIT_STAND_STATE_SIT); return true; } + case RPG_OUTDOOR_PVP: + { + botAI->rpgInfo.ChangeToOutdoorPvp(); + return true; + } default: { botAI->rpgInfo.ChangeToRest(); @@ -1220,6 +1224,17 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) std::vector path; return SelectRandomFlightTaxiNode(flightMaster, path); } + case RPG_OUTDOOR_PVP: + { + if (!bot->IsPvP()) + return false; + uint32 zoneId = bot->GetZoneId(); + if (zoneId == AREA_NAGRAND) + return false; + + OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId); + return outdoorPvP != nullptr; + } default: return false; } diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index f17891ffc65..eaba7244628 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -31,7 +31,7 @@ class NewRpgBaseAction : public MovementAction /* MOVEMENT RELATED */ bool MoveFarTo(WorldPosition dest); bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); - bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); + bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr); bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); /* QUEST RELATED CHECK */ diff --git a/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp new file mode 100644 index 00000000000..042246cfb4c --- /dev/null +++ b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp @@ -0,0 +1,128 @@ +#include "NewRpgOutdoorPvP.h" +#include "OutdoorPvP.h" +#include "OutdoorPvPMgr.h" + +bool NewRpgOutdoorPvpAction::Execute(Event event) +{ + if (!bot->IsPvP()) + { + botAI->rpgInfo.ChangeToIdle(); + return false; + } + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive()) + return false; + + uint32 zoneId = bot->GetZoneId(); + OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId); + if (!outdoorPvP || zoneId == AREA_NAGRAND) + { + botAI->rpgInfo.ChangeToIdle(); + return false; + } + + OutdoorPvP::OPvPCapturePointMap const& capturePointMap = outdoorPvP->GetCapturePoints(); + + NewRpgInfo& info = botAI->rpgInfo; + auto* dataPtr = std::get_if(&info.data); + if (!dataPtr) + return false; + auto& data = *dataPtr; + // Re-resolve stored spawn ID from the capture point map each tick (avoids dangling pointers) + OPvPCapturePoint* objective = nullptr; + if (data.capturePointSpawnId && !capturePointMap.empty()) + { + auto it = capturePointMap.find(data.capturePointSpawnId); + if (it != capturePointMap.end()) + { + OPvPCapturePoint* capturePoint = it->second; + if (capturePoint && capturePoint->_capturePoint) + { + float threshold = capturePoint->GetMinValue(); + float slider = capturePoint->GetSlider(); + uint8 faction = bot->GetTeamId(); + LOG_DEBUG("playerbots", "[NEW RPG] Bot {} with faction {} is evaluating existing RPG objective {} with threshold {} and slider value {}", bot->GetName(), faction, capturePoint->_capturePoint->GetName(), threshold, slider); + if ((faction == TEAM_HORDE && slider >= -threshold) || + (faction == TEAM_ALLIANCE && slider <= threshold)) + objective = capturePoint; + } + } + if (!objective) + data.capturePointSpawnId = 0; + } + + if (!objective) + { + objective = SelectNewObjective(capturePointMap); + if (!objective) + { + botAI->rpgInfo.ChangeToIdle(); + return true; + } + data.capturePointSpawnId = objective->m_capturePointSpawnId; + LOG_DEBUG("playerbots","[NEW RPG] Bot {} selected OutDoorPvP target capturePointSpawnId {}", bot->GetName(), data.capturePointSpawnId); + } + + GameObject* objectiveGO = objective->_capturePoint; + if (!objectiveGO) + return false; + + if (objectiveGO->GetGoType() != GAMEOBJECT_TYPE_CAPTURE_POINT) + return false; + + float radius = objectiveGO->GetGOInfo()->capturePoint.radius / 2.0f; + if (!objectiveGO->IsWithinDistInMap(bot, radius)) + return MoveFarTo(WorldPosition(objectiveGO)); + + return PatrolCapturePoint(objectiveGO, radius); +} + +OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCapturePointMap const& capturePointMap) +{ + OPvPCapturePoint* objective = nullptr; + uint8 faction = bot->GetTeamId(); + std::vector candidateObjectives; + + if (capturePointMap.empty()) + { + botAI->rpgInfo.ChangeToIdle(); + return objective; + } + for (auto const& [guid, point] : capturePointMap) + { + GameObject* capturePointObject = point->_capturePoint; + if (!capturePointObject) + continue; + + float threshold = point->GetMinValue(); + float slider = point->GetSlider(); + if (faction == TEAM_HORDE && slider > -threshold) + candidateObjectives.push_back(point); + else if (faction == TEAM_ALLIANCE && slider < threshold) + candidateObjectives.push_back(point); + } + if (candidateObjectives.empty()) + { + LOG_DEBUG("playerbots", "[New RPG] Bot {} found no valid outdoor PVP objectives to capture", bot->GetName()); + botAI->rpgInfo.ChangeToIdle(); + return objective; + } + int randomIndex = urand(0, candidateObjectives.size() - 1); + objective = candidateObjectives[randomIndex]; + return objective; +} + +bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius) +{ + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) + return false; + + // Randomly pause at the current spot before picking a new patrol point + if (urand(0, 2) == 0) + return ForceToWait(urand(3000, 6000)); + + float patrolRadius = radius * 0.8f; + if (MoveRandomNear(patrolRadius, MovementPriority::MOVEMENT_NORMAL, objectiveGO)) + return true; + + return ForceToWait(urand(3000, 6000)); +} diff --git a/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h new file mode 100644 index 00000000000..0487b9891f1 --- /dev/null +++ b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h @@ -0,0 +1,19 @@ +#ifndef PLAYERBOT_NEWRPGOUTDOORPVP_H +#define PLAYERBOT_NEWRPGOUTDOORPVP_H + +#include "NewRpgBaseAction.h" +#include "OutdoorPvP.h" + +class NewRpgOutdoorPvpAction : public NewRpgBaseAction +{ +public: + NewRpgOutdoorPvpAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg outdoor pvp") {} + + virtual bool Execute(Event event) override; + OPvPCapturePoint* SelectNewObjective(OutdoorPvP::OPvPCapturePointMap const& capturePointMap); + +private: + bool PatrolCapturePoint(GameObject* objectiveGO, float radius); +}; + +#endif diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 4e04ab08674..4935503fc25 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -47,6 +47,14 @@ void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector) return RPG_REST; if constexpr (std::is_same_v) return RPG_DO_QUEST; if constexpr (std::is_same_v) return RPG_TRAVEL_FLIGHT; + if constexpr (std::is_same_v) return RPG_OUTDOOR_PVP; return RPG_IDLE; }, data); } @@ -153,6 +162,14 @@ std::string NewRpgInfo::ToString() out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; } + else if constexpr (std::is_same_v) + { + out << "OUTDOOR_PVP"; + if (!arg.capturePointSpawnId) + out << "\nNo capture point assigned."; + else + out << "\ncapturePointSpawnId: " << arg.capturePointSpawnId; + } else out << "UNKNOWN"; }, data); diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index c2349c14b2f..9e6abdda4c7 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -58,6 +58,11 @@ struct NewRpgInfo { Rest() = default; }; + // RPG_OUTDOOR_PVP + struct OutdoorPvP + { + ObjectGuid::LowType capturePointSpawnId{0}; + }; struct Idle { }; @@ -79,7 +84,8 @@ struct NewRpgInfo WanderRandom, DoQuest, Rest, - TravelFlight + TravelFlight, + OutdoorPvP >; RpgData data; @@ -91,6 +97,7 @@ struct NewRpgInfo void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); + void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0); void ChangeToRest(); void ChangeToIdle(); bool CanChangeTo(NewRpgStatus status); diff --git a/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp b/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp index f0d0ce5a923..521b15c34ad 100644 --- a/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp +++ b/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp @@ -65,6 +65,14 @@ void NewRpgStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back( + new TriggerNode( + "outdoor pvp status", + { + NextAction("new rpg outdoor pvp", 3.0f) + } + ) + ); } void NewRpgStrategy::InitMultipliers(std::vector&) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index a772136e771..ee06c58f899 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -2861,10 +2861,10 @@ void RandomPlayerbotMgr::PrintStats() LOG_INFO("playerbots", "Bots rpg status:"); LOG_INFO("playerbots", " Idle: {}, Rest: {}, GoGrind: {}, GoCamp: {}, MoveRandom: {}, MoveNpc: {}, DoQuest: {}, " - "TravelFlight: {}", + "TravelFlight: {}, OutdoorPvP: {}", rpgStatusCount[RPG_IDLE], rpgStatusCount[RPG_REST], rpgStatusCount[RPG_GO_GRIND], rpgStatusCount[RPG_GO_CAMP], rpgStatusCount[RPG_WANDER_RANDOM], rpgStatusCount[RPG_WANDER_NPC], - rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT]); + rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT], rpgStatusCount[RPG_OUTDOOR_PVP]); LOG_INFO("playerbots", "Bots total quests:"); LOG_INFO("playerbots", " Accepted: {}, Rewarded: {}, Dropped: {}", rpgStasticTotal.questAccepted, diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index f150f7af198..8c8343db2b6 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -652,6 +652,7 @@ bool PlayerbotAIConfig::Initialize() RpgStatusProbWeight[RPG_DO_QUEST] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.DoQuest", 60); RpgStatusProbWeight[RPG_TRAVEL_FLIGHT] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.TravelFlight", 15); RpgStatusProbWeight[RPG_REST] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.Rest", 5); + RpgStatusProbWeight[RPG_OUTDOOR_PVP] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.OutdoorPvp", 10); syncLevelWithPlayers = sConfigMgr->GetOption("AiPlayerbot.SyncLevelWithPlayers", false); randomBotGroupNearby = sConfigMgr->GetOption("AiPlayerbot.RandomBotGroupNearby", false); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 7b6c1eb6f57..4e758e79e93 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -56,7 +56,8 @@ enum NewRpgStatus : int RPG_TRAVEL_FLIGHT = 6, // Taking a break RPG_REST = 7, - RPG_STATUS_END = 8 + RPG_OUTDOOR_PVP = 8, + RPG_STATUS_END = 9 }; #define MAX_SPECNO 20 From e177901402084ee57224d625d87b87208c510bf2 Mon Sep 17 00:00:00 2001 From: bashermens <31279994+hermensbas@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:18:13 +0200 Subject: [PATCH 64/87] Bug(self-bot): Self-bot should face target when using ranged or melee attack (#2300) ## Pull Request Description Currently when using self-bot and that self-bot is using ranged attacks its not facing the target, this happens because the client doesnt receive the instruction in Player::SendMessageToSet(WorldPacket const* data, bool self); ## How to Test the Changes Create new character with uses range attack as base attack (hunter, mage, druid etc) and observe. Apply patch and observe your self-bot character is now always facing towards the target. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Util/ServerFacade.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Util/ServerFacade.cpp b/src/Util/ServerFacade.cpp index 12a77de6359..1ba1eb5ea5b 100644 --- a/src/Util/ServerFacade.cpp +++ b/src/Util/ServerFacade.cpp @@ -53,8 +53,9 @@ void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool force) bot->SetOrientation(angle); if (!bot->IsRooted()) - bot->SendMovementFlagUpdate(); - // } + // enforce (bool self) true otherwhise when using real-client with self-bot wont + // recieve update; e.g. will not face the target when using (mostly ranged) attack + bot->SendMovementFlagUpdate(true); } Unit* ServerFacade::GetChaseTarget(Unit* target) From 5d7fb9e97d93116a78808d34dea5d9cd056fae26 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 11 Apr 2026 07:24:38 +0200 Subject: [PATCH 65/87] Druid - lifebloom (#2292) ## Pull Request Description Restoration Druid now use lifebloom when get Clearcasting proc. ## How to Test the Changes 1. Invite to group restoration druid and tank 2. Attack target (for example dummy) 3. When druid heals and get clearcasting should cast lifebloom on tank ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Restoration druid by default use lifebloom on tank when got clearcasting proc. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Copilot CLI to review. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers obraz obraz --- src/Ai/Class/Druid/Action/DruidActions.cpp | 10 ++++++++++ src/Ai/Class/Druid/Action/DruidActions.h | 8 ++++++++ src/Ai/Class/Druid/DruidAiObjectContext.cpp | 4 ++++ src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp | 3 +++ src/Ai/Class/Druid/Trigger/DruidTriggers.h | 6 ++++++ 5 files changed, 31 insertions(+) diff --git a/src/Ai/Class/Druid/Action/DruidActions.cpp b/src/Ai/Class/Druid/Action/DruidActions.cpp index d6a950b175f..2eb809480f4 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.cpp +++ b/src/Ai/Class/Druid/Action/DruidActions.cpp @@ -23,6 +23,16 @@ std::vector CastAbolishPoisonOnPartyAction::getAlternatives() CastSpellAction::getPrerequisites()); } +bool CastLifebloomOnMainTankAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target || !target->IsAlive() || !CastSpellAction::isUseful()) + return false; + + Aura* lifebloom = botAI->GetAura("lifebloom", target, true, true); + return !lifebloom || lifebloom->GetStackAmount() < 3 || lifebloom->GetDuration() < 2000; +} + Value* CastEntanglingRootsCcAction::GetTargetValue() { return context->GetValue("cc target", "entangling roots"); diff --git a/src/Ai/Class/Druid/Action/DruidActions.h b/src/Ai/Class/Druid/Action/DruidActions.h index e3e177590d4..016c3dfc433 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.h +++ b/src/Ai/Class/Druid/Action/DruidActions.h @@ -128,6 +128,14 @@ class CastThornsOnMainTankAction : public BuffOnMainTankAction CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {} }; +class CastLifebloomOnMainTankAction : public BuffOnMainTankAction +{ +public: + CastLifebloomOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "lifebloom", true) {} + + bool isUseful() override; +}; + class CastOmenOfClarityAction : public CastBuffSpellAction { public: diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 17b561d19f4..3d9086cffc9 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -79,6 +79,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext DruidTriggerFactoryInternal() { creators["omen of clarity"] = &DruidTriggerFactoryInternal::omen_of_clarity; + creators["clearcasting"] = &DruidTriggerFactoryInternal::clearcasting; creators["thorns"] = &DruidTriggerFactoryInternal::thorns; creators["thorns on party"] = &DruidTriggerFactoryInternal::thorns_on_party; creators["thorns on main tank"] = &DruidTriggerFactoryInternal::thorns_on_main_tank; @@ -117,6 +118,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext private: static Trigger* natures_swiftness(PlayerbotAI* botAI) { return new NaturesSwiftnessTrigger(botAI); } + static Trigger* clearcasting(PlayerbotAI* botAI) { return new ClearcastingTrigger(botAI); } static Trigger* eclipse_solar(PlayerbotAI* botAI) { return new EclipseSolarTrigger(botAI); } static Trigger* eclipse_lunar(PlayerbotAI* botAI) { return new EclipseLunarTrigger(botAI); } static Trigger* thorns(PlayerbotAI* botAI) { return new ThornsTrigger(botAI); } @@ -209,6 +211,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext creators["thorns"] = &DruidAiObjectContextInternal::thorns; creators["thorns on party"] = &DruidAiObjectContextInternal::thorns_on_party; creators["thorns on main tank"] = &DruidAiObjectContextInternal::thorns_on_main_tank; + creators["lifebloom on main tank"] = &DruidAiObjectContextInternal::lifebloom_on_main_tank; creators["cure poison"] = &DruidAiObjectContextInternal::cure_poison; creators["cure poison on party"] = &DruidAiObjectContextInternal::cure_poison_on_party; creators["abolish poison"] = &DruidAiObjectContextInternal::abolish_poison; @@ -304,6 +307,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext static Action* thorns(PlayerbotAI* botAI) { return new CastThornsAction(botAI); } static Action* thorns_on_party(PlayerbotAI* botAI) { return new CastThornsOnPartyAction(botAI); } static Action* thorns_on_main_tank(PlayerbotAI* botAI) { return new CastThornsOnMainTankAction(botAI); } + static Action* lifebloom_on_main_tank(PlayerbotAI* botAI) { return new CastLifebloomOnMainTankAction(botAI); } static Action* cure_poison(PlayerbotAI* botAI) { return new CastCurePoisonAction(botAI); } static Action* cure_poison_on_party(PlayerbotAI* botAI) { return new CastCurePoisonOnPartyAction(botAI); } static Action* abolish_poison(PlayerbotAI* botAI) { return new CastAbolishPoisonAction(botAI); } diff --git a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp index 7529cbb8ea7..0710e0fdc1e 100644 --- a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp @@ -56,6 +56,9 @@ void HealDruidStrategy::InitTriggers(std::vector& triggers) new TriggerNode("party member critical health", { NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4) })); + triggers.push_back(new TriggerNode("clearcasting", + { NextAction("lifebloom on main tank", ACTION_CRITICAL_HEAL - 1) })); + triggers.push_back(new TriggerNode( "group heal setting", { diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.h b/src/Ai/Class/Druid/Trigger/DruidTriggers.h index adad0d2a3cf..01daaeba65a 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.h +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.h @@ -61,6 +61,12 @@ class OmenOfClarityTrigger : public BuffTrigger OmenOfClarityTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "omen of clarity") {} }; +class ClearcastingTrigger : public HasAuraTrigger +{ +public: + ClearcastingTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "clearcasting") {} +}; + class RakeTrigger : public DebuffTrigger { public: From 8f7d352f7e7de7416fdcddac20f36aa9bc26f7c5 Mon Sep 17 00:00:00 2001 From: Flashtate98 <106729837+flashtate98@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:26:08 -0500 Subject: [PATCH 66/87] Implement Auchenai Crypts Strategies and TBC Dungeon Contexts (#2229) ## Pull Request Description This PR aims to add bot strategies to the Auchenai Crypts dungeon, specifically for the boss Shirrak the Dead Watcher, as his focus fire mechanic is really annoying to deal with. Additionally I have added TbcDungeonActionContext.h and TbcDungeonTriggerContext.h for future reference as I add more strategies to TBC dungeons that need it. A HUGE thank you to @brighton-chi for all his help. This has been a fun learning experience! ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. Trigger: A 20 yard radius grid search is performed to locate the focus fire trigger NPC Action: Bots will flee from the trigger NPC using MoveAway with a 5 yard safety buffer Multiplier: A for loop that prevents bots from running back into the focus fire mechanic while its active. Tanking: Tanks have a set coordinate to drag the boss to for the duration of the fight. - Describe the **processing cost** when this logic executes across many bots. Minimal, these scripts only execute when Shirrak has been engaged. ## How to Test the Changes Go into Auchenai Crypts (Normal or Heroic) and engage Shirrak the Dead Watcher ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [X] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) This logic is only applied to Auchenai Crypts and activates during Shirrak's encounter. - Does this change modify default bot behavior? - - [ ] No - - [X] Yes (**explain why**) Behavior only applies when in the instance and when engaged with Shirrak. - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [X] Yes (**explain below**) New dungeon contexts were added (TbcDungeonActionContext and TbcDungeonTriggerContext) to provide a clean and dedicated structure for future TBC dungeon strategies that will be developed. This was done to be in alignment with the existing WOTLK contexts that already exist. ## Messages to Translate Does this change add bot messages to translate? - - [X] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [X] Yes (**explain below**) Gemini was used to help me understand existing code in the module I was referencing and reusing to develop this strategy as I am not a programmer and hardly know anything about C++. ## Final Checklist - - [X] Stability is not compromised. - - [ ] Performance impact is understood, tested, and acceptable. - - [ ] Added logic complexity is justified and explained. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers If there are any better alternatives that could be used to help improve the strategy in any way, please suggest it. I am very new to this and want to learn and improve. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- .../Action/AuchenaiCryptsActions.cpp | 117 ++++++++++++++++++ .../Action/AuchenaiCryptsActions.h | 31 +++++ .../AuchenaiCryptsActionContext.h | 34 +++++ .../AuchenaiCryptsTriggerContext.h | 35 ++++++ .../Multiplier/AuchenaiCryptsMultipliers.cpp | 45 +++++++ .../Multiplier/AuchenaiCryptsMultipliers.h | 13 ++ .../Strategy/AuchenaiCryptsStrategy.cpp | 21 ++++ .../Strategy/AuchenaiCryptsStrategy.h | 19 +++ .../Trigger/AuchenaiCryptsTriggers.cpp | 34 +++++ .../Trigger/AuchenaiCryptsTriggers.h | 38 ++++++ src/Ai/Dungeon/DungeonStrategyContext.h | 4 +- src/Ai/Dungeon/TbcDungeonActionContext.h | 6 + src/Ai/Dungeon/TbcDungeonTriggerContext.h | 6 + src/Bot/Engine/BuildSharedActionContexts.cpp | 2 + src/Bot/Engine/BuildSharedTriggerContexts.cpp | 2 + src/Bot/PlayerbotAI.cpp | 5 +- 16 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp create mode 100644 src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h create mode 100644 src/Ai/Dungeon/TbcDungeonActionContext.h create mode 100644 src/Ai/Dungeon/TbcDungeonTriggerContext.h diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp new file mode 100644 index 00000000000..2ec7907c738 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp @@ -0,0 +1,117 @@ +#include "Playerbots.h" +#include "AiFactory.h" +#include "AuchenaiCryptsTriggers.h" +#include "AuchenaiCryptsActions.h" + +// Shirrak the Dead Watcher + +static const Position SHIRRAK_RANGED_POSITION = { -21.777f, -162.700f, 26.062f }; +static const Position SHIRRAK_TANK_POSITION = { -65.171f, -162.920f, 26.504f }; + +// Tank will position Shirrak at the specified coordinates, further down the corridor past the stairs + +bool ShirrakTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* shirrak = AI_VALUE2(Unit*, "find target", "shirrak the dead watcher"); + if (!shirrak) + return false; + + if (bot->GetVictim() != shirrak) + return Attack(shirrak); + + if (shirrak->GetVictim() == bot && bot->IsWithinMeleeRange(shirrak) && + bot->GetHealthPct()>30.0f) + { + const Position& position = SHIRRAK_TANK_POSITION; + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 6.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(2.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Flee from Shirrak's Focus Fire + +bool ShirrakFleeFocusFireAction::Execute(Event /*event*/) +{ + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast(AuchenaiCryptsIDs::NPC_FOCUS_FIRE), 20.0f); + + for (Creature* flare : creatureList) + { + if (flare && flare->IsAlive()) + { + float currentDistance = bot->GetDistance2d(flare); + constexpr float safeDistance = 12.0f; + constexpr float buffer = 5.0f; + + if (currentDistance < safeDistance) + { + bot->AttackStop(); + + float distanceToMove = safeDistance - currentDistance + buffer; + + return MoveAway(flare, distanceToMove); + } + } + } + return false; +} + +// Ranged should keep distance from Shirrak, staying at the edge of the stairs + +bool ShirrakRangedKeepDistanceAction::Execute(Event /*event*/) +{ + + std::vector rangedBots; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsRanged(member)) + rangedBots.push_back(member); + } + } + + auto findIt = std::find(rangedBots.begin(), rangedBots.end(), bot); + size_t botIndex = (findIt != rangedBots.end()) ? std::distance(rangedBots.begin(), findIt) : 0; + size_t count = rangedBots.size(); + + constexpr float arcSpan = M_PI / 2.0f; + float arcCenter = M_PI; + float arcStart = arcCenter - (arcSpan / 2.0f); + + float angle = (count <= 1) ? arcCenter : (arcStart + (arcSpan * (float)botIndex / (float)(count - 1))); + + constexpr float spreadRadius = 3.0f; + float targetX = SHIRRAK_RANGED_POSITION.GetPositionX() + cos(angle) * spreadRadius; + float targetY = SHIRRAK_RANGED_POSITION.GetPositionY() + sin(angle) * spreadRadius; + + float distToSpot = bot->GetExactDist2d(targetX, targetY); + + if (distToSpot > 4.0f) + { + float dX = targetX - bot->GetPositionX(); + float dY = targetY - bot->GetPositionY(); + + float moveDist = std::min(2.0f, distToSpot); + float moveX = bot->GetPositionX() + (dX / distToSpot) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToSpot) * moveDist; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h new file mode 100644 index 00000000000..4764efb6509 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h @@ -0,0 +1,31 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONS_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONS_H + +#include "AttackAction.h" +#include "MovementActions.h" +#include "AuchenaiCryptsTriggers.h" + +// Shirrak the Dead Watcher + +class ShirrakTankPositionBossAction : public AttackAction +{ +public: + ShirrakTankPositionBossAction(PlayerbotAI* botAI) : AttackAction(botAI, "shirrak tank position boss") {} + bool Execute(Event event) override; +}; + +class ShirrakFleeFocusFireAction : public MovementAction +{ +public: + ShirrakFleeFocusFireAction(PlayerbotAI* botAI) : MovementAction(botAI, "shirrak flee focus fire") {} + bool Execute(Event event) override; +}; + +class ShirrakRangedKeepDistanceAction : public MovementAction +{ +public: + ShirrakRangedKeepDistanceAction(PlayerbotAI* botAI) : MovementAction(botAI, "shirrak ranged keep distance") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h new file mode 100644 index 00000000000..4eea5871611 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h @@ -0,0 +1,34 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONCONTEXT_H + +#include "AiObjectContext.h" +#include "Action.h" +#include "AuchenaiCryptsActions.h" + +class TbcDungeonAuchenaiCryptsActionContext : public NamedObjectContext +{ +public: + TbcDungeonAuchenaiCryptsActionContext() : NamedObjectContext(false, true) + { + creators["shirrak tank position boss"] = + &TbcDungeonAuchenaiCryptsActionContext::shirrak_tank_position_boss; + + creators["shirrak flee focus fire"] = + &TbcDungeonAuchenaiCryptsActionContext::shirrak_flee_focus_fire; + + creators["shirrak ranged keep distance"] = + &TbcDungeonAuchenaiCryptsActionContext::shirrak_ranged_keep_distance; + } +private: + + static Action* shirrak_tank_position_boss( + PlayerbotAI* botAI) { return new ShirrakTankPositionBossAction(botAI); } + + static Action* shirrak_flee_focus_fire( + PlayerbotAI* botAI) { return new ShirrakFleeFocusFireAction(botAI); } + + static Action* shirrak_ranged_keep_distance( + PlayerbotAI* botAI) { return new ShirrakRangedKeepDistanceAction(botAI); } +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h new file mode 100644 index 00000000000..95f15f16a93 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h @@ -0,0 +1,35 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERCONTEXT_H + +#include "AiObjectContext.h" +#include "TriggerContext.h" +#include "AuchenaiCryptsTriggers.h" + +class TbcDungeonAuchenaiCryptsTriggerContext : public NamedObjectContext +{ +public: + // Shirrak the Dead Watcher + TbcDungeonAuchenaiCryptsTriggerContext() + { + creators["shirrak tank position boss"] = + &TbcDungeonAuchenaiCryptsTriggerContext::shirrak_tank_position_boss; + + creators["shirrak flee focus fire"] = + &TbcDungeonAuchenaiCryptsTriggerContext::shirrak_flee_focus_fire; + + creators["shirrak ranged keep distance"] = + &TbcDungeonAuchenaiCryptsTriggerContext::shirrak_ranged_keep_distance; + } +private: + // Shirrak the Dead Watcher + static Trigger* shirrak_tank_position_boss( + PlayerbotAI* botAI) { return new ShirrakTankPositionBossTrigger(botAI); } + + static Trigger* shirrak_flee_focus_fire( + PlayerbotAI* botAI) { return new ShirrakFleeFocusFireTrigger(botAI); } + + static Trigger* shirrak_ranged_keep_distance( + PlayerbotAI* botAI) { return new ShirrakRangedKeepDistanceTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp new file mode 100644 index 00000000000..2f74a5a58e7 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp @@ -0,0 +1,45 @@ +#include "AuchenaiCryptsMultipliers.h" +#include "AuchenaiCryptsActions.h" +#include "AuchenaiCryptsTriggers.h" +#include "MovementActions.h" +#include "ReachTargetActions.h" +#include "FollowActions.h" +#include "AiObjectContext.h" +#include "Playerbots.h" + +// Shirrak the Dead Watcher + +// Flee from Focus Fire and dont run back in +float ShirrakFleeFocusFireMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "shirrak the dead watcher")) + return 1.0f; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast(AuchenaiCryptsIDs::NPC_FOCUS_FIRE), 20.0f); + + for (Creature* flare : creatureList) + { + if (flare && flare->IsAlive()) + { + if (dynamic_cast(action)) + return 0.0f; + + float currentDistance = bot->GetDistance2d(flare); + constexpr float safeDistance = 12.0f; + constexpr float buffer = 5.0f; + + if (currentDistance < safeDistance + buffer && ( + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + } + } + return 1.0f; +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h new file mode 100644 index 00000000000..df5de23189f --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h @@ -0,0 +1,13 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSMULTIPLIERS_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSMULTIPLIERS_H + +#include "Multiplier.h" + +class ShirrakFleeFocusFireMultiplier : public Multiplier +{ +public: + ShirrakFleeFocusFireMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "shirrak flee focus fire") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp new file mode 100644 index 00000000000..f975d46bb60 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp @@ -0,0 +1,21 @@ +#include "AuchenaiCryptsTriggers.h" +#include "AuchenaiCryptsStrategy.h" +#include "AuchenaiCryptsMultipliers.h" + +void TbcDungeonAuchenaiCryptsStrategy::InitTriggers(std::vector& triggers) +{ + // Shirrak The Dead Watcher + triggers.push_back(new TriggerNode("shirrak tank position boss", { + NextAction("shirrak tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("shirrak flee focus fire", { + NextAction("shirrak flee focus fire", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("shirrak ranged keep distance", { + NextAction("shirrak ranged keep distance", ACTION_RAID + 1) })); +} + +void TbcDungeonAuchenaiCryptsStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new ShirrakFleeFocusFireMultiplier(botAI)); +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h new file mode 100644 index 00000000000..ff82a0266f0 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h @@ -0,0 +1,19 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSSTRATEGY_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSSTRATEGY_H + +#include "AiObjectContext.h" +#include "Strategy.h" +#include "Multiplier.h" + +class TbcDungeonAuchenaiCryptsStrategy : public Strategy +{ +public: + TbcDungeonAuchenaiCryptsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + virtual std::string const getName() override { return "tbc-ac"; } + + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp new file mode 100644 index 00000000000..372614d2f1d --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp @@ -0,0 +1,34 @@ +#include "Playerbots.h" +#include "AuchenaiCryptsTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + +// Shirrak the Dead Watcher + +bool ShirrakTankPositionBossTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "shirrak the dead watcher"); +} + +bool ShirrakFleeFocusFireTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "shirrak the dead watcher")) + return false; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast(AuchenaiCryptsIDs::NPC_FOCUS_FIRE), 20.0f); + + for (Creature* flare : creatureList) + { + if (flare && flare->IsAlive()) + return true; + } + return false; +} + +bool ShirrakRangedKeepDistanceTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "shirrak the dead watcher"); +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h new file mode 100644 index 00000000000..1d314419421 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h @@ -0,0 +1,38 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERS_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERS_H + +#include "Trigger.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum class AuchenaiCryptsIDs : uint32 +{ + // Shirrak The Dead Watcher + NPC_FOCUS_FIRE = 18374, +}; + +class ShirrakTankPositionBossTrigger : public Trigger +{ +public: + ShirrakTankPositionBossTrigger(PlayerbotAI* botAI) : Trigger(botAI, "shirrak tank position boss") {} + + bool IsActive() override; +}; + +class ShirrakFleeFocusFireTrigger : public Trigger +{ +public: + ShirrakFleeFocusFireTrigger(PlayerbotAI* botAI) : Trigger(botAI, "shirrak flee focus fire") {} + + bool IsActive() override; +}; + +class ShirrakRangedKeepDistanceTrigger : public Trigger +{ +public: + ShirrakRangedKeepDistanceTrigger(PlayerbotAI* botAI) : Trigger(botAI, "shirrak ranged keep distance") {} + + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Dungeon/DungeonStrategyContext.h b/src/Ai/Dungeon/DungeonStrategyContext.h index 3311aeee240..07e5fe505a4 100644 --- a/src/Ai/Dungeon/DungeonStrategyContext.h +++ b/src/Ai/Dungeon/DungeonStrategyContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H #include "Strategy.h" +#include "AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h" #include "UtgardeKeep/Strategy/UtgardeKeepStrategy.h" #include "Nexus/Strategy/NexusStrategy.h" #include "AzjolNerub/Strategy/AzjolNerubStrategy.h" @@ -44,7 +45,7 @@ class DungeonStrategyContext : public NamedObjectContext // ... // Burning Crusade - // ... + creators["tbc-ac"] = &DungeonStrategyContext::tbc_ac; // Auchindoun: Auchenai Crypts // Wrath of the Lich King creators["wotlk-uk"] = &DungeonStrategyContext::wotlk_uk; // Utgarde Keep @@ -65,6 +66,7 @@ class DungeonStrategyContext : public NamedObjectContext creators["wotlk-fos"] = &DungeonStrategyContext::wotlk_fos; // The Forge of Souls } private: + static Strategy* tbc_ac(PlayerbotAI* botAI) { return new TbcDungeonAuchenaiCryptsStrategy(botAI); } static Strategy* wotlk_uk(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } static Strategy* wotlk_nex(PlayerbotAI* botAI) { return new WotlkDungeonNexStrategy(botAI); } static Strategy* wotlk_an(PlayerbotAI* botAI) { return new WotlkDungeonANStrategy(botAI); } diff --git a/src/Ai/Dungeon/TbcDungeonActionContext.h b/src/Ai/Dungeon/TbcDungeonActionContext.h new file mode 100644 index 00000000000..8c35472248b --- /dev/null +++ b/src/Ai/Dungeon/TbcDungeonActionContext.h @@ -0,0 +1,6 @@ +#ifndef _PLAYERBOT_TBCDUNGEONACTIONCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONACTIONCONTEXT_H + +#include "AuchenaiCrypts/AuchenaiCryptsActionContext.h" + +#endif diff --git a/src/Ai/Dungeon/TbcDungeonTriggerContext.h b/src/Ai/Dungeon/TbcDungeonTriggerContext.h new file mode 100644 index 00000000000..9a680b7af15 --- /dev/null +++ b/src/Ai/Dungeon/TbcDungeonTriggerContext.h @@ -0,0 +1,6 @@ +#ifndef _PLAYERBOT_TBCDUNGEONTRIGGERCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONTRIGGERCONTEXT_H + +#include "AuchenaiCrypts/AuchenaiCryptsTriggerContext.h" + +#endif diff --git a/src/Bot/Engine/BuildSharedActionContexts.cpp b/src/Bot/Engine/BuildSharedActionContexts.cpp index 8fbb6c13532..7e243eadb2f 100644 --- a/src/Bot/Engine/BuildSharedActionContexts.cpp +++ b/src/Bot/Engine/BuildSharedActionContexts.cpp @@ -18,6 +18,7 @@ #include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" #include "Ai/Raid/Icecrown/RaidIccActionContext.h" +#include "Ai/Dungeon/TbcDungeonActionContext.h" #include "Ai/Dungeon/WotlkDungeonActionContext.h" void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) @@ -41,6 +42,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& triggerContexts) @@ -41,6 +42,7 @@ void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList allInstanceStrategies = { "aq20", "bwl", "karazhan", "gruulslair", "icc", "magtheridon", "moltencore", - "naxx", "onyxia", "ssc", "tempestkeep", "ulduar", "voa", "wotlk-an", "wotlk-cos", + "naxx", "onyxia", "ssc", "tbc-ac", "tempestkeep", "ulduar", "voa", "wotlk-an", "wotlk-cos", "wotlk-dtk", "wotlk-eoe", "wotlk-fos", "wotlk-gd", "wotlk-hol", "wotlk-hor", "wotlk-hos", "wotlk-nex", "wotlk-occ", "wotlk-ok", "wotlk-os", "wotlk-pos", "wotlk-toc", "wotlk-uk", "wotlk-up", "wotlk-vh", "zulaman" @@ -1600,6 +1600,9 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) case 550: strategyName = "tempestkeep"; // Tempest Keep break; + case 558: + strategyName = "tbc-ac"; // Auchindoun: Auchenai Crypts + break; case 565: strategyName = "gruulslair"; // Gruul's Lair break; From 937b4903bba4123536480481d0997d16b9ba15d0 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 17 Apr 2026 13:26:29 -0500 Subject: [PATCH 67/87] Fix Potential Dereference in AttackAction (#2308) ## Pull Request Description AttackAction::Attack() uses target before checking it. This has never historically been a problem for me, but yesterday it was somehow causing me to crash every time I ordered a bot to attack. Rebuilding didn't solve the issue so it didn't seem to be a bad build. The problem was fixed by moving the target check to the beginning of the function. I restored the function to its existing ordering today and tested again, and somehow I don't crash anymore regardless. I'm confused as hell, but regardless, this is a fix that should be made. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes Order bots to "attack" a target. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) I had GPT-5.4 try to help me identify the source of the crash. I couldn't trace it to any particular PR, but it did identify the issue that is the subject of this PR. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Base/Actions/AttackAction.cpp | 31 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Ai/Base/Actions/AttackAction.cpp b/src/Ai/Base/Actions/AttackAction.cpp index 96bf5c4d327..af964f3608e 100644 --- a/src/Ai/Base/Actions/AttackAction.cpp +++ b/src/Ai/Base/Actions/AttackAction.cpp @@ -53,34 +53,27 @@ bool AttackMyTargetAction::Execute(Event /*event*/) bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) { - Unit* oldTarget = context->GetValue("current target")->Get(); - bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); - - bool sameTarget = oldTarget == target && bot->GetVictim() == target; - bool inCombat = botAI->GetState() == BOT_STATE_COMBAT; - bool sameAttackMode = bot->HasUnitState(UNIT_STATE_MELEE_ATTACKING) == shouldMelee; - - if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FLIGHT_MOTION_TYPE || - bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) + if (!target) { if (verbose) - botAI->TellError("I cannot attack in flight"); + botAI->TellError("I have no target"); return false; } - if (!target) + if (!target->IsInWorld()) { if (verbose) - botAI->TellError("I have no target"); + botAI->TellError(std::string(target->GetName()) + " is no longer in the world."); return false; } - if (!target->IsInWorld()) + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FLIGHT_MOTION_TYPE || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) { if (verbose) - botAI->TellError(std::string(target->GetName()) + " is no longer in the world."); + botAI->TellError("I cannot attack in flight"); return false; } @@ -121,6 +114,13 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) return false; } + Unit* oldTarget = context->GetValue("current target")->Get(); + bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); + + bool sameTarget = oldTarget == target && bot->GetVictim() == target; + bool inCombat = botAI->GetState() == BOT_STATE_COMBAT; + bool sameAttackMode = bot->HasUnitState(UNIT_STATE_MELEE_ATTACKING) == shouldMelee; + if (sameTarget && inCombat && sameAttackMode) { if (verbose) @@ -146,8 +146,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) ObjectGuid guid = target->GetGUID(); bot->SetSelection(target->GetGUID()); - context->GetValue("old target")->Set(oldTarget); - + context->GetValue("old target")->Set(oldTarget); context->GetValue("current target")->Set(target); context->GetValue("available loot")->Get()->Add(guid); From ce1adebc789d8006a9a4535b15c96bf6f43a9802 Mon Sep 17 00:00:00 2001 From: Hokken Date: Fri, 17 Apr 2026 19:26:42 +0100 Subject: [PATCH 68/87] fix(Core): scope AddPlayerBot loading count to master account (#2307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `AddPlayerBot()` falsely rejects player bot additions with *"You have added too many bots (more than 40)"* even when the player has zero personal bots. This happens because the `MaxAddedBots` check at `PlayerbotMgr.cpp:124` adds `botLoading.size()` to the player's personal bot count: ```cpp uint32 count = mgr->GetPlayerbotsCount() + botLoading.size(); ``` `botLoading` is a `static std::unordered_set` on `PlayerbotHolder` — shared by both `PlayerbotMgr` (per-player) and `RandomPlayerbotMgr` (singleton). When `RandomPlayerbotMgr` loads random bots at startup (up to 60 per interval via `RandomBotsPerInterval`), their GUIDs go into the same global set. During the startup loading window, `botLoading.size()` can easily reach 100–300, far exceeding the default `MaxAddedBots = 40` limit. The result: any player who logs in during the random bot loading window and tries `.playerbot add ` gets blocked, even though the limit is intended to be per-player. ### How to reproduce 1. Set `AiPlayerbot.RandomBotAutologin = 1` (default) with 500 random bots 2. Start the server 3. Log in immediately while random bots are still loading 4. Run `.playerbot add ` for an offline character on your account 5. Get *"You have added too many bots (more than 40)"* despite having 0 personal bots 6. Wait 1–2 minutes for random bot loading to finish, try again — works ### Root cause - `PlayerbotHolder::botLoading` is declared `static` at `PlayerbotMgr.h:60`, so both `PlayerbotMgr` and `RandomPlayerbotMgr` share the same set - `AddPlayerBot()` inserts into `botLoading` at line 147 for ALL callers — both player-initiated adds (`masterAccountId > 0`) and random bot spawns (`masterAccountId = 0`) - The count check at line 124 uses `botLoading.size()` (the entire global set) instead of filtering to bots being loaded for the requesting player - The config comment confirms the intended scope: *"The maximum number of bots that a player can control simultaneously"* ## Fix Change `botLoading` from `unordered_set` to `unordered_map` where the value is the `masterAccountId` passed to `AddPlayerBot()`. Random bots are loaded with `masterAccountId = 0`. The count check now iterates the map and only counts entries matching the current player's `masterAccountId`: ```cpp uint32 loadingForMaster = 0; for (auto const& [guid, acctId] : botLoading) { if (acctId == masterAccountId) ++loadingForMaster; } uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster; ``` ### Callsite compatibility All 10 existing `botLoading` callsites were audited: | Callsite | Operation | Compatible | |----------|-----------|-----------| | `PlayerbotMgr.cpp:85` | `find()` by key | Yes | | `PlayerbotMgr.cpp:153` | `emplace()` (was `insert()`) | Changed | | `PlayerbotMgr.cpp:174` | `erase()` by key | Yes | | `PlayerbotMgr.cpp:209` | `erase()` by key | Yes | | `PlayerbotMgr.cpp:229` | `erase()` by key | Yes | | `PlayerbotMgr.cpp:1163` | `find()` by key | Yes | | `RandomPlayerbotMgr.cpp:429` | `empty()` | Yes | The six unchanged callsites use `find()`, `erase()`, and `empty()` which operate on keys identically for both `unordered_set` and `unordered_map`. ## Files changed | File | Change | |------|--------| | `src/Bot/PlayerbotMgr.h` | `botLoading` type: `unordered_set` → `unordered_map` | | `src/Bot/PlayerbotMgr.cpp` | Definition type updated, `insert` → `emplace` with `masterAccountId`, count check filters by `masterAccountId` | ## What is NOT changed - `MaxAddedBots` config key and default value (40) — unchanged - Random bot loading behavior — unchanged - The `botLoading.empty()` throttle in `RandomPlayerbotMgr` — unchanged - In-game group invite flow — unaffected (does not go through `AddPlayerBot`) - No new config keys, no schema changes, no API changes --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar Co-authored-by: Hokken Co-authored-by: Claude Opus 4.6 (1M context) --- src/Bot/PlayerbotMgr.cpp | 12 +++++++++--- src/Bot/PlayerbotMgr.h | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index c3b614a98fb..8327e2f3674 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -64,7 +64,7 @@ class BotInitGuard }; std::unordered_set BotInitGuard::botsBeingInitialized; -std::unordered_set PlayerbotHolder::botLoading; +std::unordered_map PlayerbotHolder::botLoading; PlayerbotHolder::PlayerbotHolder() : PlayerbotAIBase(false) {} class PlayerbotLoginQueryHolder : public LoginQueryHolder @@ -121,7 +121,13 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId LOG_DEBUG("playerbots", "PlayerbotMgr not found for master player with GUID: {}", masterPlayer->GetGUID().GetRawValue()); return; } - uint32 count = mgr->GetPlayerbotsCount() + botLoading.size(); + uint32 loadingForMaster = 0; + for (auto const& [guid, acctId] : botLoading) + { + if (acctId == masterAccountId) + ++loadingForMaster; + } + uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster; if (count >= PlayerbotAIConfig::instance().maxAddedBots) { allowed = false; @@ -144,7 +150,7 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId return; } - botLoading.insert(playerGuid); + botLoading.emplace(playerGuid, masterAccountId); // Always login in with world session to avoid race condition sWorld->AddQueryHolderCallback(CharacterDatabase.DelayQueryHolder(holder)) diff --git a/src/Bot/PlayerbotMgr.h b/src/Bot/PlayerbotMgr.h index b80f6f23638..316e34d47c0 100644 --- a/src/Bot/PlayerbotMgr.h +++ b/src/Bot/PlayerbotMgr.h @@ -57,7 +57,7 @@ class PlayerbotHolder : public PlayerbotAIBase virtual void OnBotLoginInternal(Player* const bot) = 0; PlayerBotMap playerBots; - static std::unordered_set botLoading; + static std::unordered_map botLoading; }; class PlayerbotMgr : public PlayerbotHolder From d01316fe645eb582224db8edd8987a63402ec053 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 17 Apr 2026 13:26:53 -0500 Subject: [PATCH 69/87] Exclude Isle of Quel'danas Areas From PvP (#2304) ## Pull Request Description Adds the vendor/quest hub areas of the Isle of Quel'danas to excluded PvP areas (Shattered Sun Staging Area, Sun's Reach Sanctum, Sun's Reach Harbor, Sun's Reach Armory). Otherwise, bots attack each other and piss off all the Shattered Sun Offensive guards and NPCs. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes Go to one of the above-mentioned areas while PvP flagged (or on a PvP server, like me). See if bots attack. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 6 +++--- src/PlayerbotAIConfig.cpp | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 88920a05041..d9f19474702 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -581,10 +581,10 @@ AiPlayerbot.AutoGearScoreLimit = 0 # Default: food, taxi, and raid are enabled AiPlayerbot.BotCheats = "food,taxi,raid" -# List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. +# List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. # While mod-playerbots does not restore removed attunement requirements, other mods, such as mod-individual-progression, may do so. # This is meant to exclude bots from such requirements. -# +# # Default: # Caverns of Time - Part 1 # - 10279, To The Master's Lair @@ -1319,7 +1319,7 @@ AiPlayerbot.DeleteRandomBotArenaTeams = 0 AiPlayerbot.PvpProhibitedZoneIds = "2255,656,2361,2362,2363,976,35,2268,3425,392,541,1446,3828,3712,3738,3565,3539,3623,4152,3988,4658,4284,4418,4436,4275,4323,4395,3703,4298,3951" # PvP Restricted Areas (bots don't pvp) -AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973" +AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973,4085,4086,4087,4088" # Improve reaction speeds in battlegrounds and arenas (may cause lag) AiPlayerbot.FastReactInBG = 1 diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 8c8343db2b6..32443c46d81 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -167,11 +167,13 @@ bool PlayerbotAIConfig::Initialize() pvpProhibitedZoneIds); LoadList>( sConfigMgr->GetOption("AiPlayerbot.PvpProhibitedAreaIds", - "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973"), + "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786," + "3973,4085,4086,4087,4088"), pvpProhibitedAreaIds); fastReactInBG = sConfigMgr->GetOption("AiPlayerbot.FastReactInBG", true); LoadList>( - sConfigMgr->GetOption("AiPlayerbot.RandomBotQuestIds", "3802,5505,6502,7761,7848,10277,10285,11492,13188,13189,24499,24511,24710,24712"), + sConfigMgr->GetOption("AiPlayerbot.RandomBotQuestIds", "3802,5505,6502,7761,7848,10277,10285,11492," + "13188,13189,24499,24511,24710,24712"), randomBotQuestIds); LoadSet>( @@ -181,7 +183,8 @@ bool PlayerbotAIConfig::Initialize() "165739,165738,175245,175970,176325,176327,123329,2560"), disallowedGameObjects); LoadSet>( - sConfigMgr->GetOption("AiPlayerbot.AttunementQuests", "10279,10277,10282,10283,10284,10285,10296,10297,10298,11481,11482,11488,11490,11492,10901,10888,10445,10985"), + sConfigMgr->GetOption("AiPlayerbot.AttunementQuests", "10279,10277,10282,10283,10284,10285,10296," + "10297,10298,11481,11482,11488,11490,11492,10901,10888,10445,10985"), attunementQuests); LoadSet>( From a34681bd7e9f55cfaebddc2eba41911e2381d051 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 17 Apr 2026 13:27:11 -0500 Subject: [PATCH 70/87] Modify UpdateAI to Allow Future Methods to Interrupt Bot Spells (#2295) Note: Resubmitted because the prior PR had master for the source ## Pull Request Description This PR adds to PlayerbotAI::UpdateAI a boolean variable, pendingCastInterrupt, and a public function, RequestCastInterrupt(), that toggles the variable and would then call InterruptSpells in UpdateAI. This lets an external script hook into UpdateAI to interrupt a spell that is in the process of being cast. This should allow raid/dungeon strategies to actually interrupt spells, such as for avoiding hazards, as you cannot do so by calling InterruptSpells() or Reset() or anything else with a raid/dungeon action method. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. There is no processing cost from this PR itself since it just adds a simple boolean check that will always return false unless other methods are implemented to call RequestCastInterrupt(). ## How to Test the Changes This PR doesn't do anything by itself, but confirmation of no performance impact can be tested by just playing with the PR merged. I tested this by introducing scripts that called RequestCastInterrupt() for Archimonde (included in the Hyjal PR now) and for Auchenai Crypts (built on the strategy from flashtate's PR just for test purposes), and spells were interrupted as intended. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [x] Yes (**explain below**) I'm not sure if it counts as "yes" here, but this PR opens up the ability for scripts to be added that would add real checks to UpdateAI. The impact of such scripts will depend on how they are implemented, however. It is possible (and intended) for the external calls to be highly limited in scope (e.g., only bots in a particular circumstance in a particular boss fight). ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Claude Sonnet 4.6 was used to brainstorm different possibilities to allow external scripts to interrupt spells. I considered different options before settling on this one due to it not requiring core changes, being easily usable in boss strategies, and not having any performance impact on its own. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/PlayerbotAI.cpp | 130 ++++++++++++++++++++++++++-------------- src/Bot/PlayerbotAI.h | 6 +- 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 744fd749737..4c1fbb53dc8 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -266,77 +266,104 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (!CanUpdateAI()) return; - // Handle the current spell + // Handle a spell that is still in its preparing phase (including channeled spells). Spell* currentSpell = bot->GetCurrentSpell(CURRENT_GENERIC_SPELL); if (!currentSpell) currentSpell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); if (currentSpell) { - const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); - if (spellInfo && currentSpell->getState() == SPELL_STATE_PREPARING) + if (currentSpell->getState() == SPELL_STATE_PREPARING) { - Unit* spellTarget = currentSpell->m_targets.GetUnitTarget(); - // Interrupt if target is dead or spell can't target dead units - if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) + // Allow external scripts to interrupt a cast in progress + if (spellInterruptRequested) { + spellInterruptRequested = false; InterruptSpell(); YieldThread(bot, GetReactDelay()); return; } - GameObject* goSpellTarget = currentSpell->m_targets.GetGOTarget(); - - if (goSpellTarget && !goSpellTarget->isSpawned()) + const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); + if (spellInfo) { - InterruptSpell(); - YieldThread(bot, GetReactDelay()); - return; - } + Unit* spellTarget = currentSpell->m_targets.GetUnitTarget(); + // Interrupt if target is dead or spell can't target dead units + if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) + { + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; + } - bool isHeal = false; - bool isSingleTarget = true; + GameObject* goSpellTarget = currentSpell->m_targets.GetGOTarget(); - for (uint8 i = 0; i < 3; ++i) - { - if (!spellInfo->Effects[i].Effect) - continue; + if (goSpellTarget && !goSpellTarget->isSpawned()) + { + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; + } + + bool isHeal = false; + bool isSingleTarget = true; - // Check if spell is a heal - if (spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL || - spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MAX_HEALTH || - spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MECHANICAL) - isHeal = true; - - // Check if spell is single-target - if ((spellInfo->Effects[i].TargetA.GetTarget() && - spellInfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ALLY) || - (spellInfo->Effects[i].TargetB.GetTarget() && - spellInfo->Effects[i].TargetB.GetTarget() != TARGET_UNIT_TARGET_ALLY)) + for (uint8 i = 0; i < 3; ++i) { - isSingleTarget = false; + if (!spellInfo->Effects[i].Effect) + continue; + + // Check if spell is a heal + if (spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL || + spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MAX_HEALTH || + spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MECHANICAL) + isHeal = true; + + // Check if spell is single-target + if ((spellInfo->Effects[i].TargetA.GetTarget() && + spellInfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ALLY) || + (spellInfo->Effects[i].TargetB.GetTarget() && + spellInfo->Effects[i].TargetB.GetTarget() != TARGET_UNIT_TARGET_ALLY)) + { + isSingleTarget = false; + } } - } - // Interrupt if target ally has full health (heal by other member) - if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) - { - InterruptSpell(); + // Interrupt if target ally has full health (heal by other member) + if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) + { + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; + } + + // Ensure bot is facing target if necessary + if (spellTarget && !bot->HasInArc(CAST_ANGLE_IN_FRONT, spellTarget) && + (spellInfo->FacingCasterFlags & SPELL_FACING_FLAG_INFRONT)) + { + ServerFacade::instance().SetFacingTo(bot, spellTarget); + } + + // Wait for spell cast YieldThread(bot, GetReactDelay()); return; } + } + } - // Ensure bot is facing target if necessary - if (spellTarget && !bot->HasInArc(CAST_ANGLE_IN_FRONT, spellTarget) && - (spellInfo->FacingCasterFlags & SPELL_FACING_FLAG_INFRONT)) - { - ServerFacade::instance().SetFacingTo(bot, spellTarget); - } - - // Wait for spell cast + if (spellInterruptRequested) + { + // At this point the preparing-cast branch above did not consume the request. + // Interrupt a current channel if one still exists; otherwise, clear the stale request. + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + { + spellInterruptRequested = false; + InterruptSpell(); YieldThread(bot, GetReactDelay()); return; } + + spellInterruptRequested = false; } // Handle transport check delay @@ -1598,7 +1625,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "ssc"; // Serpentshrine Cavern break; case 550: - strategyName = "tempestkeep"; // Tempest Keep + strategyName = "tempestkeep"; // Tempest Keep: The Eye break; case 558: strategyName = "tbc-ac"; // Auchindoun: Auchenai Crypts @@ -4192,6 +4219,19 @@ void PlayerbotAI::RemoveAura(std::string const name) bot->RemoveAurasDueToSpell(spellid); } +void PlayerbotAI::RequestSpellInterrupt() +{ + Spell* currentSpell = bot->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (currentSpell && currentSpell->getState() == SPELL_STATE_PREPARING) + { + spellInterruptRequested = true; + return; + } + + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + spellInterruptRequested = true; +} + bool PlayerbotAI::IsInterruptableSpellCasting(Unit* target, std::string const spell) { if (!IsValidUnit(target)) diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index cfa27ed4e88..5a0dc7485e6 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -3,8 +3,8 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_PLAYERbotAI_H -#define _PLAYERBOT_PLAYERbotAI_H +#ifndef _PLAYERBOT_PLAYERBOTAI_H +#define _PLAYERBOT_PLAYERBOTAI_H #include @@ -471,6 +471,7 @@ class PlayerbotAI : public PlayerbotAIBase void SpellInterrupted(uint32 spellid); int32 CalculateGlobalCooldown(uint32 spellid); void InterruptSpell(); + void RequestSpellInterrupt(); void RemoveAura(std::string const name); void RemoveShapeshift(); void WaitForSpellCast(Spell* spell); @@ -647,6 +648,7 @@ class PlayerbotAI : public PlayerbotAIBase BotCheatMask cheatMask = BotCheatMask::none; Position jumpDestination = Position(); uint32 nextTransportCheck = 0; + bool spellInterruptRequested = false; }; #endif From 6c517eb9d1e4f750bf9e3c5443e6295bc0fc46cc Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:27:44 -0600 Subject: [PATCH 71/87] Feat: Reintroduce timed logouts (#2289) ## Pull Request Description Timed logouts was enabled in the past, but was disabled due to a misdiagnosis on an issue that caused crashes. That issue was better understood and resolved in #2131. Now timed logouts are reintroduced for alt-bots and addclass-bots. As before, random-bots do not and should not accept logout commands. Note that if the bot's master has instant logout privileges according to `InstantLogout` in worldserver.conf, so would the bot. If not, neither would the bot. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. Feature at minimum logic needed to implement. No measurable processing cost. ## How to Test the Changes 1. Have your account gmlevel set to 2 in acore_auth>account_access just to test how it works with security parameters. 2. Set `InstantLogout = 3` in worldserver.conf. 3. Invite an addclass or alt-bot. 4. Bot should logout if whispered "logout" to or if you logout. 5. If not in a resting place, neither you nor the bot should be able to instant logout according to the security setting. 6. After the bot logout, log it back in and whisper "logout" again, but right after whisper "cancel logout" or "logout cancel". That should cancel the logout. 7. Have your account gmlevel set to 3 in acore_auth>account_access. (when you change your gmlevel, you need to log out of your account for the change to take effect) 8. You are now at admin gmlevel, and with `InstantLogout = 3`, you and any bot under your command should be able to logout instantly. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Bots now trigger instant or timed logout under the same circumstances that would apply to their master. - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [x] Yes (**explain below**) Checks if bot is eligible for instant logout or not. If not, timed logout applies to them. ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers While testing this I was having some odd issues with the command `account set gmlevel`. Not sure what's going on there, but for purposes of testing this and changing your account security level, doing it directly in the DB at "acore_auth>account_access" is going to be most reliable. --- src/Bot/PlayerbotAI.cpp | 47 ++++++++++---------------- src/Bot/PlayerbotMgr.cpp | 72 ++++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 4c1fbb53dc8..f02a79e70a1 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -243,10 +243,22 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) nextAICheckDelay = 0; // Early return if bot is in invalid state - if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || - bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) + if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->IsDuringRemoveFromWorld()) return; + // During timed logout countdown, cancel if bot enters combat (this cancellation is handled client-side for real players). + if (bot->GetSession()->isLogingOut()) + { + bool canLogoutInCombat = bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING); + if (bot->IsInCombat() && !canLogoutInCombat) + { + WorldPackets::Character::LogoutCancel cancelData = WorldPacket(CMSG_LOGOUT_CANCEL); + bot->GetSession()->HandleLogoutCancelOpcode(cancelData); + } + else + return; + } + // Handle cheat options (set bot health and power if cheats are enabled) if (bot->IsAlive() && (static_cast(GetCheat()) > 0 || static_cast(sPlayerbotAIConfig.botCheatMask) > 0)) @@ -715,30 +727,9 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr Reset(true); } - // TODO: missing implementation to port - /*else if (filtered == "logout") - { - if (!(bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut())) - { - if (type == CHAT_MSG_WHISPER) - TellPlayer(&fromPlayer, BOT_TEXT("logout_start")); - - if (master && master->GetPlayerbotMgr()) - SetShouldLogOut(true); - } - } - else if (filtered == "logout cancel") - { - if (bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut()) - { - if (type == CHAT_MSG_WHISPER) - TellPlayer(&fromPlayer, BOT_TEXT("logout_cancel")); - - WorldPacket p; - bot->GetSession()->HandleLogoutCancelOpcode(p); - SetShouldLogOut(false); - } - } + // Commented-out logout commands blocks removed from here and implemented in HandleCommand. + // Remaining is a commented-out action delay command block. + /* else if ((filtered.size() > 5) && (filtered.substr(0, 5) == "wait ") && (filtered.find("wait for attack") == std::string::npos)) { @@ -1084,7 +1075,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro TellMaster(message); } } - else if (filtered == "logout cancel") + else if (filtered == "cancel logout" || filtered == "logout cancel") { if (!bot->GetSession()->isLogingOut()) return; @@ -1100,9 +1091,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro bot->GetSession()->HandleLogoutCancelOpcode(data); } else - { chatCommands.push_back(ChatCommandHolder(filtered, fromPlayer, type)); - } } void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index 8327e2f3674..fd205fe2376 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -299,6 +299,11 @@ void PlayerbotHolder::LogoutAllBots() if (!botAI || botAI->IsRealPlayer()) continue; + // If bot is mid-countdown, cancel the timer so LogoutPlayerBot proceeds immediately. + WorldSession* session = bot->GetSession(); + if (session && session->isLogingOut()) + session->SetLogoutStartTime(0); + LogoutPlayerBot(bot->GetGUID()); } } @@ -361,36 +366,50 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) WorldSession* botWorldSessionPtr = bot->GetSession(); WorldSession* masterWorldSessionPtr = nullptr; + // If already in timed logout countdown, complete it once the 20-second timer expires. if (botWorldSessionPtr->isLogingOut()) + { + if (botWorldSessionPtr->ShouldLogOut(time(nullptr))) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "goodbye", "Goodbye!", {}); + botAI->TellMaster(message); + RemoveFromPlayerbotsMap(guid); + botWorldSessionPtr->LogoutPlayer(true); + delete botWorldSessionPtr; + } return; + } Player* master = botAI->GetMaster(); if (master) masterWorldSessionPtr = master->GetSession(); - // TODO: Review whether or not to implement timed logout. - // Unused block. Useful only for timed logout. -/* - // check for instant logout - bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr)); - - if (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) - logout = true; - - if (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer()) - logout = true; - - if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || - botWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)) - logout = true; + // Instant logout checking: + bool logout = + bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || + (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer()) || + // Master's socket is already gone (EXIT GAME -> EXIT NOW is the most typical cause). + // Force instant logout. Without this, the bot restarts its 20-second countdown and fires LogoutPlayer() 20 seconds + // after the master's Player object has been deleted, causing the bot's logout to crash on the now deleted master. + (masterWorldSessionPtr && masterWorldSessionPtr->IsSocketClosed()) || + (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) || + // If the bot's master has security clearance for `InstantLogout` in worldserver.conf, so does the bot. + (master && + (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || + master->HasUnitState(UNIT_STATE_IN_FLIGHT) || + (masterWorldSessionPtr && + masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))); + + if (!logout) + { + // Start the 20-second logout countdown. CancelLogout() can interrupt this. + WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); + botWorldSessionPtr->HandleLogoutRequestOpcode(data); + return; + } - if (master && - (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || master->HasUnitState(UNIT_STATE_IN_FLIGHT) || - (masterWorldSessionPtr && - masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))) - logout = true; -*/ - // Instant logout (the only option right now) { std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( "goodbye", "Goodbye!", {}); @@ -1478,6 +1497,15 @@ void PlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) { SetNextCheckDelay(sPlayerbotAIConfig.reactDelay); CheckTellErrors(elapsed); + + // Complete timed logouts for added bots once the 20-second countdown has elapsed. + std::vector expiredLogouts; + for (auto const& [botGuid, bot] : playerBots) + if (bot && bot->GetSession() && bot->GetSession()->ShouldLogOut(time(nullptr))) + expiredLogouts.push_back(botGuid); + + for (ObjectGuid const& guid : expiredLogouts) + LogoutPlayerBot(guid); } void PlayerbotMgr::HandleCommand(uint32 type, std::string const text) From c0c2b6ab5b501dcec4fad85d5ff3bf6ae9627e9e Mon Sep 17 00:00:00 2001 From: Scarecr0w12 Date: Fri, 17 Apr 2026 15:54:36 -0500 Subject: [PATCH 72/87] feat(Core/Playerbots): Initialize bot professions and specializations (#2287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description Initialize random bot professions from the factory using class-matching or weighted-random profession pairs, respect the active primary profession cap, and restore required profession tools during bot init/refresh. This PR also initializes profession specializations for eligible bots so crafted professions are not left in an unspecialized state after profession assignment. Supported specialization families include: - Alchemy: Transmute / Elixir / Potion - Engineering: Goblin / Gnomish - Leatherworking: Dragonscale / Elemental / Tribal - Tailoring: Spellfire / Mooncloth / Shadoweave - Blacksmithing: Armorsmith / Weaponsmith, plus Hammersmith / Axesmith / Swordsmith for eligible Weaponsmith bots Specialization choices are stored in bot values so they remain stable across later refreshes. Required tool items are also restored for relevant professions during maintenance. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Select one or two professions during factory initialization from a small weighted list. - Clamp the assigned professions to the configured primary profession limit. - Learn the profession starter spell and set skill to the bot’s profession cap. - For professions with supported specialization branches, assign exactly one valid specialization when the bot meets the same level/skill gates used by AzerothCore profession scripts. - Persist the specialization selection in stored bot values so the choice is stable and does not need to be recalculated repeatedly. - Restore missing profession tools only when the bot has the related profession and the tool is absent. - Describe the **processing cost** when this logic executes across many bots. - The added logic executes only during bot init/refresh, not as part of per-tick combat or trigger evaluation. - Runtime cost is limited to a few small switch statements, stored value lookups, spell checks, and item presence checks. - No expensive repeated searches, map scans, or per-trigger decision trees were added. - The design keeps specialization selection deterministic after first assignment by storing the result, avoiding repeated random branching later. ## How to Test the Changes 1. Build and restart the server with this branch. 2. Trigger random bot creation, refresh, or level-based reroll for multiple bots. 3. Verify in `Playerbots.log` that bots receive profession pairs and, when eligible, profession specializations. 4. Check that low-level bots do not receive specializations before the required thresholds. 5. Check that eligible bots do receive one specialization for supported profession families. 6. Verify that specialization choices remain stable across subsequent refreshes. 7. Verify that profession tools are restored when missing: - Mining Pick - Blacksmith Hammer - Arclight Spanner - Runed Arcanite Rod - Skinning Knife 8. For a few bots, inspect in game or via debug tooling that profession spells/specialization spells are present as expected. Expected behavior: - Bots receive professions that respect the configured primary profession limit. - Profession skill values are initialized to the level-based cap. - Eligible bots receive exactly one valid specialization for supported profession families. - Specialization assignments are logged and persist across refreshes. - Profession tools are restored only when required. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [x] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) Explanation: - The added work runs during initialization/refresh rather than normal per-tick behavior. - Logic is bounded, data-local, and based on direct skill/spell/value checks. - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Explanation: - Bots can now start with initialized professions, required tools, and eligible profession specializations instead of remaining partially configured or unspecialized. - Does this change add new decision branches or increase maintenance complexity? - - [ ] No - - [x] Yes (**explain below**) Explanation: - The factory now contains specialization assignment branches for supported profession families. - Complexity is intentionally limited to init-time switch-based logic with stored specialization values to preserve predictability. ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) AI assistance was used for: - code generation and refactoring in `PlayerbotFactory` - drafting and refining profession/specialization initialization logic - PR description preparation All generated and suggested code was reviewed, adjusted, built locally, and validated before submission. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers - Target branch is `test-staging`. - Profession/specialization logic is intentionally limited to init/refresh paths to avoid per-tick cost. - Specialization selections are stored to keep bot behavior stable across later refreshes. - Recent changes also add debug logging for assigned specializations and save the bot after specialization learning so assignments are visible and persisted. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 7 +- src/Bot/Factory/PlayerbotFactory.cpp | 595 ++++++++++++++++++++++++--- src/Bot/Factory/PlayerbotFactory.h | 82 ++++ src/PlayerbotAIConfig.cpp | 2 + src/PlayerbotAIConfig.h | 1 + 5 files changed, 620 insertions(+), 67 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index d9f19474702..b3a3d2ab046 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -651,9 +651,14 @@ AiPlayerbot.BotTaxiGapJitterMs = 100 #################################################################################################### # PROFESSIONS -# Note: Random bots currently do not get professions # +# Percentage of randombots in each class bucket that receive a class-matching +# weighted profession combination. The remaining randombots use the weighted +# random sane-pair profession pool. +# Default: 30 +AiPlayerbot.ClassMatchingProfessionChance = 30 + # Automatically adds the 'master fishing' strategy to bots that have the fishing skill when the bots master fishes. # Default: 1 (Enabled) AiPlayerbot.EnableFishingWithMaster = 1 diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 11f301feb4f..9dfae8987e5 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -5,6 +5,7 @@ #include "PlayerbotFactory.h" +#include #include #include "AccountMgr.h" @@ -47,10 +48,11 @@ static std::vector initSlotsOrder = {EQUIPMENT_SLOT_TRINKET1, EQUIPMENT_ EQUIPMENT_SLOT_LEGS, EQUIPMENT_SLOT_HANDS, EQUIPMENT_SLOT_NECK, EQUIPMENT_SLOT_BODY, EQUIPMENT_SLOT_WAIST, EQUIPMENT_SLOT_FEET, EQUIPMENT_SLOT_WRISTS, EQUIPMENT_SLOT_FINGER1, EQUIPMENT_SLOT_FINGER2, EQUIPMENT_SLOT_BACK}; -uint32 PlayerbotFactory::tradeSkills[] = {SKILL_ALCHEMY, SKILL_ENCHANTING, SKILL_SKINNING, SKILL_TAILORING, - SKILL_LEATHERWORKING, SKILL_ENGINEERING, SKILL_HERBALISM, SKILL_MINING, - SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_FIRST_AID, SKILL_FISHING, - SKILL_JEWELCRAFTING}; +uint32 PlayerbotFactory::tradeSkills[] = {SKILL_ALCHEMY, SKILL_ENCHANTING, SKILL_SKINNING, + SKILL_TAILORING, SKILL_LEATHERWORKING, SKILL_ENGINEERING, + SKILL_HERBALISM, SKILL_INSCRIPTION, SKILL_MINING, + SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_FIRST_AID, + SKILL_FISHING, SKILL_JEWELCRAFTING}; std::list PlayerbotFactory::classQuestIds; std::list PlayerbotFactory::specialQuestIds; @@ -58,6 +60,264 @@ std::vector PlayerbotFactory::enchantSpellIdCache; std::vector PlayerbotFactory::enchantGemIdCache; std::unordered_map> PlayerbotFactory::trainerIdCache; +bool PlayerbotFactory::IsPrimaryTradeSkill(uint16 skillId) +{ + SkillLineEntry const* skillLine = sSkillLineStore.LookupEntry(skillId); + return skillLine && skillLine->categoryId == SKILL_CATEGORY_PROFESSION; +} + +bool PlayerbotFactory::IsGatheringTradeSkill(uint16 skillId) +{ + switch (skillId) + { + case SKILL_HERBALISM: + case SKILL_MINING: + case SKILL_SKINNING: + return true; + default: + return false; + } +} + +bool PlayerbotFactory::IsCraftingTradeSkill(uint16 skillId) +{ + return IsPrimaryTradeSkill(skillId) && !IsGatheringTradeSkill(skillId); +} + +uint32 PlayerbotFactory::GetProfessionStarterSpell(uint16 skillId) +{ + static constexpr std::array, 14> ProfessionStarterSpells = {{ + {SKILL_ALCHEMY, 2259}, + {SKILL_BLACKSMITHING, 2018}, + {SKILL_COOKING, 2550}, + {SKILL_ENCHANTING, 7411}, + {SKILL_ENGINEERING, 4036}, + {SKILL_FIRST_AID, 3273}, + {SKILL_FISHING, 7620}, + {SKILL_HERBALISM, 2366}, + {SKILL_INSCRIPTION, 45357}, + {SKILL_JEWELCRAFTING, 25229}, + {SKILL_LEATHERWORKING, 2108}, + {SKILL_MINING, 2575}, + {SKILL_SKINNING, 8613}, + {SKILL_TAILORING, 3908} + }}; + + for (auto const& [professionSkill, starterSpell] : ProfessionStarterSpells) + { + if (professionSkill == skillId) + return starterSpell; + } + + return 0; +} + +std::vector PlayerbotFactory::GetClassProfessionPairs(Player* bot) +{ + switch (bot->getClass()) + { + case CLASS_WARRIOR: + return {{SKILL_MINING, SKILL_BLACKSMITHING, 45}, + {SKILL_MINING, SKILL_ENGINEERING, 30}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 15}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 10}}; + case CLASS_PALADIN: + return {{SKILL_MINING, SKILL_BLACKSMITHING, 45}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 30}, + {SKILL_MINING, SKILL_ENGINEERING, 15}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 10}}; + case CLASS_DEATH_KNIGHT: + return {{SKILL_MINING, SKILL_BLACKSMITHING, 45}, + {SKILL_MINING, SKILL_ENGINEERING, 35}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 20}}; + case CLASS_HUNTER: + return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 45}, + {SKILL_MINING, SKILL_ENGINEERING, 35}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 10}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 10}}; + case CLASS_ROGUE: + return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 35}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}, + {SKILL_MINING, SKILL_ENGINEERING, 25}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 10}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 5}}; + case CLASS_DRUID: + return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 35}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 35}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 20}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 10}}; + case CLASS_SHAMAN: + return {{SKILL_HERBALISM, SKILL_ALCHEMY, 35}, + {SKILL_SKINNING, SKILL_LEATHERWORKING, 25}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 25}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 15}}; + case CLASS_PRIEST: + return {{SKILL_TAILORING, SKILL_ENCHANTING, 45}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 30}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}}; + case CLASS_MAGE: + return {{SKILL_TAILORING, SKILL_ENCHANTING, 50}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 25}}; + case CLASS_WARLOCK: + default: + return {{SKILL_TAILORING, SKILL_ENCHANTING, 50}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 25}}; + } +} + +std::vector PlayerbotFactory::GetRandomProfessionPairs() +{ + return {{SKILL_MINING, SKILL_BLACKSMITHING, 20}, + {SKILL_MINING, SKILL_ENGINEERING, 18}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 16}, + {SKILL_SKINNING, SKILL_LEATHERWORKING, 18}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 18}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 14}, + {SKILL_TAILORING, SKILL_ENCHANTING, 10}, + {SKILL_HERBALISM, SKILL_MINING, 6}, + {SKILL_HERBALISM, SKILL_SKINNING, 5}, + {SKILL_MINING, SKILL_SKINNING, 5}}; +} + +std::pair PlayerbotFactory::ChooseProfessionPair( + std::vector const& professionPairs) +{ + uint32 totalWeight = 0; + for (WeightedProfessionPair const& pair : professionPairs) + totalWeight += pair.weight; + + if (!totalWeight) + return {SKILL_HERBALISM, SKILL_ALCHEMY}; + + uint32 roll = urand(1, totalWeight); + for (WeightedProfessionPair const& pair : professionPairs) + { + if (roll <= pair.weight) + return {pair.firstSkill, pair.secondSkill}; + + roll -= pair.weight; + } + + WeightedProfessionPair const& fallback = professionPairs.back(); + return {fallback.firstSkill, fallback.secondSkill}; +} + +bool PlayerbotFactory::HasProfessionPair(std::vector const& professionPairs, + uint16 firstSkill, uint16 secondSkill) +{ + for (WeightedProfessionPair const& pair : professionPairs) + { + if (pair.firstSkill == firstSkill && pair.secondSkill == secondSkill) + return true; + } + + return false; +} + +uint16 PlayerbotFactory::ChooseSingleProfession(std::vector const& professionPairs) +{ + std::vector> gatheringSkills; + std::vector> craftingSkills; + + auto addWeightedSkill = [](std::vector>& skills, uint16 skillId, uint32 weight) + { + for (std::pair& skill : skills) + { + if (skill.first == skillId) + { + skill.second += weight; + return; + } + } + + skills.push_back({skillId, weight}); + }; + + for (WeightedProfessionPair const& pair : professionPairs) + { + if (IsGatheringTradeSkill(pair.firstSkill)) + addWeightedSkill(gatheringSkills, pair.firstSkill, pair.weight); + if (IsCraftingTradeSkill(pair.firstSkill)) + addWeightedSkill(craftingSkills, pair.firstSkill, pair.weight); + + if (IsGatheringTradeSkill(pair.secondSkill)) + addWeightedSkill(gatheringSkills, pair.secondSkill, pair.weight); + if (IsCraftingTradeSkill(pair.secondSkill)) + addWeightedSkill(craftingSkills, pair.secondSkill, pair.weight); + } + + std::vector>* selectedPool = nullptr; + if (!gatheringSkills.empty() && !craftingSkills.empty()) + selectedPool = urand(0, 1) == 0 ? &gatheringSkills : &craftingSkills; + else if (!gatheringSkills.empty()) + selectedPool = &gatheringSkills; + else if (!craftingSkills.empty()) + selectedPool = &craftingSkills; + + if (!selectedPool || selectedPool->empty()) + return SKILL_HERBALISM; + + uint32 totalWeight = 0; + for (std::pair const& skill : *selectedPool) + totalWeight += skill.second; + + if (!totalWeight) + return selectedPool->front().first; + + uint32 roll = urand(1, totalWeight); + for (std::pair const& skill : *selectedPool) + { + if (roll <= skill.second) + return skill.first; + + roll -= skill.second; + } + + return selectedPool->back().first; +} + +uint32 PlayerbotFactory::GetStoredOrRandomValue(Player* bot, + std::string const& key, + uint32 minValue, + uint32 maxValue) +{ + uint32 value = sRandomPlayerbotMgr.GetValue(bot, key); + if (value < minValue || value > maxValue) + { + value = urand(minValue, maxValue); + sRandomPlayerbotMgr.SetValue(bot, key, value); + } + + return value; +} + +bool PlayerbotFactory::HasAnySpell(Player* bot, std::vector const& spells) +{ + for (uint32 spellId : spells) + { + if (bot->HasSpell(spellId)) + return true; + } + + return false; +} + +bool PlayerbotFactory::LearnProfessionSpecialization(Player* bot, + ProfessionSpecializationSpell knownSpell, + ProfessionSpecializationSpell learnSpell) +{ + uint32 const knownSpellId = static_cast(knownSpell); + uint32 const learnSpellId = static_cast(learnSpell); + + if (bot->HasSpell(knownSpellId) || !sSpellMgr->GetSpellInfo(learnSpellId)) + return false; + + bot->CastSpell(bot, learnSpellId, true); + return bot->HasSpell(knownSpellId); +} + PlayerbotFactory::PlayerbotFactory(Player* bot, uint32 level, uint32 itemQuality, uint32 gearScoreLimit) : level(level), itemQuality(itemQuality), gearScoreLimit(gearScoreLimit), bot(bot) { @@ -2250,69 +2510,278 @@ bool PlayerbotFactory::CanEquipUnseenItem(uint8 slot, uint16& dest, uint32 item) void PlayerbotFactory::InitTradeSkills() { + if (!sRandomPlayerbotMgr.IsRandomBot(bot)) + return; + + uint32 const maxPrimaryTradeSkills = + std::min(2, sWorld->getIntConfig(CONFIG_MAX_PRIMARY_TRADE_SKILL)); + uint16 firstSkill = sRandomPlayerbotMgr.GetValue(bot, "firstSkill"); uint16 secondSkill = sRandomPlayerbotMgr.GetValue(bot, "secondSkill"); - if (!firstSkill || !secondSkill) + ProfessionRollType professionRollType = + static_cast(sRandomPlayerbotMgr.GetValue(bot, "professionRollType")); + + if (professionRollType != ProfessionRollType::Class && professionRollType != ProfessionRollType::Random) { - std::vector firstSkills; - std::vector secondSkills; + professionRollType = urand(1, 100) <= sPlayerbotAIConfig.classMatchingProfessionChance + ? ProfessionRollType::Class + : ProfessionRollType::Random; + sRandomPlayerbotMgr.SetValue(bot, "professionRollType", static_cast(professionRollType)); + } - switch (bot->getClass()) - { - case CLASS_WARRIOR: - case CLASS_PALADIN: - case CLASS_DEATH_KNIGHT: - firstSkills.push_back(SKILL_MINING); - secondSkills.push_back(SKILL_BLACKSMITHING); - secondSkills.push_back(SKILL_ENGINEERING); - secondSkills.push_back(SKILL_JEWELCRAFTING); - break; - case CLASS_SHAMAN: - case CLASS_DRUID: - case CLASS_HUNTER: - case CLASS_ROGUE: - firstSkills.push_back(SKILL_SKINNING); - secondSkills.push_back(SKILL_LEATHERWORKING); - break; - default: - firstSkills.push_back(SKILL_TAILORING); - secondSkills.push_back(SKILL_ENCHANTING); - } + std::vector professionPairs = professionRollType == ProfessionRollType::Class + ? GetClassProfessionPairs(bot) + : GetRandomProfessionPairs(); - switch (urand(0, 6)) + bool const hasStoredProfessionPair = firstSkill && secondSkill && firstSkill != secondSkill && + IsPrimaryTradeSkill(firstSkill) && IsPrimaryTradeSkill(secondSkill) && + HasProfessionPair(professionPairs, firstSkill, secondSkill); + bool const keepExistingProfessionPair = maxPrimaryTradeSkills < 2 && hasStoredProfessionPair; + + if (maxPrimaryTradeSkills == 1 && !keepExistingProfessionPair) + { + if (!IsPrimaryTradeSkill(firstSkill) || secondSkill != 0) { - case 0: - firstSkill = SKILL_HERBALISM; - secondSkill = SKILL_ALCHEMY; - break; - case 1: - firstSkill = SKILL_HERBALISM; - secondSkill = SKILL_MINING; - break; - case 2: - firstSkill = SKILL_MINING; - secondSkill = SKILL_SKINNING; - break; - case 3: - firstSkill = SKILL_HERBALISM; - secondSkill = SKILL_SKINNING; - break; - default: - firstSkill = firstSkills[urand(0, firstSkills.size() - 1)]; - secondSkill = secondSkills[urand(0, secondSkills.size() - 1)]; - break; + firstSkill = ChooseSingleProfession(professionPairs); + secondSkill = 0; + + sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill); + sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill); } + } + else if (maxPrimaryTradeSkills == 0 && !keepExistingProfessionPair) + { + firstSkill = 0; + secondSkill = 0; sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill); sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill); } + if (maxPrimaryTradeSkills >= 2 && + (!firstSkill || !secondSkill || firstSkill == secondSkill || !IsPrimaryTradeSkill(firstSkill) || + !IsPrimaryTradeSkill(secondSkill) || !HasProfessionPair(professionPairs, firstSkill, secondSkill))) + { + auto const& professionPair = ChooseProfessionPair(professionPairs); + firstSkill = professionPair.first; + secondSkill = professionPair.second; + + sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill); + sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill); + } + + std::vector primarySkills; + if (keepExistingProfessionPair) + { + primarySkills.push_back(firstSkill); + primarySkills.push_back(secondSkill); + } + else if (maxPrimaryTradeSkills > 0) + primarySkills.push_back(firstSkill); + if (!keepExistingProfessionPair && maxPrimaryTradeSkills > 1) + primarySkills.push_back(secondSkill); + SetRandomSkill(SKILL_FIRST_AID); SetRandomSkill(SKILL_FISHING); SetRandomSkill(SKILL_COOKING); - SetRandomSkill(firstSkill); - SetRandomSkill(secondSkill); + for (uint16 skillId : primarySkills) + SetRandomSkill(skillId); + + std::vector skillsToLearn = {SKILL_FIRST_AID, SKILL_FISHING, SKILL_COOKING}; + skillsToLearn.insert(skillsToLearn.end(), primarySkills.begin(), primarySkills.end()); + + for (uint16 skillId : skillsToLearn) + { + uint32 spellId = GetProfessionStarterSpell(skillId); + if (!spellId || bot->HasSpell(spellId)) + continue; + + if (IsPrimaryTradeSkill(skillId) && !bot->GetFreePrimaryProfessionPoints() && + !(keepExistingProfessionPair && bot->HasSkill(skillId))) + continue; + + bot->learnSpell(spellId, false); + } + + InitTradeSpecializations(); +} + +void PlayerbotFactory::InitTradeSpecializations() +{ + InitAlchemySpecialization(); + InitEngineeringSpecialization(); + InitLeatherworkingSpecialization(); + InitTailoringSpecialization(); + InitBlacksmithingSpecialization(); +} + +bool PlayerbotFactory::InitAlchemySpecialization() +{ + if (!bot->HasSkill(SKILL_ALCHEMY) || + bot->GetBaseSkillValue(SKILL_ALCHEMY) < 325 || + bot->GetLevel() <= 67) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Transmute), + static_cast(ProfessionSpecializationSpell::Elixir), + static_cast(ProfessionSpecializationSpell::Potion)})) + return false; + + switch (GetStoredOrRandomValue(bot, "alchemySpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Transmute, + ProfessionSpecializationSpell::LearnTransmute); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Elixir, + ProfessionSpecializationSpell::LearnElixir); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Potion, + ProfessionSpecializationSpell::LearnPotion); + } +} + +bool PlayerbotFactory::InitEngineeringSpecialization() +{ + if (!bot->HasSkill(SKILL_ENGINEERING) || + bot->GetBaseSkillValue(SKILL_ENGINEERING) < 200 || + bot->GetLevel() < 30) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Goblin), + static_cast(ProfessionSpecializationSpell::Gnomish)})) + return false; + + switch (GetStoredOrRandomValue(bot, "engineeringSpecialization", 1, 2)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Goblin, + ProfessionSpecializationSpell::LearnGoblin); + case 2: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Gnomish, + ProfessionSpecializationSpell::LearnGnomish); + } +} + +bool PlayerbotFactory::InitLeatherworkingSpecialization() +{ + if (!bot->HasSkill(SKILL_LEATHERWORKING) || + bot->GetBaseSkillValue(SKILL_LEATHERWORKING) < 225 || + bot->GetLevel() <= 40) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Dragon), + static_cast(ProfessionSpecializationSpell::Elemental), + static_cast(ProfessionSpecializationSpell::Tribal)})) + return false; + + switch (GetStoredOrRandomValue(bot, "leatherSpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Dragon, + ProfessionSpecializationSpell::LearnDragon); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Elemental, + ProfessionSpecializationSpell::LearnElemental); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Tribal, + ProfessionSpecializationSpell::LearnTribal); + } +} + +bool PlayerbotFactory::InitTailoringSpecialization() +{ + if (!bot->HasSkill(SKILL_TAILORING) || + bot->GetBaseSkillValue(SKILL_TAILORING) < 350 || + bot->GetLevel() <= 59) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Spellfire), + static_cast(ProfessionSpecializationSpell::Mooncloth), + static_cast(ProfessionSpecializationSpell::Shadoweave)})) + return false; + + switch (GetStoredOrRandomValue(bot, "tailorSpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Spellfire, + ProfessionSpecializationSpell::LearnSpellfire); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Mooncloth, + ProfessionSpecializationSpell::LearnMooncloth); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Shadoweave, + ProfessionSpecializationSpell::LearnShadoweave); + } +} + +bool PlayerbotFactory::InitBlacksmithingSpecialization() +{ + bool learnedSpecialization = false; + + if (!bot->HasSkill(SKILL_BLACKSMITHING) || + bot->GetBaseSkillValue(SKILL_BLACKSMITHING) < 225) + return false; + + if (!bot->HasSpell(static_cast(ProfessionSpecializationSpell::Armor)) && + !bot->HasSpell(static_cast(ProfessionSpecializationSpell::Weapon))) + { + switch (GetStoredOrRandomValue(bot, "blacksmithSpecialization", 1, 2)) + { + case 1: + learnedSpecialization = LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Armor, + ProfessionSpecializationSpell::LearnArmor); + break; + case 2: + default: + learnedSpecialization = LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Weapon, + ProfessionSpecializationSpell::LearnWeapon); + break; + } + } + + if (!bot->HasSpell(static_cast(ProfessionSpecializationSpell::Weapon)) || + bot->GetBaseSkillValue(SKILL_BLACKSMITHING) < 250 || + bot->GetLevel() <= 49 || + HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Hammer), + static_cast(ProfessionSpecializationSpell::Axe), + static_cast(ProfessionSpecializationSpell::Sword)})) + return learnedSpecialization; + + switch (GetStoredOrRandomValue(bot, "blacksmithWeaponSpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Hammer, + ProfessionSpecializationSpell::LearnHammer); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Axe, + ProfessionSpecializationSpell::LearnAxe); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Sword, + ProfessionSpecializationSpell::LearnSword); + } } void PlayerbotFactory::UpdateTradeSkills() @@ -2456,6 +2925,9 @@ void PlayerbotFactory::InitSkills() break; } + InitTradeSkills(); + InitInventorySkill(); + // switch (bot->getClass()) // { // case CLASS_WARRIOR: @@ -3804,30 +4276,21 @@ void PlayerbotFactory::InitInventory() void PlayerbotFactory::InitInventorySkill() { - if (bot->HasSkill(SKILL_MINING)) - { + if (bot->HasSkill(SKILL_MINING) && !bot->HasItemCount(2901, 1, true)) StoreItem(2901, 1); // Mining Pick - } - if (bot->HasSkill(SKILL_BLACKSMITHING) || bot->HasSkill(SKILL_ENGINEERING)) - { + if ((bot->HasSkill(SKILL_BLACKSMITHING) || bot->HasSkill(SKILL_ENGINEERING)) && + !bot->HasItemCount(5956, 1, true)) StoreItem(5956, 1); // Blacksmith Hammer - } - if (bot->HasSkill(SKILL_ENGINEERING)) - { + if (bot->HasSkill(SKILL_ENGINEERING) && !bot->HasItemCount(6219, 1, true)) StoreItem(6219, 1); // Arclight Spanner - } - if (bot->HasSkill(SKILL_ENCHANTING)) - { + if (bot->HasSkill(SKILL_ENCHANTING) && !bot->HasItemCount(16207, 1, true)) StoreItem(16207, 1); // Runed Arcanite Rod - } - if (bot->HasSkill(SKILL_SKINNING)) - { + if (bot->HasSkill(SKILL_SKINNING) && !bot->HasItemCount(7005, 1, true)) StoreItem(7005, 1); // Skinning Knife - } } Item* PlayerbotFactory::StoreItem(uint32 itemId, uint32 count) diff --git a/src/Bot/Factory/PlayerbotFactory.h b/src/Bot/Factory/PlayerbotFactory.h index d943463eaa9..1962c0428c5 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -6,6 +6,9 @@ #ifndef _PLAYERBOT_PLAYERBOTFACTORY_H #define _PLAYERBOT_PLAYERBOTFACTORY_H +#include +#include + #include "InventoryAction.h" #include "Player.h" #include "PlayerbotAI.h" @@ -87,12 +90,91 @@ class PlayerbotFactory void InitAttunementQuests(); private: + enum class ProfessionSpecializationSpell : uint32 + { + Weapon = 9787, + Armor = 9788, + Hammer = 17040, + Axe = 17041, + Sword = 17039, + + LearnWeapon = 9789, + LearnArmor = 9790, + LearnHammer = 39099, + LearnAxe = 39098, + LearnSword = 39097, + + Dragon = 10656, + Elemental = 10658, + Tribal = 10660, + + LearnDragon = 10657, + LearnElemental = 10659, + LearnTribal = 10661, + + Spellfire = 26797, + Mooncloth = 26798, + Shadoweave = 26801, + + Goblin = 20222, + Gnomish = 20219, + + LearnGoblin = 20221, + LearnGnomish = 20220, + + LearnSpellfire = 26796, + LearnMooncloth = 26799, + LearnShadoweave = 26800, + + Transmute = 28672, + Elixir = 28677, + Potion = 28675, + + LearnTransmute = 28674, + LearnElixir = 28678, + LearnPotion = 28676 + }; + + enum class ProfessionRollType : uint32 + { + Random = 1, + Class = 2 + }; + + struct WeightedProfessionPair + { + uint16 firstSkill; + uint16 secondSkill; + uint32 weight; + }; + void Prepare(); // void InitSecondEquipmentSet(); // void InitEquipmentNew(bool incremental); bool CanEquipItem(ItemTemplate const* proto); bool CanEquipUnseenItem(uint8 slot, uint16& dest, uint32 item); + static bool IsPrimaryTradeSkill(uint16 skillId); + static bool IsGatheringTradeSkill(uint16 skillId); + static bool IsCraftingTradeSkill(uint16 skillId); + static uint32 GetProfessionStarterSpell(uint16 skillId); + static std::vector GetClassProfessionPairs(Player* bot); + static std::vector GetRandomProfessionPairs(); + static std::pair ChooseProfessionPair(std::vector const& professionPairs); + static bool HasProfessionPair(std::vector const& professionPairs, + uint16 firstSkill, uint16 secondSkill); + static uint16 ChooseSingleProfession(std::vector const& professionPairs); + static uint32 GetStoredOrRandomValue(Player* bot, std::string const& key, uint32 minValue, uint32 maxValue); + static bool HasAnySpell(Player* bot, std::vector const& spells); + static bool LearnProfessionSpecialization(Player* bot, + ProfessionSpecializationSpell knownSpell, + ProfessionSpecializationSpell learnSpell); void InitTradeSkills(); + void InitTradeSpecializations(); + bool InitAlchemySpecialization(); + bool InitEngineeringSpecialization(); + bool InitLeatherworkingSpecialization(); + bool InitTailoringSpecialization(); + bool InitBlacksmithingSpecialization(); void UpdateTradeSkills(); void SetRandomSkill(uint16 id); void ClearSpells(); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 32443c46d81..a8daf972a9a 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -236,6 +236,8 @@ bool PlayerbotAIConfig::Initialize() EnableICCBuffs = sConfigMgr->GetOption("AiPlayerbot.EnableICCBuffs", true); //////////////////////////// Professions + classMatchingProfessionChance = + std::min(100, sConfigMgr->GetOption("AiPlayerbot.ClassMatchingProfessionChance", 30)); fishingDistanceFromMaster = sConfigMgr->GetOption("AiPlayerbot.FishingDistanceFromMaster", 10.0f); endFishingWithMaster = sConfigMgr->GetOption("AiPlayerbot.EndFishingWithMaster", 30.0f); fishingDistance = sConfigMgr->GetOption("AiPlayerbot.FishingDistance", 40.0f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 4e758e79e93..8776726789d 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -152,6 +152,7 @@ class PlayerbotAIConfig // Professions bool enableFishingWithMaster; + uint32 classMatchingProfessionChance; float fishingDistanceFromMaster, fishingDistance, endFishingWithMaster; // chat From 665a702a65a3cd77157bfa1edf6d9f1d607b6ae4 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 17 Apr 2026 22:54:52 +0200 Subject: [PATCH 73/87] Worldbuff classic support (#2311) ## Pull Request Description Added support for flask and food for vanilla and TBC ## Vanilla | Class | Spec | Buff IDs | Buffs | | --- | --- | --- | --- | | Warrior | Arms | `17538`, `24799` | Elixir of the Mongoose; Smoked Desert Dumplings | | Warrior | Fury | `17538`, `24799` | Elixir of the Mongoose; Smoked Desert Dumplings | | Warrior | Protection | `17626`, `25661` | Flask of the Titans; Dirge's Kickin' Chimaerok Chops | | Paladin | Holy | `17627`, `18194` | Flask of Distilled Wisdom; Nightfin Soup | | Paladin | Protection | `17626`, `25661` | Flask of the Titans; Dirge's Kickin' Chimaerok Chops | | Paladin | Retribution | `17628`, `24799` | Flask of Supreme Power; Smoked Desert Dumplings | | Hunter | Beast Mastery | `17538`, `18192` | Elixir of the Mongoose; Grilled Squid | | Hunter | Marksmanship | `17538`, `18192` | Elixir of the Mongoose; Grilled Squid | | Hunter | Survival | `17538`, `18192` | Elixir of the Mongoose; Grilled Squid | | Rogue | Assassination | `17538`, `18192` | Elixir of the Mongoose; Grilled Squid | | Rogue | Combat | `17538`, `18192` | Elixir of the Mongoose; Grilled Squid | | Rogue | Subtlety | `17538`, `18192` | Elixir of the Mongoose; Grilled Squid | | Priest | Discipline | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Priest | Holy | `17627`, `18194` | Flask of Distilled Wisdom; Nightfin Soup | | Priest | Shadow | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Shaman | Elemental | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Shaman | Enhancement | `17538`, `24799` | Elixir of the Mongoose; Smoked Desert Dumplings | | Shaman | Restoration | `17627`, `18194` | Flask of Distilled Wisdom; Nightfin Soup | | Mage | Arcane | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Mage | Fire | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Mage | Frost | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Warlock | Affliction | `17628`, `25661` | Flask of Supreme Power; Dirge's Kickin' Chimaerok Chops | | Warlock | Demonology | `17628`, `25661` | Flask of Supreme Power; Dirge's Kickin' Chimaerok Chops | | Warlock | Destruction | `17628`, `25661` | Flask of Supreme Power; Dirge's Kickin' Chimaerok Chops | | Druid | Balance | `17628`, `18194` | Flask of Supreme Power; Nightfin Soup | | Druid | Feral Bear | `17626`, `25661` | Flask of the Titans; Dirge's Kickin' Chimaerok Chops | | Druid | Restoration | `17627`, `18194` | Flask of Distilled Wisdom; Nightfin Soup | | Druid | Feral Cat | `17538`, `24799` | Elixir of the Mongoose; Smoked Desert Dumplings | ## TBC | Class | Spec | Buff IDs | Buffs | | --- | --- | --- | --- | | Warrior | Arms | `28520`, `33256` | Flask of Relentless Assault; Roasted Clefthoof | | Warrior | Fury | `28520`, `33256` | Flask of Relentless Assault; Roasted Clefthoof | | Warrior | Protection | `28518`, `33257` | Flask of Fortification; Fisherman's Feast | | Paladin | Holy | `28491`, `39627`, `33263` | Elixir of Healing Power; Elixir of Draenic Wisdom; Blackened Basilisk | | Paladin | Protection | `28518`, `33257` | Flask of Fortification; Fisherman's Feast | | Paladin | Retribution | `28520`, `33256` | Flask of Relentless Assault; Roasted Clefthoof | | Hunter | Beast Mastery | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Hunter | Marksmanship | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Hunter | Survival | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Rogue | Assassination | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Rogue | Combat | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Rogue | Subtlety | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Priest | Discipline | `28491`, `39627`, `33263` | Elixir of Healing Power; Elixir of Draenic Wisdom; Blackened Basilisk | | Priest | Holy | `28491`, `39627`, `33263` | Elixir of Healing Power; Elixir of Draenic Wisdom; Blackened Basilisk | | Priest | Shadow | `28540`, `33263` | Flask of Pure Death; Blackened Basilisk | | Shaman | Elemental | `28521`, `33263` | Flask of Blinding Light; Blackened Basilisk | | Shaman | Enhancement | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | | Shaman | Restoration | `28491`, `39627`, `33263` | Elixir of Healing Power; Elixir of Draenic Wisdom; Blackened Basilisk | | Mage | Arcane | `28521`, `33263` | Flask of Blinding Light; Blackened Basilisk | | Mage | Fire | `28540`, `33263` | Flask of Pure Death; Blackened Basilisk | | Mage | Frost | `28540`, `33263` | Flask of Pure Death; Blackened Basilisk | | Warlock | Affliction | `28540`, `33263` | Flask of Pure Death; Blackened Basilisk | | Warlock | Demonology | `28540`, `33263` | Flask of Pure Death; Blackened Basilisk | | Warlock | Destruction | `28540`, `33263` | Flask of Pure Death; Blackened Basilisk | | Druid | Balance | `28521`, `33263` | Flask of Blinding Light; Blackened Basilisk | | Druid | Feral Bear | `28518`, `33257` | Flask of Fortification; Fisherman's Feast | | Druid | Restoration | `28491`, `39627`, `33263` | Elixir of Healing Power; Elixir of Draenic Wisdom; Blackened Basilisk | | Druid | Feral Cat | `28520`, `33261` | Flask of Relentless Assault; Warp Burger | ## How to Test the Changes 1. Invite bot with 60-79 level 2. Add him strategy `nc +worldbuff` 3. Bot should apply buffs ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Prepare list of buffs based on website and apply them to matrix. Randomly reviewed ids with guides and in-game ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers [VANILLA_CONSUMABLE_WORLD_BUFFS.md](https://github.com/user-attachments/files/26761020/VANILLA_CONSUMABLE_WORLD_BUFFS.md) [TBC_CONSUMABLE_WORLD_BUFFS.md](https://github.com/user-attachments/files/26761021/TBC_CONSUMABLE_WORLD_BUFFS.md) --- conf/playerbots.conf.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index b3a3d2ab046..5ce5485d094 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1802,10 +1802,10 @@ AiPlayerbot.PremadeSpecLink.11.6.80 = 05320021--230033312031500531353013251 # Requires sending the command "nc +worldbuff" in chat to a bot (or a group of bots) to enable # Each entry in the matrix should be formatted as follows: Entry:FactionID,ClassID,SpecID,MinimumLevel,MaximumLevel:SpellID1,SpellID2,etc.; # FactionID may be set to 0 for the entry to apply buffs to bots of either faction -# The default entries create a cross-faction list of level 80 buffs for each implemented pve spec from the "Premade Specs" section +# The default entries create a cross-faction level 60-69 Vanilla buffs, level 70-79 TBC buffs, and level 80 buffs for each implemented pve spec from the "Premade Specs" section # The default entries may be deleted or modified, and new custom entries may be added -AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIOR FURY 2:0,1,1,80,80:53760,57358; # WARRIOR PROTECTION 3:0,1,2,80,80:53758,57356; # PALADIN HOLY 4:0,2,0,80,80:53749,57332,60347; # PALADIN PROTECTION 5:0,2,1,80,80:53758,57356; # PALADIN RETRIBUTION 6:0,2,2,80,80:53760,57371; # HUNTER BEAST 7:0,3,0,80,80:53760,57325; # HUNTER MARKSMANSHIP 8:0,3,1,80,80:53760,57358; # HUNTER SURVIVAL 9:0,3,2,80,80:53760,57367; # ROGUE ASSASSINATION 10:0,4,0,80,80:53760,57325; # ROGUE COMBAT 11:0,4,1,80,80:53760,57358; # ROGUE SUBTLETY 12:0,4,2,80,80:53760,57367; # PRIEST DISCIPLINE 13:0,5,0,80,80:53755,57327; # PRIEST HOLY 14:0,5,1,80,80:53755,57327; # PRIEST SHADOW 15:0,5,2,80,80:53755,57327; # DEATH KNIGHT BLOOD 16:0,6,0,80,80:53758,57356; # DEATH KNIGHT FROST 17:0,6,1,80,80:53760,57358; # DEATH KNIGHT UNHOLY 18:0,6,2,80,80:53760,57358; # DEATH KNIGHT BLOOD DPS 19:0,6,3,80,80:53760,57371; # SHAMAN ELEMENTAL 20:0,7,0,80,80:53755,57327; # SHAMAN ENHANCEMENT 21:0,7,1,80,80:53760,57325; # SHAMAN RESTORATION 22:0,7,2,80,80:53755,57327; # MAGE ARCANE 23:0,8,0,80,80:53755,57327; # MAGE FIRE 24:0,8,1,80,80:53755,57327; # MAGE FROST 25:0,8,2,80,80:53755,57327; # WARLOCK AFFLICTION 26:0,9,0,80,80:53755,57327; # WARLOCK DEMONOLOGY 27:0,9,1,80,80:53755,57327; # WARLOCK DESTRUCTION 28:0,9,2,80,80:53755,57327; # DRUID BALANCE 29:0,11,0,80,80:53755,57327; # DRUID FERAL BEAR 30:0,11,1,80,80:53749,53763,57367; # DRUID RESTORATION 31:0,11,2,80,80:54212,57334; # DRUID FERAL CAT 32:0,11,3,80,80:53760,57358 +AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIOR FURY 2:0,1,1,80,80:53760,57358; # WARRIOR PROTECTION 3:0,1,2,80,80:53758,57356; # PALADIN HOLY 4:0,2,0,80,80:53749,57332,60347; # PALADIN PROTECTION 5:0,2,1,80,80:53758,57356; # PALADIN RETRIBUTION 6:0,2,2,80,80:53760,57371; # HUNTER BEAST 7:0,3,0,80,80:53760,57325; # HUNTER MARKSMANSHIP 8:0,3,1,80,80:53760,57358; # HUNTER SURVIVAL 9:0,3,2,80,80:53760,57367; # ROGUE ASSASSINATION 10:0,4,0,80,80:53760,57325; # ROGUE COMBAT 11:0,4,1,80,80:53760,57358; # ROGUE SUBTLETY 12:0,4,2,80,80:53760,57367; # PRIEST DISCIPLINE 13:0,5,0,80,80:53755,57327; # PRIEST HOLY 14:0,5,1,80,80:53755,57327; # PRIEST SHADOW 15:0,5,2,80,80:53755,57327; # DEATH KNIGHT BLOOD 16:0,6,0,80,80:53758,57356; # DEATH KNIGHT FROST 17:0,6,1,80,80:53760,57358; # DEATH KNIGHT UNHOLY 18:0,6,2,80,80:53760,57358; # DEATH KNIGHT BLOOD DPS 19:0,6,3,80,80:53760,57371; # SHAMAN ELEMENTAL 20:0,7,0,80,80:53755,57327; # SHAMAN ENHANCEMENT 21:0,7,1,80,80:53760,57325; # SHAMAN RESTORATION 22:0,7,2,80,80:53755,57327; # MAGE ARCANE 23:0,8,0,80,80:53755,57327; # MAGE FIRE 24:0,8,1,80,80:53755,57327; # MAGE FROST 25:0,8,2,80,80:53755,57327; # WARLOCK AFFLICTION 26:0,9,0,80,80:53755,57327; # WARLOCK DEMONOLOGY 27:0,9,1,80,80:53755,57327; # WARLOCK DESTRUCTION 28:0,9,2,80,80:53755,57327; # DRUID BALANCE 29:0,11,0,80,80:53755,57327; # DRUID FERAL BEAR 30:0,11,1,80,80:53749,53763,57367; # DRUID RESTORATION 31:0,11,2,80,80:54212,57334; # DRUID FERAL CAT 32:0,11,3,80,80:53760,57358; # WARRIOR ARMS TBC 33:0,1,0,70,79:28520,33256; # WARRIOR FURY TBC 34:0,1,1,70,79:28520,33256; # WARRIOR PROTECTION TBC 35:0,1,2,70,79:28518,33257; # PALADIN HOLY TBC 36:0,2,0,70,79:28491,39627,33263; # PALADIN PROTECTION TBC 37:0,2,1,70,79:28518,33257; # PALADIN RETRIBUTION TBC 38:0,2,2,70,79:28520,33256; # HUNTER BEAST TBC 39:0,3,0,70,79:28520,33261; # HUNTER MARKSMANSHIP TBC 40:0,3,1,70,79:28520,33261; # HUNTER SURVIVAL TBC 41:0,3,2,70,79:28520,33261; # ROGUE ASSASSINATION TBC 42:0,4,0,70,79:28520,33261; # ROGUE COMBAT TBC 43:0,4,1,70,79:28520,33261; # ROGUE SUBTLETY TBC 44:0,4,2,70,79:28520,33261; # PRIEST DISCIPLINE TBC 45:0,5,0,70,79:28491,39627,33263; # PRIEST HOLY TBC 46:0,5,1,70,79:28491,39627,33263; # PRIEST SHADOW TBC 47:0,5,2,70,79:28540,33263; # SHAMAN ELEMENTAL TBC 48:0,7,0,70,79:28521,33263; # SHAMAN ENHANCEMENT TBC 49:0,7,1,70,79:28520,33261; # SHAMAN RESTORATION TBC 50:0,7,2,70,79:28491,39627,33263; # MAGE ARCANE TBC 51:0,8,0,70,79:28521,33263; # MAGE FIRE TBC 52:0,8,1,70,79:28540,33263; # MAGE FROST TBC 53:0,8,2,70,79:28540,33263; # WARLOCK AFFLICTION TBC 54:0,9,0,70,79:28540,33263; # WARLOCK DEMONOLOGY TBC 55:0,9,1,70,79:28540,33263; # WARLOCK DESTRUCTION TBC 56:0,9,2,70,79:28540,33263; # DRUID BALANCE TBC 57:0,11,0,70,79:28521,33263; # DRUID FERAL BEAR TBC 58:0,11,1,70,79:28518,33257; # DRUID RESTORATION TBC 59:0,11,2,70,79:28491,39627,33263; # DRUID FERAL CAT TBC 60:0,11,3,70,79:28520,33261; # WARRIOR ARMS VANILLA 61:0,1,0,60,69:17538,24799; # WARRIOR FURY VANILLA 62:0,1,1,60,69:17538,24799; # WARRIOR PROTECTION VANILLA 63:0,1,2,60,69:17626,25661; # PALADIN HOLY VANILLA 64:0,2,0,60,69:17627,18194; # PALADIN PROTECTION VANILLA 65:0,2,1,60,69:17626,25661; # PALADIN RETRIBUTION VANILLA 66:0,2,2,60,69:17628,24799; # HUNTER BEAST VANILLA 67:0,3,0,60,69:17538,18192; # HUNTER MARKSMANSHIP VANILLA 68:0,3,1,60,69:17538,18192; # HUNTER SURVIVAL VANILLA 69:0,3,2,60,69:17538,18192; # ROGUE ASSASSINATION VANILLA 70:0,4,0,60,69:17538,18192; # ROGUE COMBAT VANILLA 71:0,4,1,60,69:17538,18192; # ROGUE SUBTLETY VANILLA 72:0,4,2,60,69:17538,18192; # PRIEST DISCIPLINE VANILLA 73:0,5,0,60,69:17628,18194; # PRIEST HOLY VANILLA 74:0,5,1,60,69:17627,18194; # PRIEST SHADOW VANILLA 75:0,5,2,60,69:17628,18194; # SHAMAN ELEMENTAL VANILLA 76:0,7,0,60,69:17628,18194; # SHAMAN ENHANCEMENT VANILLA 77:0,7,1,60,69:17538,24799; # SHAMAN RESTORATION VANILLA 78:0,7,2,60,69:17627,18194; # MAGE ARCANE VANILLA 79:0,8,0,60,69:17628,18194; # MAGE FIRE VANILLA 80:0,8,1,60,69:17628,18194; # MAGE FROST VANILLA 81:0,8,2,60,69:17628,18194; # WARLOCK AFFLICTION VANILLA 82:0,9,0,60,69:17628,25661; # WARLOCK DEMONOLOGY VANILLA 83:0,9,1,60,69:17628,25661; # WARLOCK DESTRUCTION VANILLA 84:0,9,2,60,69:17628,25661; # DRUID BALANCE VANILLA 85:0,11,0,60,69:17628,18194; # DRUID FERAL BEAR VANILLA 86:0,11,1,60,69:17626,25661; # DRUID RESTORATION VANILLA 87:0,11,2,60,69:17627,18194; # DRUID FERAL CAT VANILLA 88:0,11,3,60,69:17538,24799 # # From 19249e90a0e254bb36a66653201894edf293da16 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 17 Apr 2026 23:07:47 +0200 Subject: [PATCH 74/87] Pull strategy migration (#2310) ## Pull Request Description Pull strategy migration from cmangos for tank specializations ## How to Test the Changes 1. Invite bot tank 2. Use `reset boAI` or `nc +pull,+pull back` + `co +pull,+pull back` 3. Order bot to pull using command `pull my target` or `pull rti target` 4. Bot should run to mob, use ranged skill and back to point where he started pull Without `pull back` strategy bot run to mob, use ranged skill and wait on mob until he come to bot ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Help with migration and solving some problems ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Stability test after randomize new bots obraz --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> --- .../2026_04_12_00_ai_playerbot_pull_texts.sql | 102 ++++++ src/Ai/Base/ActionContext.h | 15 + src/Ai/Base/Actions/GenericSpellActions.cpp | 25 +- src/Ai/Base/Actions/GenericSpellActions.h | 5 + src/Ai/Base/Actions/PullActions.cpp | 321 ++++++++++++++++++ src/Ai/Base/Actions/PullActions.h | 90 +++++ src/Ai/Base/ChatActionContext.h | 5 + src/Ai/Base/ChatTriggerContext.h | 6 + .../Strategy/ChatCommandHandlerStrategy.cpp | 6 + src/Ai/Base/Strategy/PullStrategy.cpp | 222 +++++++++++- src/Ai/Base/Strategy/PullStrategy.h | 58 +++- .../Base/Strategy/WaitForAttackStrategy.cpp | 1 + src/Ai/Base/StrategyContext.h | 2 + src/Ai/Base/Trigger/PullTriggers.cpp | 68 ++++ src/Ai/Base/Trigger/PullTriggers.h | 35 ++ src/Ai/Base/TriggerContext.h | 7 + src/Ai/Class/Dk/DKAiObjectContext.cpp | 4 +- .../Dk/Strategy/DeathKnightPullStrategy.cpp | 43 +++ .../Dk/Strategy/DeathKnightPullStrategy.h | 19 ++ src/Ai/Class/Druid/DruidAiObjectContext.cpp | 3 + .../Druid/Strategy/DruidPullStrategy.cpp | 46 +++ .../Class/Druid/Strategy/DruidPullStrategy.h | 20 ++ .../Class/Paladin/PaladinAiObjectContext.cpp | 3 + .../Paladin/Strategy/PaladinPullStrategy.cpp | 46 +++ .../Paladin/Strategy/PaladinPullStrategy.h | 20 ++ .../Warrior/Strategy/WarriorPullStrategy.cpp | 27 ++ .../Warrior/Strategy/WarriorPullStrategy.h | 19 ++ .../Class/Warrior/WarriorAiObjectContext.cpp | 4 +- src/Bot/Engine/Engine.cpp | 6 + src/Bot/Engine/Engine.h | 1 + src/Bot/Factory/AiFactory.cpp | 16 +- src/Bot/PlayerbotAI.cpp | 5 + src/Bot/PlayerbotAI.h | 1 + 33 files changed, 1229 insertions(+), 22 deletions(-) create mode 100644 data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql create mode 100644 src/Ai/Base/Actions/PullActions.cpp create mode 100644 src/Ai/Base/Actions/PullActions.h create mode 100644 src/Ai/Base/Trigger/PullTriggers.cpp create mode 100644 src/Ai/Base/Trigger/PullTriggers.h create mode 100644 src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp create mode 100644 src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h create mode 100644 src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp create mode 100644 src/Ai/Class/Druid/Strategy/DruidPullStrategy.h create mode 100644 src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp create mode 100644 src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h create mode 100644 src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp create mode 100644 src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h diff --git a/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql b/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql new file mode 100644 index 00000000000..4e9c175832b --- /dev/null +++ b/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql @@ -0,0 +1,102 @@ +-- ######################################################### +-- Playerbots - Add pull command texts +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'pull_no_target_error', + 'pull_target_too_far_error', + 'pull_invalid_target_error', + 'pull_action_unavailable_error' +); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'pull_no_target_error', + 'pull_target_too_far_error', + 'pull_invalid_target_error', + 'pull_action_unavailable_error' +); + +-- pull_no_target_error +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 ( + 1755, + 'pull_no_target_error', + 'You have no target', + 0, 0, + '대상이 없습니다', + 'Vous n''avez pas de cible', + 'Du hast kein Ziel', + '你没有目标', + '你沒有目標', + 'No tienes objetivo', + 'No tienes objetivo', + 'У вас нет цели'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_no_target_error', 100); + +-- pull_target_too_far_error +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 ( + 1756, + 'pull_target_too_far_error', + 'The target is too far away', + 0, 0, + '대상이 너무 멀리 있습니다', + 'La cible est trop loin', + 'Das Ziel ist zu weit entfernt', + '目标太远了', + '目標太遠了', + 'El objetivo está demasiado lejos', + 'El objetivo está demasiado lejos', + 'Цель слишком далеко'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_target_too_far_error', 100); + +-- pull_invalid_target_error +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 ( + 1757, + 'pull_invalid_target_error', + 'The target can''t be pulled', + 0, 0, + '해당 대상은 풀링할 수 없습니다', + 'La cible ne peut pas être attirée', + 'Das Ziel kann nicht gepullt werden', + '该目标无法被拉怪', + '該目標無法被拉怪', + 'No se puede hacer pull al objetivo', + 'No se puede hacer pull al objetivo', + 'Эту цель нельзя пуллить'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_invalid_target_error', 100); + +-- pull_action_unavailable_error: %action_name is replaced with the configured pull action +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 ( + 1758, + 'pull_action_unavailable_error', + 'Can''t perform pull action ''%action_name''', + 0, 0, + '''%action_name'' 풀 액션을 수행할 수 없습니다', + 'Impossible d''effectuer l''action d''engagement ''%action_name''', + 'Die Pull-Aktion ''%action_name'' kann nicht ausgeführt werden', + '无法执行拉怪动作“%action_name”', + '無法執行拉怪動作「%action_name」', + 'No se puede realizar la acción de pull ''%action_name''', + 'No se puede realizar la acción de pull ''%action_name''', + 'Невозможно выполнить действие пула ''%action_name'''); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_action_unavailable_error', 100); diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 79b5b498581..3026cfd5009 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -45,6 +45,7 @@ #include "NonCombatActions.h" #include "OutfitAction.h" #include "PositionAction.h" +#include "PullActions.h" #include "DropQuestAction.h" #include "RandomBotUpdateAction.h" #include "ReachTargetActions.h" @@ -105,6 +106,13 @@ class ActionContext : public NamedObjectContext creators["shoot"] = &ActionContext::shoot; creators["lifeblood"] = &ActionContext::lifeblood; creators["arcane torrent"] = &ActionContext::arcane_torrent; + creators["pull my target"] = &ActionContext::pull_my_target; + creators["pull rti target"] = &ActionContext::pull_rti_target; + creators["pull start"] = &ActionContext::pull_start; + creators["pull action"] = &ActionContext::pull_action; + creators["pull end"] = &ActionContext::pull_end; + creators["return to pull position"] = &ActionContext::return_to_pull_position; + creators["reach pull"] = &ActionContext::reach_pull; creators["end pull"] = &ActionContext::end_pull; creators["healthstone"] = &ActionContext::healthstone; creators["healing potion"] = &ActionContext::healing_potion; @@ -313,6 +321,13 @@ class ActionContext : public NamedObjectContext static Action* gift_of_the_naaru(PlayerbotAI* botAI) { return new CastGiftOfTheNaaruAction(botAI); } static Action* lifeblood(PlayerbotAI* botAI) { return new CastLifeBloodAction(botAI); } static Action* arcane_torrent(PlayerbotAI* botAI) { return new CastArcaneTorrentAction(botAI); } + static Action* pull_my_target(PlayerbotAI* botAI) { return new PullMyTargetAction(botAI); } + static Action* pull_rti_target(PlayerbotAI* botAI) { return new PullRtiTargetAction(botAI); } + static Action* pull_start(PlayerbotAI* botAI) { return new PullStartAction(botAI); } + static Action* pull_action(PlayerbotAI* botAI) { return new PullAction(botAI); } + static Action* pull_end(PlayerbotAI* botAI) { return new PullEndAction(botAI); } + static Action* return_to_pull_position(PlayerbotAI* botAI) { return new ReturnToPullPositionAction(botAI); } + static Action* reach_pull(PlayerbotAI* botAI) { return new ReachPullAction(botAI); } static Action* mana_tap(PlayerbotAI* botAI) { return new CastManaTapAction(botAI); } static Action* end_pull(PlayerbotAI* botAI) { return new ChangeCombatStrategyAction(botAI, "-pull"); } static Action* cancel_channel(PlayerbotAI* botAI) { return new CancelChannelAction(botAI); } diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 41911f62d8e..c81aca21467 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -273,7 +273,7 @@ bool BuffOnPartyAction::Execute(Event /*event*/) } // End greater buff fix -CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot") +CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0) { if (Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) { @@ -283,17 +283,40 @@ CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "s { case ITEM_SUBCLASS_WEAPON_GUN: spell += " gun"; + shootSpellId = 3018; break; case ITEM_SUBCLASS_WEAPON_BOW: spell += " bow"; + shootSpellId = 3018; break; case ITEM_SUBCLASS_WEAPON_CROSSBOW: spell += " crossbow"; + shootSpellId = 3018; + break; + case ITEM_SUBCLASS_WEAPON_THROWN: + spell = "throw"; + shootSpellId = 2764; break; } } } +bool CastShootAction::isPossible() +{ + if (shootSpellId) + return botAI->CanCastSpell(shootSpellId, GetTarget(), false); + + return CastSpellAction::isPossible(); +} + +bool CastShootAction::Execute(Event /*event*/) +{ + if (shootSpellId) + return botAI->CastSpell(shootSpellId, GetTarget()); + + return botAI->CastSpell(spell, GetTarget()); +} + Value* CastDebuffSpellOnAttackerAction::GetTargetValue() { return context->GetValue("attacker without aura", spell); diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index dc07857138d..e9dacb7d2fe 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -253,7 +253,12 @@ class CastShootAction : public CastSpellAction public: CastShootAction(PlayerbotAI* botAI); + bool isPossible() override; + bool Execute(Event event) override; ActionThreatType getThreatType() override { return ActionThreatType::None; } + +private: + uint32 shootSpellId; }; class CastLifeBloodAction : public CastHealingSpellAction diff --git a/src/Ai/Base/Actions/PullActions.cpp b/src/Ai/Base/Actions/PullActions.cpp new file mode 100644 index 00000000000..805abd36258 --- /dev/null +++ b/src/Ai/Base/Actions/PullActions.cpp @@ -0,0 +1,321 @@ +/* + * 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 "AttackersValue.h" +#include "CreatureAI.h" +#include "Playerbots.h" +#include "PlayerbotTextMgr.h" +#include "PositionValue.h" +#include "PullActions.h" +#include "PullStrategy.h" +#include "RtiTargetValue.h" +#include + +namespace +{ +float GetPullReachDistance(Player* bot, Unit* target, PullStrategy const* strategy) +{ + if (!bot || !target || !strategy) + return 0.0f; + + float const combatDistance = bot->GetCombatReach() + target->GetCombatReach(); + return std::max(0.0f, strategy->GetRange() - combatDistance); +} + +bool IsWithinPullRange(Player* bot, Unit* target, PullStrategy const* strategy) +{ + return bot && target && strategy && bot->GetExactDist(target) <= strategy->GetRange(); +} +} + +bool PullRequestAction::Execute(Event event) +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + if (!botAI->IsTank(bot)) + return false; + + Unit* target = GetPullTarget(event); + if (!target || !target->IsInWorld()) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_no_target_error", "You have no target", {}); + botAI->TellError(text); + return false; + } + + float const maxPullDistance = sPlayerbotAIConfig.reactDistance * 3.0f; + if (target->GetMapId() != bot->GetMapId() || bot->GetDistance(target) > maxPullDistance) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_target_too_far_error", "The target is too far away", {}); + botAI->TellError(text); + return false; + } + + if (!AttackersValue::IsPossibleTarget(target, bot)) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_invalid_target_error", "The target can't be pulled", {}); + botAI->TellError(text); + return false; + } + + if (!strategy->CanDoPullAction(target)) + { + std::string const actionName = strategy->GetPullActionName(); + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_action_unavailable_error", + "Can't perform pull action '%action_name'", + {{"%action_name", actionName}}); + botAI->TellError(text); + return false; + } + + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo pullPosition = posMap["pull"]; + pullPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId()); + posMap["pull"] = pullPosition; + + strategy->RequestPull(target); + context->GetValue("current target")->Set(target); + botAI->ChangeEngine(BOT_STATE_COMBAT); + botAI->SetNextCheckDelay(sPlayerbotAIConfig.reactDelay); + return true; +} + +Unit* PullMyTargetAction::GetPullTarget(Event event) +{ + Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); + if (event.GetSource() == "attack anything") + return botAI->GetCreature(event.getObject()); + + return requester ? requester->GetSelectedUnit() : nullptr; +} + +Unit* PullRtiTargetAction::GetPullTarget(Event /*event*/) +{ + Unit* rtiTarget = AI_VALUE(Unit*, "rti target"); + if (rtiTarget) + return rtiTarget; + + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + std::string const rti = AI_VALUE(std::string, "rti"); + int32 const index = RtiTargetValue::GetRtiIndex(rti); + if (index < 0) + return nullptr; + + ObjectGuid const guid = group->GetTargetIcon(index); + return guid.IsEmpty() ? nullptr : botAI->GetUnit(guid); +} + +bool PullStartAction::Execute(Event event) +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* target = strategy->GetTarget(); + if (!target) + return false; + + std::string const preActionName = strategy->GetPreActionName(); + if (!preActionName.empty() && !botAI->DoSpecificAction(preActionName, event, true)) + return false; + + if (Pet* pet = bot->GetPet()) + { + Creature* creature = pet->ToCreature(); + if (creature) + { + strategy->SetPetReactState(creature->GetReactState()); + creature->SetReactState(REACT_PASSIVE); + } + } + + strategy->OnPullStarted(); + return true; +} + +PullAction::PullAction(PlayerbotAI* botAI, std::string const name) : CastSpellAction(botAI, name) { InitPullAction(); } + +Unit* PullAction::GetTarget() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return nullptr; + + return strategy->GetTarget(); +} + +std::vector PullAction::getPrerequisites() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + Unit* target = strategy ? strategy->GetTarget() : nullptr; + if (!strategy || !target) + return {}; + + return IsWithinPullRange(bot, target, strategy) ? std::vector{} + : std::vector{ NextAction("reach pull", ACTION_MOVE) }; +} + +bool PullAction::Execute(Event event) +{ + InitPullAction(); + + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* target = strategy->GetTarget(); + if (!target || !target->IsInWorld()) + return false; + + if (target->IsInCombat()) + return false; + + if (!IsWithinPullRange(bot, target, strategy)) + { + strategy->RequestPull(target, false); + return false; + } + + if (bot->isMoving()) + { + bot->StopMoving(); + strategy->RequestPull(target, false); + return false; + } + + context->GetValue("current target")->Set(target); + if (!botAI->DoSpecificAction(strategy->GetPullActionName(), event, true)) + return false; + + return true; +} + +bool PullAction::isPossible() +{ + InitPullAction(); + + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* target = strategy->GetTarget(); + std::string const spellName = strategy->GetSpellName(); + if (!target || !target->IsInWorld() || target->GetMapId() != bot->GetMapId() || spellName.empty()) + return false; + + return true; +} + +void PullAction::InitPullAction() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return; + + std::string const spellName = strategy->GetSpellName(); + if (spellName.empty()) + return; + + spell = spellName; + + bool isShoot = (spellName == "shoot" || spellName == "shoot bow" || + spellName == "shoot gun" || spellName == "shoot crossbow" || + spellName == "throw"); + range = botAI->GetRange(isShoot ? "shoot" : "spell"); +} + +bool PullEndAction::Execute(Event /*event*/) +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* pullTarget = strategy->GetTarget(); + + if (!strategy->HasPullStarted() && !strategy->IsPullPendingToStart() && !strategy->HasTarget()) + return false; + + if (Pet* pet = bot->GetPet()) + { + Creature* creature = pet->ToCreature(); + if (creature) + creature->SetReactState(strategy->GetPetReactState()); + } + + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo pullPosition = posMap["pull"]; + if (pullPosition.isSet()) + posMap.erase("pull"); + + if (pullTarget && context->GetValue("current target")->Get() == pullTarget) + context->GetValue("current target")->Set(nullptr); + + strategy->OnPullEnded(); + return true; +} + +bool ReturnToPullPositionAction::Execute(Event /*event*/) +{ + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + if (!pullPosition.isSet() || pullPosition.mapId != bot->GetMapId()) + return false; + + return MoveTo(pullPosition.mapId, pullPosition.x, pullPosition.y, pullPosition.z, + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true); +} + +bool ReturnToPullPositionAction::isUseful() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + Unit* target = strategy ? strategy->GetTarget() : nullptr; + if (!strategy || !target || !target->IsInCombat()) + return false; + + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + return pullPosition.isSet() && pullPosition.mapId == bot->GetMapId() && + bot->GetDistance(pullPosition.x, pullPosition.y, pullPosition.z) > sPlayerbotAIConfig.followDistance; +} + +bool ReachPullAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!target || !strategy) + return false; + + float const reachDistance = GetPullReachDistance(bot, target, strategy); + return ReachCombatTo(target, reachDistance); +} + +bool ReachPullAction::isUseful() +{ + if (botAI->HasStrategy("stay", botAI->GetState())) + return false; + + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr) + return false; + + PullStrategy* strategy = PullStrategy::Get(botAI); + Unit* target = strategy ? strategy->GetTarget() : nullptr; + return target && !IsWithinPullRange(bot, target, strategy); +} + +Unit* ReachPullAction::GetTarget() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return nullptr; + + return strategy->GetTarget(); +} diff --git a/src/Ai/Base/Actions/PullActions.h b/src/Ai/Base/Actions/PullActions.h new file mode 100644 index 00000000000..299dd4a1ef8 --- /dev/null +++ b/src/Ai/Base/Actions/PullActions.h @@ -0,0 +1,90 @@ +/* + * 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_PULLACTIONS_H +#define _PLAYERBOT_PULLACTIONS_H + +#include "GenericSpellActions.h" +#include "ReachTargetActions.h" + +class PullRequestAction : public Action +{ +public: + PullRequestAction(PlayerbotAI* botAI, std::string const name) : Action(botAI, name) {} + + bool Execute(Event event) override; + +protected: + virtual Unit* GetPullTarget(Event event) = 0; +}; + +class PullMyTargetAction : public PullRequestAction +{ +public: + PullMyTargetAction(PlayerbotAI* botAI) : PullRequestAction(botAI, "pull my target") {} + +private: + Unit* GetPullTarget(Event event) override; +}; + +class PullRtiTargetAction : public PullRequestAction +{ +public: + PullRtiTargetAction(PlayerbotAI* botAI) : PullRequestAction(botAI, "pull rti target") {} + +private: + Unit* GetPullTarget(Event event) override; +}; + +class PullStartAction : public Action +{ +public: + PullStartAction(PlayerbotAI* botAI, std::string const name = "pull start") : Action(botAI, name) {} + + bool Execute(Event event) override; +}; + +class PullAction : public CastSpellAction +{ +public: + PullAction(PlayerbotAI* botAI, std::string const name = "pull action"); + + bool Execute(Event event) override; + bool isPossible() override; + std::vector getPrerequisites() override; + Unit* GetTarget() override; + +private: + void InitPullAction(); +}; + +class PullEndAction : public Action +{ +public: + PullEndAction(PlayerbotAI* botAI, std::string const name = "pull end") : Action(botAI, name) {} + + bool Execute(Event event) override; +}; + +class ReachPullAction : public ReachTargetAction +{ +public: + ReachPullAction(PlayerbotAI* botAI) : ReachTargetAction(botAI, "reach pull", botAI->GetRange("spell")) {} + + bool Execute(Event event) override; + bool isUseful() override; + Unit* GetTarget() override; +}; + +class ReturnToPullPositionAction : public MovementAction +{ +public: + ReturnToPullPositionAction(PlayerbotAI* botAI) : MovementAction(botAI, "return to pull position") {} + + bool Execute(Event event) override; + bool isUseful() override; +}; + +#endif diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 6f11fb33c64..af51c23ae07 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -43,6 +43,7 @@ #include "NewRpgAction.h" #include "PassLeadershipToMasterAction.h" #include "PositionAction.h" +#include "PullActions.h" #include "QueryItemUsageAction.h" #include "QueryQuestAction.h" #include "RangeAction.h" @@ -138,6 +139,8 @@ class ChatActionContext : public NamedObjectContext creators["autogear"] = &ChatActionContext::autogear; creators["equip upgrade"] = &ChatActionContext::equip_upgrade; creators["attack my target"] = &ChatActionContext::attack_my_target; + creators["pull my target"] = &ChatActionContext::pull_my_target; + creators["pull rti target"] = &ChatActionContext::pull_rti_target; creators["chat"] = &ChatActionContext::chat; creators["home"] = &ChatActionContext::home; creators["destroy"] = &ChatActionContext::destroy; @@ -250,6 +253,8 @@ class ChatActionContext : public NamedObjectContext static Action* home(PlayerbotAI* botAI) { return new SetHomeAction(botAI); } static Action* chat(PlayerbotAI* botAI) { return new ChangeChatAction(botAI); } static Action* attack_my_target(PlayerbotAI* botAI) { return new AttackMyTargetAction(botAI); } + static Action* pull_my_target(PlayerbotAI* botAI) { return new PullMyTargetAction(botAI); } + static Action* pull_rti_target(PlayerbotAI* botAI) { return new PullRtiTargetAction(botAI); } static Action* trainer(PlayerbotAI* botAI) { return new TrainerAction(botAI); } static Action* maintenance(PlayerbotAI* botAI) { return new MaintenanceAction(botAI); } static Action* remove_glyph(PlayerbotAI* botAI) { return new RemoveGlyphAction(botAI); } diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 40316bd62a3..7742a9305be 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -66,6 +66,9 @@ class ChatTriggerContext : public NamedObjectContext creators["autogear"] = &ChatTriggerContext::autogear; creators["equip upgrade"] = &ChatTriggerContext::equip_upgrade; creators["attack"] = &ChatTriggerContext::attack; + creators["pull"] = &ChatTriggerContext::pull; + creators["pull back"] = &ChatTriggerContext::pull_back; + creators["pull rti"] = &ChatTriggerContext::pull_rti; creators["chat"] = &ChatTriggerContext::chat; creators["accept"] = &ChatTriggerContext::accept; creators["home"] = &ChatTriggerContext::home; @@ -209,6 +212,9 @@ class ChatTriggerContext : public NamedObjectContext static Trigger* accept(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "accept"); } static Trigger* chat(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "chat"); } static Trigger* attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attack"); } + static Trigger* pull(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pull"); } + static Trigger* pull_back(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pull back"); } + static Trigger* pull_rti(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pull rti"); } static Trigger* trainer(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "trainer"); } static Trigger* maintenance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "maintenance"); } static Trigger* remove_glyph(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "remove glyph"); } diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index 77a8d9d0b2d..adcadb23366 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -81,6 +81,12 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger new TriggerNode("attackers", { NextAction("tell attackers", relevance) })); triggers.push_back( new TriggerNode("target", { NextAction("tell target", relevance) })); + triggers.push_back( + new TriggerNode("pull", { NextAction("pull my target", relevance) })); + triggers.push_back( + new TriggerNode("pull back", { NextAction("pull my target", relevance) })); + triggers.push_back( + new TriggerNode("pull rti", { NextAction("pull rti target", relevance) })); triggers.push_back( new TriggerNode("ready", { NextAction("ready check", relevance) })); triggers.push_back( diff --git a/src/Ai/Base/Strategy/PullStrategy.cpp b/src/Ai/Base/Strategy/PullStrategy.cpp index d0c7c9eacbe..fce9172795a 100644 --- a/src/Ai/Base/Strategy/PullStrategy.cpp +++ b/src/Ai/Base/Strategy/PullStrategy.cpp @@ -5,8 +5,184 @@ #include "PullStrategy.h" +#include "AiObjectContext.h" #include "PassiveMultiplier.h" +#include "Player.h" +#include "PlayerbotAI.h" #include "Playerbots.h" +#include "SpellMgr.h" + +class PullStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + PullStrategyActionNodeFactory() + { + creators["pull start"] = &pull_start; + } + +private: + static ActionNode* pull_start(PlayerbotAI* /*botAI*/) + { + return new ActionNode("pull start", {}, {}, { NextAction("pull action", ACTION_NORMAL) }); + } +}; + +PullStrategy::PullStrategy(PlayerbotAI* botAI, std::string const action, std::string const preAction) + : Strategy(botAI), action(action), preAction(preAction) +{ + actionNodeFactories.Add(new PullStrategyActionNodeFactory()); +} + +PullStrategy* PullStrategy::Get(PlayerbotAI* botAI) +{ + if (!botAI) + return nullptr; + + if (PullStrategy* strategy = dynamic_cast(botAI->GetStrategy("pull", BOT_STATE_NON_COMBAT))) + { + if (strategy->IsPullPendingToStart() || strategy->HasPullStarted() || strategy->HasTarget()) + return strategy; + } + + return dynamic_cast(botAI->GetStrategy("pull", BOT_STATE_COMBAT)); +} + +Unit* PullStrategy::GetTarget() const +{ + ObjectGuid const guid = botAI->GetAiObjectContext()->GetValue("pull target")->Get(); + if (guid.IsEmpty()) + return nullptr; + + Unit* target = botAI->GetUnit(guid); + Player* bot = botAI->GetBot(); + if (!bot || !target || !target->IsInWorld() || target->GetMapId() != bot->GetMapId()) + return nullptr; + + return target; +} + +bool PullStrategy::HasTarget() const { return GetTarget() != nullptr; } + +void PullStrategy::SetTarget(Unit* target) +{ + botAI->GetAiObjectContext()->GetValue("pull target")->Set(target ? target->GetGUID() : ObjectGuid::Empty); +} + +std::string PullStrategy::GetPullActionName() const +{ + return action; +} + +std::string PullStrategy::GetSpellName() const +{ + Player* bot = botAI->GetBot(); + std::string spellName = GetPullActionName(); + if (!bot || spellName != "shoot") + return spellName; + + Item* equippedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!equippedWeapon) + return spellName; + + ItemTemplate const* itemTemplate = equippedWeapon->GetTemplate(); + if (!itemTemplate) + return spellName; + + switch (itemTemplate->SubClass) + { + case ITEM_SUBCLASS_WEAPON_THROWN: + return "throw"; + case ITEM_SUBCLASS_WEAPON_GUN: + return "shoot gun"; + case ITEM_SUBCLASS_WEAPON_BOW: + return "shoot bow"; + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + return "shoot crossbow"; + default: + return spellName; + } +} + +float PullStrategy::GetRange() const +{ + Player* bot = botAI->GetBot(); + std::string const spellName = GetSpellName(); + if (bot && !spellName.empty()) + { + uint32 const spellId = botAI->GetAiObjectContext()->GetValue("spell id", spellName)->Get(); + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId)) + return bot->GetSpellMaxRangeForTarget(GetTarget(), spellInfo) - CONTACT_DISTANCE; + } + + return (action == "shoot" ? botAI->GetRange("shoot") : botAI->GetRange("spell")) - CONTACT_DISTANCE; +} + +std::string PullStrategy::GetPreActionName() const +{ + return preAction; +} + +bool PullStrategy::CanDoPullAction(Unit* target) +{ + Player* bot = botAI->GetBot(); + if (!bot || !target) + return false; + + if (!target->IsInWorld() || target->GetMapId() != bot->GetMapId()) + return false; + + if (bot->getClass() != CLASS_DRUID && bot->getClass() != CLASS_PALADIN && + GetPullActionName() == "shoot" && !bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) + { + return false; + } + + std::string const spellName = GetSpellName(); + if (spellName.empty()) + return false; + + return true; +} + +void PullStrategy::RequestPull(Unit* target, bool resetTime) +{ + SetTarget(target); + pendingToStart = true; + if (resetTime) + pullStartTime = time(nullptr); +} + +void PullStrategy::OnPullStarted() { pendingToStart = false; } + +void PullStrategy::OnPullEnded() +{ + pullStartTime = 0; + pendingToStart = false; + SetTarget(nullptr); +} + +PullMultiplier::PullMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "pull") {} + +float PullMultiplier::GetValue(Action* action) +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + if (!strategy || !strategy->HasTarget() || !action) + return 1.0f; + + std::string const actionName = action->getName(); + if (actionName == "pull my target" || + actionName == "pull rti target" || + actionName == "reach pull" || + actionName == "pull start" || + actionName == "pull action" || + actionName == "return to pull position" || + actionName == "pull end" || + actionName == "follow" || + actionName == "set facing") + return 1.0f; + + return 0.0f; +} class MagePullMultiplier : public PassiveMultiplier { @@ -24,8 +200,16 @@ float MagePullMultiplier::GetValue(Action* action) if (!action) return 1.0f; + PullStrategy const* strategy = PullStrategy::Get(botAI); + if (!strategy || !strategy->HasTarget()) + return 1.0f; + std::string const name = action->getName(); - if (actionName == name || name == "reach spell" || name == "change strategy") + if (actionName == name || name == "pull action" || name == "pull start" || name == "pull end" || + name == "pull my target" || name == "pull rti target" || + name == "reach spell" || name == "reach pull" || + name == "return to pull position" || name == "follow" || + name == "set facing" || name == "change strategy") return 1.0f; return PassiveMultiplier::GetValue(action); @@ -34,18 +218,32 @@ float MagePullMultiplier::GetValue(Action* action) std::vector PullStrategy::getDefaultActions() { return { - NextAction(action, 105.0f), - NextAction("follow", 104.0f), - NextAction("end pull", 103.0f), + NextAction("pull action", 105.0f), }; } -void PullStrategy::InitTriggers(std::vector& triggers) { CombatStrategy::InitTriggers(triggers); } +void PullStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "pull start", + { + NextAction("pull start", 106.0f), + NextAction("pull action", ACTION_MOVE) + } + )); + + triggers.push_back(new TriggerNode( + "pull end", + { + NextAction("pull end", 107.0f) + } + )); +} void PullStrategy::InitMultipliers(std::vector& multipliers) { + multipliers.push_back(new PullMultiplier(botAI)); multipliers.push_back(new MagePullMultiplier(botAI, action)); - CombatStrategy::InitMultipliers(multipliers); } void PossibleAddsStrategy::InitTriggers(std::vector& triggers) @@ -61,3 +259,15 @@ void PossibleAddsStrategy::InitTriggers(std::vector& triggers) ) ); } + +void PullBackStrategy::InitTriggers(std::vector& triggers) +{ + Strategy::InitTriggers(triggers); + + triggers.push_back(new TriggerNode( + "return to pull position", + { + NextAction("return to pull position", ACTION_MOVE + 5.0f) + } + )); +} diff --git a/src/Ai/Base/Strategy/PullStrategy.h b/src/Ai/Base/Strategy/PullStrategy.h index bdd7332f3b6..428699c5641 100644 --- a/src/Ai/Base/Strategy/PullStrategy.h +++ b/src/Ai/Base/Strategy/PullStrategy.h @@ -6,22 +6,65 @@ #ifndef _PLAYERBOT_PULLSTRATEGY_H #define _PLAYERBOT_PULLSTRATEGY_H -#include "CombatStrategy.h" +#include "Strategy.h" + +class Action; +class Multiplier; +class Unit; class PlayerbotAI; -class PullStrategy : public CombatStrategy +class PullStrategy : public Strategy { public: - PullStrategy(PlayerbotAI* botAI, std::string const action) : CombatStrategy(botAI), action(action) {} + PullStrategy(PlayerbotAI* botAI, std::string const action, std::string const preAction = ""); void InitTriggers(std::vector& triggers) override; void InitMultipliers(std::vector& multipliers) override; std::string const getName() override { return "pull"; } std::vector getDefaultActions() override; + uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_NONCOMBAT; } + + static PullStrategy* Get(PlayerbotAI* botAI); + static uint8 GetMaxPullTime() { return 15; } + + time_t GetPullStartTime() const { return pullStartTime; } + bool IsPullPendingToStart() const { return pendingToStart; } + bool HasPullStarted() const { return pullStartTime > 0; } + + bool CanDoPullAction(Unit* target); + Unit* GetTarget() const; + bool HasTarget() const; + + virtual std::string GetPullActionName() const; + std::string GetSpellName() const; + float GetRange() const; + virtual std::string GetPreActionName() const; + + void RequestPull(Unit* target, bool resetTime = true); + void OnPullStarted(); + void OnPullEnded(); + + ReactStates GetPetReactState() const { return petReactState; } + void SetPetReactState(ReactStates reactState) { petReactState = reactState; } + +private: + void SetTarget(Unit* target); private: std::string const action; + std::string const preAction; + bool pendingToStart = false; + time_t pullStartTime = 0; + ReactStates petReactState = REACT_DEFENSIVE; +}; + +class PullMultiplier : public Multiplier +{ +public: + PullMultiplier(PlayerbotAI* botAI); + + float GetValue(Action* action) override; }; class PossibleAddsStrategy : public Strategy @@ -33,4 +76,13 @@ class PossibleAddsStrategy : public Strategy std::string const getName() override { return "adds"; } }; +class PullBackStrategy : public Strategy +{ +public: + PullBackStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "pull back"; } +}; + #endif diff --git a/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp b/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp index a381405124f..21950f00c78 100644 --- a/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp +++ b/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp @@ -82,6 +82,7 @@ float WaitForAttackMultiplier::GetValue(Action* action) actionName != "set facing" && actionName != "pull my target" && actionName != "pull rti target" && + actionName != "reach pull" && actionName != "pull start" && actionName != "pull action" && actionName != "pull end") diff --git a/src/Ai/Base/StrategyContext.h b/src/Ai/Base/StrategyContext.h index 5386e872bfd..8dab9c40d6d 100644 --- a/src/Ai/Base/StrategyContext.h +++ b/src/Ai/Base/StrategyContext.h @@ -95,6 +95,7 @@ class StrategyContext : public NamedObjectContext creators["sit"] = &StrategyContext::sit; creators["mark rti"] = &StrategyContext::mark_rti; creators["adds"] = &StrategyContext::possible_adds; + creators["pull back"] = &StrategyContext::pull_back; creators["close"] = &StrategyContext::close; creators["ranged"] = &StrategyContext::ranged; creators["behind"] = &StrategyContext::behind; @@ -171,6 +172,7 @@ class StrategyContext : public NamedObjectContext static Strategy* map_full(PlayerbotAI* botAI) { return new MapFullStrategy(botAI); } static Strategy* sit(PlayerbotAI* botAI) { return new SitStrategy(botAI); } static Strategy* possible_adds(PlayerbotAI* botAI) { return new PossibleAddsStrategy(botAI); } + static Strategy* pull_back(PlayerbotAI* botAI) { return new PullBackStrategy(botAI); } static Strategy* mount(PlayerbotAI* botAI) { return new MountStrategy(botAI); } static Strategy* bg(PlayerbotAI* botAI) { return new BGStrategy(botAI); } static Strategy* battleground(PlayerbotAI* botAI) { return new BattlegroundStrategy(botAI); } diff --git a/src/Ai/Base/Trigger/PullTriggers.cpp b/src/Ai/Base/Trigger/PullTriggers.cpp new file mode 100644 index 00000000000..84c550fafda --- /dev/null +++ b/src/Ai/Base/Trigger/PullTriggers.cpp @@ -0,0 +1,68 @@ +/* + * 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 "PullTriggers.h" + +#include "PositionValue.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "PullStrategy.h" + +bool PullStartTrigger::IsActive() +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + return strategy && strategy->IsPullPendingToStart(); +} + +bool PullEndTrigger::IsActive() +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + Player* bot = botAI->GetBot(); + if (!bot) + return false; + + if (!strategy || !strategy->HasPullStarted()) + return false; + + Unit* target = strategy->GetTarget(); + if (!target || !target->IsInWorld()) + return true; + + time_t const secondsSincePullStarted = time(nullptr) - strategy->GetPullStartTime(); + if (secondsSincePullStarted >= PullStrategy::GetMaxPullTime()) + return true; + + float distanceToPullTarget = bot->GetDistance(target); + if (distanceToPullTarget > ATTACK_DISTANCE && !target->IsNonMeleeSpellCast(false, false, true) && + (!botAI->IsRanged(bot) || distanceToPullTarget > botAI->GetRange("spell"))) + return false; + + if (!botAI->HasStrategy("pull back", BOT_STATE_COMBAT)) + return true; + + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + if (!pullPosition.isSet() || pullPosition.mapId != bot->GetMapId()) + return true; + + return bot->GetDistance(pullPosition.x, pullPosition.y, pullPosition.z) <= botAI->GetRange("follow"); +} + +bool ReturnToPullPositionTrigger::IsActive() +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + Player* bot = botAI->GetBot(); + if (!bot) + return false; + + Unit* target = strategy ? strategy->GetTarget() : nullptr; + if (!strategy || !strategy->HasPullStarted() || !target || !target->IsInCombat() || + !botAI->HasStrategy("pull back", BOT_STATE_COMBAT)) + return false; + + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + return pullPosition.isSet() && pullPosition.mapId == bot->GetMapId() && + bot->GetDistance(pullPosition.x, pullPosition.y, pullPosition.z) > sPlayerbotAIConfig.followDistance; +} diff --git a/src/Ai/Base/Trigger/PullTriggers.h b/src/Ai/Base/Trigger/PullTriggers.h new file mode 100644 index 00000000000..d5603617753 --- /dev/null +++ b/src/Ai/Base/Trigger/PullTriggers.h @@ -0,0 +1,35 @@ +/* + * 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_PULLTRIGGERS_H +#define _PLAYERBOT_PULLTRIGGERS_H + +#include "Trigger.h" + +class PullStartTrigger : public Trigger +{ +public: + PullStartTrigger(PlayerbotAI* botAI, std::string const name = "pull start") : Trigger(botAI, name) {} + + bool IsActive() override; +}; + +class PullEndTrigger : public Trigger +{ +public: + PullEndTrigger(PlayerbotAI* botAI, std::string const name = "pull end") : Trigger(botAI, name) {} + + bool IsActive() override; +}; + +class ReturnToPullPositionTrigger : public Trigger +{ +public: + ReturnToPullPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to pull position") {} + + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index bfdddecf7a3..54edbb01794 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -16,6 +16,7 @@ #include "NewRpgStrategy.h" #include "NewRpgTriggers.h" #include "PvpTriggers.h" +#include "PullTriggers.h" #include "RpgTriggers.h" #include "RtiTriggers.h" #include "StuckTriggers.h" @@ -129,6 +130,9 @@ class TriggerContext : public NamedObjectContext creators["has attackers"] = &TriggerContext::has_attackers; creators["no possible targets"] = &TriggerContext::no_possible_targets; creators["possible adds"] = &TriggerContext::possible_adds; + creators["pull start"] = &TriggerContext::pull_start; + creators["pull end"] = &TriggerContext::pull_end; + creators["return to pull position"] = &TriggerContext::return_to_pull_position; creators["no drink"] = &TriggerContext::no_drink; creators["no food"] = &TriggerContext::no_food; @@ -280,6 +284,9 @@ class TriggerContext : public NamedObjectContext static Trigger* swimming(PlayerbotAI* botAI) { return new IsSwimmingTrigger(botAI); } static Trigger* no_possible_targets(PlayerbotAI* botAI) { return new NoPossibleTargetsTrigger(botAI); } static Trigger* possible_adds(PlayerbotAI* botAI) { return new PossibleAddsTrigger(botAI); } + static Trigger* pull_start(PlayerbotAI* botAI) { return new PullStartTrigger(botAI); } + static Trigger* pull_end(PlayerbotAI* botAI) { return new PullEndTrigger(botAI); } + static Trigger* return_to_pull_position(PlayerbotAI* botAI) { return new ReturnToPullPositionTrigger(botAI); } static Trigger* can_loot(PlayerbotAI* botAI) { return new CanLootTrigger(botAI); } static Trigger* far_from_loot_target(PlayerbotAI* botAI) { return new FarFromCurrentLootTrigger(botAI); } static Trigger* far_from_master(PlayerbotAI* botAI) { return new FarFromMasterTrigger(botAI); } diff --git a/src/Ai/Class/Dk/DKAiObjectContext.cpp b/src/Ai/Class/Dk/DKAiObjectContext.cpp index 9a8271aa704..85f8bc8cd85 100644 --- a/src/Ai/Class/Dk/DKAiObjectContext.cpp +++ b/src/Ai/Class/Dk/DKAiObjectContext.cpp @@ -8,11 +8,11 @@ #include "BloodDKStrategy.h" #include "DKActions.h" #include "DKTriggers.h" +#include "DeathKnightPullStrategy.h" #include "FrostDKStrategy.h" #include "GenericDKNonCombatStrategy.h" #include "GenericTriggers.h" #include "Playerbots.h" -#include "PullStrategy.h" #include "UnholyDKStrategy.h" class DeathKnightStrategyFactoryInternal : public NamedObjectContext @@ -28,7 +28,7 @@ class DeathKnightStrategyFactoryInternal : public NamedObjectContext private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericDKNonCombatStrategy(botAI); } - static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "icy touch"); } + static Strategy* pull(PlayerbotAI* botAI) { return new DeathKnightPullStrategy(botAI); } static Strategy* frost_aoe(PlayerbotAI* botAI) { return new FrostDKAoeStrategy(botAI); } static Strategy* unholy_aoe(PlayerbotAI* botAI) { return new UnholyDKAoeStrategy(botAI); } }; diff --git a/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp new file mode 100644 index 00000000000..be643b50ce0 --- /dev/null +++ b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp @@ -0,0 +1,43 @@ +/* + * 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 "DeathKnightPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string DeathKnightPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + Unit* target = GetTarget(); + if (!bot || !target || + (!botAI->HasStrategy("blood", BOT_STATE_COMBAT) && !botAI->HasStrategy("blood", BOT_STATE_NON_COMBAT))) + { + return PullStrategy::GetPullActionName(); + } + + uint32 const deathGripSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "death grip")->Get(); + if (deathGripSpellId && bot->HasSpell(deathGripSpellId) && + botAI->CanCastSpell(deathGripSpellId, target)) + { + return "death grip"; + } + + uint32 const icyTouchSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "icy touch")->Get(); + if (!icyTouchSpellId || !bot->HasSpell(icyTouchSpellId) || + !botAI->CanCastSpell(icyTouchSpellId, target)) + { + uint32 const darkCommandSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "dark command")->Get(); + if (darkCommandSpellId && bot->HasSpell(darkCommandSpellId) && + botAI->CanCastSpell(darkCommandSpellId, target)) + { + return "dark command"; + } + } + + return PullStrategy::GetPullActionName(); +} diff --git a/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h new file mode 100644 index 00000000000..ce80c69f64a --- /dev/null +++ b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h @@ -0,0 +1,19 @@ +/* + * 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_DEATH_KNIGHT_PULL_STRATEGY_H +#define _PLAYERBOT_DEATH_KNIGHT_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class DeathKnightPullStrategy : public PullStrategy +{ +public: + DeathKnightPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "icy touch") {} + + std::string GetPullActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 3d9086cffc9..4d74d1db357 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -19,6 +19,7 @@ #include "MeleeDruidStrategy.h" #include "OffhealDruidCatStrategy.h" #include "Playerbots.h" +#include "DruidPullStrategy.h" class DruidStrategyFactoryInternal : public NamedObjectContext { @@ -26,6 +27,7 @@ class DruidStrategyFactoryInternal : public NamedObjectContext DruidStrategyFactoryInternal() { creators["nc"] = &DruidStrategyFactoryInternal::nc; + creators["pull"] = &DruidStrategyFactoryInternal::pull; creators["cat aoe"] = &DruidStrategyFactoryInternal::cat_aoe; creators["caster aoe"] = &DruidStrategyFactoryInternal::caster_aoe; creators["caster debuff"] = &DruidStrategyFactoryInternal::caster_debuff; @@ -40,6 +42,7 @@ class DruidStrategyFactoryInternal : public NamedObjectContext private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericDruidNonCombatStrategy(botAI); } + static Strategy* pull(PlayerbotAI* botAI) { return new DruidPullStrategy(botAI); } static Strategy* cat_aoe(PlayerbotAI* botAI) { return new CatAoeDruidStrategy(botAI); } static Strategy* caster_aoe(PlayerbotAI* botAI) { return new CasterDruidAoeStrategy(botAI); } static Strategy* caster_debuff(PlayerbotAI* botAI) { return new CasterDruidDebuffStrategy(botAI); } diff --git a/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp new file mode 100644 index 00000000000..dc72b9e823d --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp @@ -0,0 +1,46 @@ +/* + * 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 "DruidPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string DruidPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + std::string actionName = PullStrategy::GetPullActionName(); + if (!bot) + return actionName; + + uint32 const faerieFireFeralId = botAI->GetAiObjectContext()->GetValue("spell id", "faerie fire (feral)")->Get(); + if (faerieFireFeralId && bot->HasSpell(faerieFireFeralId) && + (botAI->HasStrategy("bear", BOT_STATE_COMBAT) || botAI->HasStrategy("cat", BOT_STATE_COMBAT))) + { + actionName = "faerie fire (feral)"; + } + + Unit* target = GetTarget(); + uint32 const faerieFireSpellId = botAI->GetAiObjectContext()->GetValue("spell id", actionName)->Get(); + if (target && (!faerieFireSpellId || !bot->HasSpell(faerieFireSpellId) || + !botAI->CanCastSpell(faerieFireSpellId, target))) + { + uint32 const growlSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "growl")->Get(); + if (growlSpellId && bot->HasSpell(growlSpellId) && botAI->CanCastSpell(growlSpellId, target)) + return "growl"; + } + + return actionName; +} + +std::string DruidPullStrategy::GetPreActionName() const +{ + if (GetPullActionName() == "faerie fire") + return ""; + + return PullStrategy::GetPreActionName(); +} diff --git a/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h new file mode 100644 index 00000000000..9a52f262a44 --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h @@ -0,0 +1,20 @@ +/* + * 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_DRUID_PULL_STRATEGY_H +#define _PLAYERBOT_DRUID_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class DruidPullStrategy : public PullStrategy +{ +public: + DruidPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "faerie fire", "dire bear form") {} + + std::string GetPullActionName() const override; + std::string GetPreActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 7edbf5c8f49..a58ede3f88c 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -12,6 +12,7 @@ #include "OffhealRetPaladinStrategy.h" #include "PaladinActions.h" #include "PaladinBuffStrategies.h" +#include "PaladinPullStrategy.h" #include "PaladinTriggers.h" #include "Playerbots.h" #include "TankPaladinStrategy.h" @@ -22,6 +23,7 @@ class PaladinStrategyFactoryInternal : public NamedObjectContext PaladinStrategyFactoryInternal() { creators["nc"] = &PaladinStrategyFactoryInternal::nc; + creators["pull"] = &PaladinStrategyFactoryInternal::pull; creators["cure"] = &PaladinStrategyFactoryInternal::cure; creators["boost"] = &PaladinStrategyFactoryInternal::boost; creators["cc"] = &PaladinStrategyFactoryInternal::cc; @@ -31,6 +33,7 @@ class PaladinStrategyFactoryInternal : public NamedObjectContext private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericPaladinNonCombatStrategy(botAI); } + static Strategy* pull(PlayerbotAI* botAI) { return new PaladinPullStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new PaladinCureStrategy(botAI); } static Strategy* boost(PlayerbotAI* botAI) { return new PaladinBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new PaladinCcStrategy(botAI); } diff --git a/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp new file mode 100644 index 00000000000..ba0381b5ab3 --- /dev/null +++ b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp @@ -0,0 +1,46 @@ +/* + * 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 "PaladinPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string PaladinPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + Unit* target = GetTarget(); + if (!bot || !target || + (!botAI->HasStrategy("tank", BOT_STATE_COMBAT) && !botAI->HasStrategy("tank", BOT_STATE_NON_COMBAT))) + { + return PullStrategy::GetPullActionName(); + } + + uint32 const avengersShieldSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "avenger's shield")->Get(); + if (avengersShieldSpellId && bot->HasSpell(avengersShieldSpellId) && + botAI->CanCastSpell(avengersShieldSpellId, target)) + { + return "avenger's shield"; + } + + uint32 const handOfReckoningSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "hand of reckoning")->Get(); + if (handOfReckoningSpellId && bot->HasSpell(handOfReckoningSpellId) && + botAI->CanCastSpell(handOfReckoningSpellId, target)) + { + return "hand of reckoning"; + } + + return PullStrategy::GetPullActionName(); +} + +std::string PaladinPullStrategy::GetPreActionName() const +{ + if (botAI->HasStrategy("tank", BOT_STATE_COMBAT) || botAI->HasStrategy("tank", BOT_STATE_NON_COMBAT)) + return ""; + + return PullStrategy::GetPreActionName(); +} diff --git a/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h new file mode 100644 index 00000000000..43d014ec7c4 --- /dev/null +++ b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h @@ -0,0 +1,20 @@ +/* + * 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_PALADIN_PULL_STRATEGY_H +#define _PLAYERBOT_PALADIN_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class PaladinPullStrategy : public PullStrategy +{ +public: + PaladinPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "judgement", "seal of righteousness") {} + + std::string GetPullActionName() const override; + std::string GetPreActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp new file mode 100644 index 00000000000..cdae7e29cea --- /dev/null +++ b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp @@ -0,0 +1,27 @@ +/* + * 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 "WarriorPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" + +std::string WarriorPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + Unit* target = GetTarget(); + if (!bot || !target) + return PullStrategy::GetPullActionName(); + + uint32 const heroicThrowSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "heroic throw")->Get(); + if (heroicThrowSpellId && bot->HasSpell(heroicThrowSpellId) && + botAI->CanCastSpell(heroicThrowSpellId, target)) + { + return "heroic throw"; + } + + return PullStrategy::GetPullActionName(); +} diff --git a/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h new file mode 100644 index 00000000000..c63bb21eec0 --- /dev/null +++ b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h @@ -0,0 +1,19 @@ +/* + * 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_WARRIOR_PULL_STRATEGY_H +#define _PLAYERBOT_WARRIOR_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class WarriorPullStrategy : public PullStrategy +{ +public: + WarriorPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "shoot") {} + + std::string GetPullActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp b/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp index 22754beab42..78130772902 100644 --- a/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp +++ b/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp @@ -10,8 +10,8 @@ #include "GenericWarriorNonCombatStrategy.h" #include "NamedObjectContext.h" #include "Playerbots.h" -#include "PullStrategy.h" #include "TankWarriorStrategy.h" +#include "WarriorPullStrategy.h" #include "WarriorActions.h" #include "WarriorTriggers.h" @@ -28,7 +28,7 @@ class WarriorStrategyFactoryInternal : public NamedObjectContext private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericWarriorNonCombatStrategy(botAI); } static Strategy* warrior_aoe(PlayerbotAI* botAI) { return new WarrirorAoeStrategy(botAI); } - static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "shoot"); } + static Strategy* pull(PlayerbotAI* botAI) { return new WarriorPullStrategy(botAI); } }; class WarriorCombatStrategyFactoryInternal : public NamedObjectContext diff --git a/src/Bot/Engine/Engine.cpp b/src/Bot/Engine/Engine.cpp index bb4f2eb351d..3131f7514dc 100644 --- a/src/Bot/Engine/Engine.cpp +++ b/src/Bot/Engine/Engine.cpp @@ -428,6 +428,12 @@ void Engine::toggleStrategy(std::string const name) bool Engine::HasStrategy(std::string const name) { return strategies.find(name) != strategies.end(); } +Strategy* Engine::GetStrategy(std::string const name) +{ + std::map::iterator i = strategies.find(name); + return i != strategies.end() ? i->second : nullptr; +} + void Engine::ProcessTriggers(bool minimal) { std::unordered_map fires; diff --git a/src/Bot/Engine/Engine.h b/src/Bot/Engine/Engine.h index 8a7c34189d8..976252cc510 100644 --- a/src/Bot/Engine/Engine.h +++ b/src/Bot/Engine/Engine.h @@ -70,6 +70,7 @@ class Engine : public PlayerbotAIAware void addStrategiesNoInit(std::string first, ...); bool removeStrategy(std::string const name, bool init = true); bool HasStrategy(std::string const name); + Strategy* GetStrategy(std::string const name); void removeAllStrategies(); void toggleStrategy(std::string const name); std::string const ListStrategies(); diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index a821886f97e..6c638e8071c 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -315,7 +315,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) - engine->addStrategiesNoInit("tank", "tank assist", "aoe", nullptr); + engine->addStrategiesNoInit("tank", "tank assist", "pull", "pull back", "aoe", nullptr); else if (tab == WARRIOR_TAB_ARMS || !player->HasSpell(1680)) // Whirlwind engine->addStrategiesNoInit("arms", "aoe", "dps assist", nullptr); else @@ -333,7 +333,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION) - engine->addStrategiesNoInit("tank", "tank assist", "bthreat", "barmor", "cure", nullptr); + engine->addStrategiesNoInit("tank", "tank assist", "pull", "pull back", "bthreat", "barmor", "cure", nullptr); else if (tab == PALADIN_TAB_HOLY) engine->addStrategiesNoInit("heal", "dps assist", "cure", "bcast", nullptr); else @@ -352,7 +352,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa if (player->HasSpell(768) /*cat form*/ && !player->HasAura(16931) /*thick hide*/) engine->addStrategiesNoInit("cat", "dps assist", nullptr); else - engine->addStrategiesNoInit("bear", "tank assist", nullptr); + engine->addStrategiesNoInit("bear", "tank assist", "pull", "pull back", nullptr); } break; case CLASS_HUNTER: @@ -383,7 +383,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_DEATH_KNIGHT: if (tab == DEATH_KNIGHT_TAB_BLOOD) - engine->addStrategiesNoInit("blood", "tank assist", nullptr); + engine->addStrategiesNoInit("blood", "tank assist", "pull", "pull back", nullptr); else if (tab == DEATH_KNIGHT_TAB_FROST) engine->addStrategiesNoInit("frost", "frost aoe", "dps assist", nullptr); else @@ -510,7 +510,7 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION) { - nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "barmor", nullptr); + nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr); if (player->GetLevel() >= 20) nonCombatEngine->addStrategy("bhealth", false); else @@ -548,14 +548,14 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const if (player->GetLevel() >= 20 && !player->HasAura(16931) /*thick hide*/) nonCombatEngine->addStrategy("dps assist", false); else - nonCombatEngine->addStrategy("tank assist", false); + nonCombatEngine->addStrategiesNoInit("tank assist", "pull", nullptr); } else nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr); break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) - nonCombatEngine->addStrategy("tank assist", false); + nonCombatEngine->addStrategiesNoInit("tank assist", "pull", nullptr); else nonCombatEngine->addStrategy("dps assist", false); break; @@ -571,7 +571,7 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const break; case CLASS_DEATH_KNIGHT: if (tab == DEATH_KNIGHT_TAB_BLOOD) - nonCombatEngine->addStrategy("tank assist", false); + nonCombatEngine->addStrategiesNoInit("tank assist", "pull", nullptr); else nonCombatEngine->addStrategy("dps assist", false); break; diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index f02a79e70a1..7edc48affa0 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1795,6 +1795,11 @@ bool PlayerbotAI::ContainsStrategy(StrategyType type) bool PlayerbotAI::HasStrategy(std::string const name, BotState type) { return engines[type]->HasStrategy(name); } +Strategy* PlayerbotAI::GetStrategy(std::string const name, BotState type) +{ + return engines[type] ? engines[type]->GetStrategy(name) : nullptr; +} + void PlayerbotAI::ResetStrategies(bool load) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 5a0dc7485e6..dc7770ed8da 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -405,6 +405,7 @@ class PlayerbotAI : public PlayerbotAIBase void ChangeStrategy(std::string const name, BotState type); void ClearStrategies(BotState type); std::vector GetStrategies(BotState type); + Strategy* GetStrategy(std::string const name, BotState type); void ApplyInstanceStrategies(uint32 mapId, bool tellMaster = false); void EvaluateHealerDpsStrategy(); bool ContainsStrategy(StrategyType type); From 9c5b1d0027a7cbdb337437e5a373c1f932bc62eb Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 19 Apr 2026 18:49:09 +0200 Subject: [PATCH 75/87] Pull strategy GetTarget fix (#2316) ## Pull Request Description Added safe-guard condition for PullStrategy.GetTarget ## How to Test the Changes 1. Invite bot tank 2. Use `reset boAI` or `nc +pull,+pull back` + `co +pull,+pull back` 3. Order bot to pull using command `pull my target` or `pull rti target` on dead creature 4. Bot should run to mob, use ranged skill and back to point where he started pull Without `pull back` strategy bot run to mob, use ranged skill and wait on mob until he come to bot ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Fix proposed by @brighton-chi --- src/Ai/Base/Strategy/PullStrategy.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ai/Base/Strategy/PullStrategy.cpp b/src/Ai/Base/Strategy/PullStrategy.cpp index fce9172795a..3d5e3e94aef 100644 --- a/src/Ai/Base/Strategy/PullStrategy.cpp +++ b/src/Ai/Base/Strategy/PullStrategy.cpp @@ -55,7 +55,8 @@ Unit* PullStrategy::GetTarget() const Unit* target = botAI->GetUnit(guid); Player* bot = botAI->GetBot(); - if (!bot || !target || !target->IsInWorld() || target->GetMapId() != bot->GetMapId()) + if (!bot || !target || !target->IsAlive() || !target->IsInWorld() || + target->GetMapId() != bot->GetMapId()) return nullptr; return target; From 52273b4971c923220bcbefa5c8bcaefe648a1968 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Tue, 21 Apr 2026 19:37:54 +0200 Subject: [PATCH 76/87] Pull multiplier fix (#2317) ## Pull Request Description Fix for pull strategy multiplier and ending pull command ## How to Test the Changes 1. Invite tank bot 2. Order him to attack mob (not pull) ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Strategy/PullStrategy.cpp | 3 +++ src/Ai/Base/Trigger/PullTriggers.cpp | 8 +------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Ai/Base/Strategy/PullStrategy.cpp b/src/Ai/Base/Strategy/PullStrategy.cpp index 3d5e3e94aef..31351c57183 100644 --- a/src/Ai/Base/Strategy/PullStrategy.cpp +++ b/src/Ai/Base/Strategy/PullStrategy.cpp @@ -170,6 +170,9 @@ float PullMultiplier::GetValue(Action* action) if (!strategy || !strategy->HasTarget() || !action) return 1.0f; + if (!strategy->IsPullPendingToStart() && !strategy->HasPullStarted()) + return 1.0f; + std::string const actionName = action->getName(); if (actionName == "pull my target" || actionName == "pull rti target" || diff --git a/src/Ai/Base/Trigger/PullTriggers.cpp b/src/Ai/Base/Trigger/PullTriggers.cpp index 84c550fafda..4d23b3896ff 100644 --- a/src/Ai/Base/Trigger/PullTriggers.cpp +++ b/src/Ai/Base/Trigger/PullTriggers.cpp @@ -20,15 +20,12 @@ bool PullStartTrigger::IsActive() bool PullEndTrigger::IsActive() { PullStrategy const* strategy = PullStrategy::Get(botAI); - Player* bot = botAI->GetBot(); - if (!bot) - return false; if (!strategy || !strategy->HasPullStarted()) return false; Unit* target = strategy->GetTarget(); - if (!target || !target->IsInWorld()) + if (!target || !target->IsInWorld() || !target->IsAlive()) return true; time_t const secondsSincePullStarted = time(nullptr) - strategy->GetPullStartTime(); @@ -53,9 +50,6 @@ bool PullEndTrigger::IsActive() bool ReturnToPullPositionTrigger::IsActive() { PullStrategy const* strategy = PullStrategy::Get(botAI); - Player* bot = botAI->GetBot(); - if (!bot) - return false; Unit* target = strategy ? strategy->GetTarget() : nullptr; if (!strategy || !strategy->HasPullStarted() || !target || !target->IsInCombat() || From a4b37c9fbcded3580ead2b860ad7972340b5d41b Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:27:06 -0700 Subject: [PATCH 77/87] Revert "Feat: Reintroduce timed logouts" (#2329) Reverts mod-playerbots/mod-playerbots#2289 --- src/Bot/PlayerbotAI.cpp | 47 ++++++++++++++++---------- src/Bot/PlayerbotMgr.cpp | 72 ++++++++++++---------------------------- 2 files changed, 51 insertions(+), 68 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 7edc48affa0..25c93676f70 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -243,22 +243,10 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) nextAICheckDelay = 0; // Early return if bot is in invalid state - if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->IsDuringRemoveFromWorld()) + if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || + bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) return; - // During timed logout countdown, cancel if bot enters combat (this cancellation is handled client-side for real players). - if (bot->GetSession()->isLogingOut()) - { - bool canLogoutInCombat = bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING); - if (bot->IsInCombat() && !canLogoutInCombat) - { - WorldPackets::Character::LogoutCancel cancelData = WorldPacket(CMSG_LOGOUT_CANCEL); - bot->GetSession()->HandleLogoutCancelOpcode(cancelData); - } - else - return; - } - // Handle cheat options (set bot health and power if cheats are enabled) if (bot->IsAlive() && (static_cast(GetCheat()) > 0 || static_cast(sPlayerbotAIConfig.botCheatMask) > 0)) @@ -727,9 +715,30 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr Reset(true); } - // Commented-out logout commands blocks removed from here and implemented in HandleCommand. - // Remaining is a commented-out action delay command block. - /* + // TODO: missing implementation to port + /*else if (filtered == "logout") + { + if (!(bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut())) + { + if (type == CHAT_MSG_WHISPER) + TellPlayer(&fromPlayer, BOT_TEXT("logout_start")); + + if (master && master->GetPlayerbotMgr()) + SetShouldLogOut(true); + } + } + else if (filtered == "logout cancel") + { + if (bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut()) + { + if (type == CHAT_MSG_WHISPER) + TellPlayer(&fromPlayer, BOT_TEXT("logout_cancel")); + + WorldPacket p; + bot->GetSession()->HandleLogoutCancelOpcode(p); + SetShouldLogOut(false); + } + } else if ((filtered.size() > 5) && (filtered.substr(0, 5) == "wait ") && (filtered.find("wait for attack") == std::string::npos)) { @@ -1075,7 +1084,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro TellMaster(message); } } - else if (filtered == "cancel logout" || filtered == "logout cancel") + else if (filtered == "logout cancel") { if (!bot->GetSession()->isLogingOut()) return; @@ -1091,7 +1100,9 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro bot->GetSession()->HandleLogoutCancelOpcode(data); } else + { chatCommands.push_back(ChatCommandHolder(filtered, fromPlayer, type)); + } } void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index fd205fe2376..8327e2f3674 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -299,11 +299,6 @@ void PlayerbotHolder::LogoutAllBots() if (!botAI || botAI->IsRealPlayer()) continue; - // If bot is mid-countdown, cancel the timer so LogoutPlayerBot proceeds immediately. - WorldSession* session = bot->GetSession(); - if (session && session->isLogingOut()) - session->SetLogoutStartTime(0); - LogoutPlayerBot(bot->GetGUID()); } } @@ -366,50 +361,36 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) WorldSession* botWorldSessionPtr = bot->GetSession(); WorldSession* masterWorldSessionPtr = nullptr; - // If already in timed logout countdown, complete it once the 20-second timer expires. if (botWorldSessionPtr->isLogingOut()) - { - if (botWorldSessionPtr->ShouldLogOut(time(nullptr))) - { - std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( - "goodbye", "Goodbye!", {}); - botAI->TellMaster(message); - RemoveFromPlayerbotsMap(guid); - botWorldSessionPtr->LogoutPlayer(true); - delete botWorldSessionPtr; - } return; - } Player* master = botAI->GetMaster(); if (master) masterWorldSessionPtr = master->GetSession(); - // Instant logout checking: - bool logout = - bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || - bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || - (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer()) || - // Master's socket is already gone (EXIT GAME -> EXIT NOW is the most typical cause). - // Force instant logout. Without this, the bot restarts its 20-second countdown and fires LogoutPlayer() 20 seconds - // after the master's Player object has been deleted, causing the bot's logout to crash on the now deleted master. - (masterWorldSessionPtr && masterWorldSessionPtr->IsSocketClosed()) || - (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) || - // If the bot's master has security clearance for `InstantLogout` in worldserver.conf, so does the bot. - (master && - (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || - master->HasUnitState(UNIT_STATE_IN_FLIGHT) || - (masterWorldSessionPtr && - masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))); - - if (!logout) - { - // Start the 20-second logout countdown. CancelLogout() can interrupt this. - WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); - botWorldSessionPtr->HandleLogoutRequestOpcode(data); - return; - } + // TODO: Review whether or not to implement timed logout. + // Unused block. Useful only for timed logout. +/* + // check for instant logout + bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr)); + + if (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) + logout = true; + + if (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer()) + logout = true; + + if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || + botWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)) + logout = true; + if (master && + (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || master->HasUnitState(UNIT_STATE_IN_FLIGHT) || + (masterWorldSessionPtr && + masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))) + logout = true; +*/ + // Instant logout (the only option right now) { std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( "goodbye", "Goodbye!", {}); @@ -1497,15 +1478,6 @@ void PlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) { SetNextCheckDelay(sPlayerbotAIConfig.reactDelay); CheckTellErrors(elapsed); - - // Complete timed logouts for added bots once the 20-second countdown has elapsed. - std::vector expiredLogouts; - for (auto const& [botGuid, bot] : playerBots) - if (bot && bot->GetSession() && bot->GetSession()->ShouldLogOut(time(nullptr))) - expiredLogouts.push_back(botGuid); - - for (ObjectGuid const& guid : expiredLogouts) - LogoutPlayerBot(guid); } void PlayerbotMgr::HandleCommand(uint32 type, std::string const text) From 866a73dfbf131333f8cda1d8aab958021b6bf42f Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:03:36 -0700 Subject: [PATCH 78/87] Clean up unused variables (#2268) ## Pull Request Description Clean up a bunch of additional unused variable warnings. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Claude reviewed the warnings log from a build and suggested a series of changes. I focused just on these warnings for now. Every line was reviewed. Some sections need to be reviewed by author for intent. ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- .../AutoMaintenanceOnLevelupAction.cpp | 2 +- src/Ai/Base/Actions/BankAction.cpp | 2 +- src/Ai/Base/Actions/CastCustomSpellAction.h | 4 +- src/Ai/Base/Actions/ChatShortcutActions.cpp | 2 +- src/Ai/Base/Actions/ChooseRpgTargetAction.cpp | 1 + src/Ai/Base/Actions/DropQuestAction.cpp | 31 +------------- src/Ai/Base/Actions/FollowActions.cpp | 4 +- src/Ai/Base/Actions/GuildBankAction.cpp | 2 +- src/Ai/Base/Actions/MailAction.cpp | 8 ++-- src/Ai/Base/Actions/MovementActions.cpp | 27 ++++-------- src/Ai/Base/Actions/MovementActions.h | 5 +-- src/Ai/Base/Actions/ReadyCheckAction.cpp | 12 +++--- src/Ai/Base/Actions/ReleaseSpiritAction.cpp | 6 +-- src/Ai/Base/Actions/ReleaseSpiritAction.h | 2 +- src/Ai/Base/Actions/SayAction.cpp | 10 ++--- src/Ai/Base/Actions/SayAction.h | 8 ++-- src/Ai/Base/Actions/SeeSpellAction.cpp | 2 +- src/Ai/Base/Actions/UseMeetingStoneAction.cpp | 2 +- .../Strategy/ChatCommandHandlerStrategy.cpp | 2 +- src/Ai/Base/Strategy/CombatStrategy.cpp | 6 +-- src/Ai/Base/Strategy/DuelStrategy.cpp | 2 +- src/Ai/Base/Strategy/FollowMasterStrategy.cpp | 2 +- src/Ai/Base/Strategy/GuardStrategy.cpp | 2 +- src/Ai/Base/Strategy/NonCombatStrategy.cpp | 2 +- src/Ai/Base/Strategy/RTSCStrategy.cpp | 2 +- src/Ai/Base/Strategy/RacialsStrategy.cpp | 2 +- src/Ai/Base/Strategy/UsePotionsStrategy.cpp | 2 +- src/Ai/Base/Value/Arrow.cpp | 3 +- src/Ai/Base/Value/CcTargetValue.cpp | 2 +- src/Ai/Base/Value/CurrentCcTargetValue.cpp | 2 +- src/Ai/Base/Value/DpsTargetValue.cpp | 2 +- src/Ai/Base/Value/ItemCountValue.cpp | 2 - src/Ai/Base/Value/ItemCountValue.h | 2 +- src/Ai/Base/Value/ItemUsageValue.cpp | 2 - src/Ai/Base/Value/LeastHpTargetValue.cpp | 2 +- src/Ai/Base/Value/LootStrategyValue.cpp | 2 +- src/Ai/Base/Value/NearestCorpsesValue.cpp | 2 +- src/Ai/Base/Value/PartyMemberValue.cpp | 2 +- src/Ai/Base/Value/TankTargetValue.cpp | 2 +- src/Ai/Base/Value/TargetValue.cpp | 2 +- .../Strategy/GenericDKNonCombatStrategy.cpp | 2 +- .../Druid/Strategy/CatDpsDruidStrategy.cpp | 2 +- src/Ai/Class/Hunter/Action/HunterActions.h | 8 ++-- .../Strategy/GenericPaladinStrategy.cpp | 2 +- .../Strategy/GenericWarlockStrategy.cpp | 4 +- .../Warlock/Strategy/TankWarlockStrategy.cpp | 2 +- .../Warrior/Strategy/ArmsWarriorStrategy.cpp | 16 +++---- .../Warrior/Strategy/FuryWarriorStrategy.cpp | 10 ++--- .../Warrior/Strategy/TankWarriorStrategy.cpp | 20 ++++----- .../PitOfSaron/Action/PitOfSaronActions.cpp | 2 +- .../Multiplier/PitOfSaronMultipliers.cpp | 2 +- .../Strategy/TrialOfTheChampionStrategy.cpp | 2 +- .../Multiplier/RaidGruulsLairMultipliers.cpp | 2 +- .../Trigger/RaidGruulsLairTriggers.cpp | 8 ++-- .../GruulsLair/Util/RaidGruulsLairHelpers.cpp | 4 +- .../GruulsLair/Util/RaidGruulsLairHelpers.h | 4 +- .../Raid/Icecrown/Action/RaidIccActions.cpp | 28 ++++++------- src/Ai/Raid/Icecrown/Action/RaidIccActions.h | 10 ++--- .../Raid/Icecrown/Trigger/RaidIccTriggers.cpp | 2 +- .../Karazhan/Action/RaidKarazhanActions.cpp | 8 ++-- .../Karazhan/Util/RaidKarazhanHelpers.cpp | 4 +- .../Raid/Karazhan/Util/RaidKarazhanHelpers.h | 2 +- .../Action/RaidMagtheridonActions.cpp | 4 +- .../Action/RaidMagtheridonActions.h | 2 +- .../Util/RaidMagtheridonHelpers.cpp | 2 +- .../Onyxia/Strategy/RaidOnyxiaStrategy.cpp | 2 +- .../Action/RaidSSCActions.cpp | 42 ++++++++----------- .../Action/RaidSSCActions.h | 14 +++---- .../Multiplier/RaidSSCMultipliers.cpp | 2 - .../Action/RaidTempestKeepActions.cpp | 4 +- .../Action/RaidTempestKeepActions.h | 2 +- .../Raid/Ulduar/Action/RaidUlduarActions.cpp | 5 +-- .../Multiplier/RaidZulAmanMultipliers.cpp | 2 +- .../ZulAman/Trigger/RaidZulAmanTriggers.cpp | 6 +-- .../Raid/ZulAman/Util/RaidZulAmanHelpers.cpp | 2 +- src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h | 2 +- src/Bot/Engine/Engine.cpp | 2 +- src/Bot/Factory/PlayerbotFactory.cpp | 6 +-- src/Bot/PlayerbotAI.cpp | 4 +- src/Bot/PlayerbotMgr.cpp | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 2 +- src/Db/PlayerbotRepository.cpp | 2 +- src/Mgr/Item/ItemVisitors.h | 3 -- src/Mgr/Item/RandomItemMgr.cpp | 4 +- src/Mgr/Item/StatsWeightCalculator.cpp | 2 +- src/Mgr/Talent/Talentspec.cpp | 4 +- src/Script/Playerbots.cpp | 4 +- src/Util/ServerFacade.cpp | 2 +- 88 files changed, 196 insertions(+), 255 deletions(-) diff --git a/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp b/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp index 72433c15fdc..75152d87b32 100644 --- a/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp +++ b/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp @@ -73,7 +73,7 @@ void AutoMaintenanceOnLevelupAction::LearnSpells(std::ostringstream* out) LearnQuestSpells(out); } -void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* out) +void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* /*out*/) { PlayerbotFactory factory(bot, bot->GetLevel()); factory.InitSkills(); diff --git a/src/Ai/Base/Actions/BankAction.cpp b/src/Ai/Base/Actions/BankAction.cpp index 4d8d6c4d8c3..5a4975f77b2 100644 --- a/src/Ai/Base/Actions/BankAction.cpp +++ b/src/Ai/Base/Actions/BankAction.cpp @@ -27,7 +27,7 @@ bool BankAction::Execute(Event event) return false; } -bool BankAction::ExecuteBank(std::string const text, Unit* bank) +bool BankAction::ExecuteBank(std::string const text, Unit* /*bank*/) { if (text.empty() || text == "?") { diff --git a/src/Ai/Base/Actions/CastCustomSpellAction.h b/src/Ai/Base/Actions/CastCustomSpellAction.h index ed53b18a5ae..6cfc1e68929 100644 --- a/src/Ai/Base/Actions/CastCustomSpellAction.h +++ b/src/Ai/Base/Actions/CastCustomSpellAction.h @@ -21,7 +21,7 @@ class CastCustomSpellAction : public InventoryAction } bool Execute(Event event) override; - virtual std::string const castString(WorldObject* target) { return "cast"; } + virtual std::string const castString(WorldObject* /*target*/) { return "cast"; } protected: bool ncCast = false; @@ -49,7 +49,7 @@ class CastRandomSpellAction : public ListSpellsAction bool isUseful() override { return false; } virtual bool AcceptSpell(SpellInfo const* spellInfo); - virtual uint32 GetSpellPriority(SpellInfo const* spellInfo) { return 1; } + virtual uint32 GetSpellPriority(SpellInfo const* /*spellInfo*/) { return 1; } virtual bool castSpell(uint32 spellId, WorldObject* wo); bool Execute(Event event) override; diff --git a/src/Ai/Base/Actions/ChatShortcutActions.cpp b/src/Ai/Base/Actions/ChatShortcutActions.cpp index 0caeb5e0dc1..b715cb2bb54 100644 --- a/src/Ai/Base/Actions/ChatShortcutActions.cpp +++ b/src/Ai/Base/Actions/ChatShortcutActions.cpp @@ -80,7 +80,7 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) true, priority); } - if (Pet* pet = bot->GetPet()) + if (bot->GetPet()) botAI->PetFollow(); if (moved) diff --git a/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp b/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp index 76e6fe05f7b..8d15ae9be9b 100644 --- a/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp +++ b/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp @@ -116,6 +116,7 @@ bool ChooseRpgTargetAction::Execute(Event /*event*/) GuidPosition masterRpgTarget; if (master && master != bot && GET_PLAYERBOT_AI(master) && master->GetMapId() == bot->GetMapId() && !master->IsBeingTeleported()) { + //TODO Implement Player* player = botAI->GetMaster(); //GuidPosition masterRpgTarget = PAI_VALUE(GuidPosition, "rpg target"); //not used, line marked for removal. } diff --git a/src/Ai/Base/Actions/DropQuestAction.cpp b/src/Ai/Base/Actions/DropQuestAction.cpp index b3cba9c5602..61ab4254a79 100644 --- a/src/Ai/Base/Actions/DropQuestAction.cpp +++ b/src/Ai/Base/Actions/DropQuestAction.cpp @@ -68,25 +68,13 @@ bool CleanQuestLogAction::Execute(Event event) } if (!sPlayerbotAIConfig.dropObsoleteQuests) - { return false; - } // Only output this message if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { botAI->TellMaster("Clean Quest Log command received, removing grey/trivial quests..."); - } uint8 botLevel = bot->GetLevel(); // Get bot's level - uint8 numQuest = 0; - for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) - { - if (bot->GetQuestSlotQuestId(slot)) - { - numQuest++; - } - } for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { @@ -101,34 +89,24 @@ bool CleanQuestLogAction::Execute(Event event) // Determine if quest is trivial by comparing levels int32 questLevel = quest->GetQuestLevel(); if (questLevel == -1) // For scaling quests, default to bot level - { questLevel = botLevel; - } // Set the level difference for when a quest becomes trivial // This was determined by using the Lua code the client uses int32 trivialLevel = 5; if (botLevel >= 40) - { trivialLevel = 8; - } else if (botLevel >= 30) - { trivialLevel = 7; - } else if (botLevel >= 20) - { trivialLevel = 6; - } // Check if the quest is trivial (grey) for the bot if ((botLevel - questLevel) > trivialLevel) { // Output only if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey)."); - } // Remove quest botAI->rpgStatistic.questDropped++; @@ -137,8 +115,6 @@ bool CleanQuestLogAction::Execute(Event event) bot->SetQuestStatus(questId, QUEST_STATUS_NONE); bot->RemoveRewardedQuest(questId); - numQuest--; - if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { const std::string text_quest = ChatHelper::FormatQuest(quest); @@ -147,17 +123,13 @@ bool CleanQuestLogAction::Execute(Event event) } if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] has been removed."); - } } else { // Only output if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] is not trivial and will be kept."); - } } } @@ -174,7 +146,6 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG { std::random_device rd; std::mt19937 g(rd()); - std::shuffle(slots.begin(), slots.end(), g); } @@ -200,8 +171,10 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG bot->GetLevel() <= bot->GetQuestLevel(quest) + uint32(lowLevelDiff)) // Quest is not gray { if (bot->GetLevel() + 5 > bot->GetQuestLevel(quest)) // Quest is not red + { if (!isGreen) continue; + } } else // Quest is gray { diff --git a/src/Ai/Base/Actions/FollowActions.cpp b/src/Ai/Base/Actions/FollowActions.cpp index 48ce74ad347..f82b05368fd 100644 --- a/src/Ai/Base/Actions/FollowActions.cpp +++ b/src/Ai/Base/Actions/FollowActions.cpp @@ -168,8 +168,8 @@ bool FollowAction::Execute(Event /*event*/) ? MovementPriority::MOVEMENT_COMBAT : MovementPriority::MOVEMENT_NORMAL; - bool const movingAllowed = IsMovingAllowed(mapId, destX, destY, destZ); - bool const dupMove = IsDuplicateMove(mapId, destX, destY, destZ); + bool const movingAllowed = IsMovingAllowed(); + bool const dupMove = IsDuplicateMove(destX, destY, destZ); bool const waiting = IsWaitingForLastMove(priority); if (movingAllowed && !dupMove && !waiting) diff --git a/src/Ai/Base/Actions/GuildBankAction.cpp b/src/Ai/Base/Actions/GuildBankAction.cpp index 9693556d783..81fb0f4bd5c 100644 --- a/src/Ai/Base/Actions/GuildBankAction.cpp +++ b/src/Ai/Base/Actions/GuildBankAction.cpp @@ -53,7 +53,7 @@ bool GuildBankAction::Execute(std::string const text, GameObject* bank) return result; } -bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* bank) +bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* /*bank*/) { uint32 playerSlot = item->GetSlot(); uint32 playerBag = item->GetBagSlot(); diff --git a/src/Ai/Base/Actions/MailAction.cpp b/src/Ai/Base/Actions/MailAction.cpp index 6fd07e1e94f..80f6002a3ce 100644 --- a/src/Ai/Base/Actions/MailAction.cpp +++ b/src/Ai/Base/Actions/MailAction.cpp @@ -78,7 +78,7 @@ class TellMailProcessor : public MailProcessor class TakeMailProcessor : public MailProcessor { public: - bool Process(uint32 index, Mail* mail, PlayerbotAI* botAI) override + bool Process(uint32 /*index*/, Mail* mail, PlayerbotAI* botAI) override { Player* bot = botAI->GetBot(); if (!CheckBagSpace(bot)) @@ -104,7 +104,7 @@ class TakeMailProcessor : public MailProcessor { std::vector guids; for (MailItemInfoVec::iterator i = mail->items.begin(); i != mail->items.end(); ++i) - if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(i->item_template)) + if (sObjectMgr->GetItemTemplate(i->item_template)) guids.push_back(i->item_guid); for (std::vector::iterator i = guids.begin(); i != guids.end(); ++i) @@ -157,7 +157,7 @@ class TakeMailProcessor : public MailProcessor class DeleteMailProcessor : public MailProcessor { public: - bool Process(uint32 index, Mail* mail, PlayerbotAI* botAI) override + bool Process(uint32 /*index*/, Mail* mail, PlayerbotAI* botAI) override { std::ostringstream out; out << "|cffffffff" << mail->subject << "|cffff0000 deleted"; @@ -172,7 +172,7 @@ class DeleteMailProcessor : public MailProcessor class ReadMailProcessor : public MailProcessor { public: - bool Process(uint32 index, Mail* mail, PlayerbotAI* botAI) override + bool Process(uint32 /*index*/, Mail* mail, PlayerbotAI* botAI) override { std::ostringstream out, body; out << "|cffffffff" << mail->subject; diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 85855f60dc9..18e76ca7276 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -63,10 +63,10 @@ void MovementAction::CreateWp(Player* wpOwner, float x, float y, float z, float bool MovementAction::JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority) { UpdateMovementState(); - if (!IsMovingAllowed(mapId, x, y, z)) + if (!IsMovingAllowed()) return false; - if (IsDuplicateMove(mapId, x, y, z)) + if (IsDuplicateMove(x, y, z)) return false; if (IsWaitingForLastMove(priority)) @@ -171,11 +171,11 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool exact_waypoint, MovementPriority priority, bool lessDelay, bool backwards) { UpdateMovementState(); - if (!IsMovingAllowed(mapId, x, y, z)) + if (!IsMovingAllowed()) { return false; } - if (IsDuplicateMove(mapId, x, y, z)) + if (IsDuplicateMove(x, y, z)) { return false; } @@ -897,20 +897,7 @@ bool MovementAction::IsMovingAllowed(WorldObject* target) return IsMovingAllowed(); } -bool MovementAction::IsMovingAllowed(uint32 mapId, float x, float y, float z) -{ - // removed sqrt as means distance limit was effectively 22500 (ReactDistance�) - // leaving it commented incase we find ReactDistance limit causes problems - // float distance = sqrt(bot->GetDistance(x, y, z)); - - // Remove react distance limit - // if (!bot->InBattleground()) - // return false; - - return IsMovingAllowed(); -} - -bool MovementAction::IsDuplicateMove(uint32 mapId, float x, float y, float z) +bool MovementAction::IsDuplicateMove(float x, float y, float z) { LastMovement& lastMove = *context->GetValue("last movement"); @@ -1286,7 +1273,7 @@ bool MovementAction::Follow(Unit* target, float distance, float angle) return true; } -bool MovementAction::ChaseTo(WorldObject* obj, float distance, float angle) +bool MovementAction::ChaseTo(WorldObject* obj, float distance) { if (!IsMovingAllowed()) { @@ -1859,7 +1846,7 @@ bool FleeAction::isUseful() bool FleeWithPetAction::Execute(Event /*event*/) { - if (Pet* pet = bot->GetPet()) + if (bot->GetPet()) botAI->PetFollow(); return Flee(AI_VALUE(Unit*, "current target")); diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index 377e8360a3d..b1c56621189 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -43,14 +43,13 @@ class MovementAction : public Action float GetFollowAngle(); bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance); bool Follow(Unit* target, float distance, float angle); - bool ChaseTo(WorldObject* obj, float distance = 0.0f, float angle = 0.0f); + bool ChaseTo(WorldObject* obj, float distance = 0.0f); bool ReachCombatTo(Unit* target, float distance = 0.0f); float MoveDelay(float distance, bool backwards = false); void WaitForReach(float distance); void SetNextMovementDelay(float delayMillis); bool IsMovingAllowed(WorldObject* target); - bool IsMovingAllowed(uint32 mapId, float x, float y, float z); - bool IsDuplicateMove(uint32 mapId, float x, float y, float z); + bool IsDuplicateMove(float x, float y, float z); bool IsWaitingForLastMove(MovementPriority priority); bool IsMovingAllowed(); bool Flee(Unit* target); diff --git a/src/Ai/Base/Actions/ReadyCheckAction.cpp b/src/Ai/Base/Actions/ReadyCheckAction.cpp index 1c510d69d41..625deb8a4be 100644 --- a/src/Ai/Base/Actions/ReadyCheckAction.cpp +++ b/src/Ai/Base/Actions/ReadyCheckAction.cpp @@ -45,7 +45,7 @@ std::once_flag ReadyChecker::initFlag; class HealthChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* /*botAI*/, AiObjectContext* context) override { return AI_VALUE2(uint8, "health", "self target") > sPlayerbotAIConfig.almostFullHealth; } @@ -56,7 +56,7 @@ class HealthChecker : public ReadyChecker class ManaChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* /*botAI*/, AiObjectContext* context) override { return !AI_VALUE2(bool, "has mana", "self target") || AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; @@ -68,7 +68,7 @@ class ManaChecker : public ReadyChecker class DistanceChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* botAI, AiObjectContext* /*context*/) override { Player* bot = botAI->GetBot(); if (Player* master = botAI->GetMaster()) @@ -90,7 +90,7 @@ class DistanceChecker : public ReadyChecker class HunterChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* botAI, AiObjectContext* /*context*/) override { Player* bot = botAI->GetBot(); if (bot->getClass() == CLASS_HUNTER) @@ -126,7 +126,7 @@ class ItemCountChecker : public ReadyChecker public: ItemCountChecker(std::string const item, std::string const name) : item(item), name(name) {} - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* /*botAI*/, AiObjectContext* context) override { return AI_VALUE2(uint32, "item count", item) > 0; } @@ -225,4 +225,4 @@ bool ReadyCheckAction::ReadyCheck() return true; } -bool FinishReadyCheckAction::Execute(Event event) { return ReadyCheck(); } +bool FinishReadyCheckAction::Execute(Event /*event*/) { return ReadyCheck(); } diff --git a/src/Ai/Base/Actions/ReleaseSpiritAction.cpp b/src/Ai/Base/Actions/ReleaseSpiritAction.cpp index 9a923d4fd38..19bb870db20 100644 --- a/src/Ai/Base/Actions/ReleaseSpiritAction.cpp +++ b/src/Ai/Base/Actions/ReleaseSpiritAction.cpp @@ -65,7 +65,7 @@ void ReleaseSpiritAction::IncrementDeathCount() const } } -void ReleaseSpiritAction::LogRelease(const std::string& releaseMsg, bool isAutoRelease) const +void ReleaseSpiritAction::LogRelease(const std::string& releaseMsg) const { const std::string teamPrefix = bot->GetTeamId() == TEAM_ALLIANCE ? "A" : "H"; @@ -82,13 +82,13 @@ bool AutoReleaseSpiritAction::Execute(Event /*event*/) { IncrementDeathCount(); bot->DurabilityRepairAll(false, 1.0f, false); - LogRelease("auto released", true); + LogRelease("auto released"); WorldPacket packet(CMSG_REPOP_REQUEST); packet << uint8(0); bot->GetSession()->HandleRepopRequestOpcode(packet); - LogRelease("releases spirit", true); + LogRelease("releases spirit"); if (bot->InBattleground()) { diff --git a/src/Ai/Base/Actions/ReleaseSpiritAction.h b/src/Ai/Base/Actions/ReleaseSpiritAction.h index 57851214a1e..af5be1da8c3 100644 --- a/src/Ai/Base/Actions/ReleaseSpiritAction.h +++ b/src/Ai/Base/Actions/ReleaseSpiritAction.h @@ -18,7 +18,7 @@ class ReleaseSpiritAction : public Action : Action(botAI, name) {} bool Execute(Event event) override; - void LogRelease(const std::string& releaseType, bool isAutoRelease = false) const; + void LogRelease(const std::string& releaseType) const; protected: void IncrementDeathCount() const; diff --git a/src/Ai/Base/Actions/SayAction.cpp b/src/Ai/Base/Actions/SayAction.cpp index 9580fe228aa..1e8af760533 100644 --- a/src/Ai/Base/Actions/SayAction.cpp +++ b/src/Ai/Base/Actions/SayAction.cpp @@ -154,7 +154,7 @@ bool SayAction::isUseful() return (time(nullptr) - lastSaid) > 30; } -void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint32& guid2, std::string& msg, std::string& chanName, std::string& name) +void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, std::string& msg, std::string& chanName, std::string& name) { std::string respondsText = ""; @@ -205,14 +205,14 @@ void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint if (msg.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && (GET_PLAYERBOT_AI(bot)->GetChatHelper()->ExtractAllItemIds(msg).size() > 0 || GET_PLAYERBOT_AI(bot)->GetChatHelper()->ExtractAllQuestIds(msg).size() > 0)) { - HandleToxicLinksReply(bot, chatChannelSource, msg, name); + HandleToxicLinksReply(bot, chatChannelSource); return; } //thunderfury if (GET_PLAYERBOT_AI(bot)->GetChatHelper()->ExtractAllItemIds(msg).count(19019)) { - HandleThunderfuryReply(bot, chatChannelSource, msg, name); + HandleThunderfuryReply(bot, chatChannelSource); return; } @@ -220,7 +220,7 @@ void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint SendGeneralResponse(bot, chatChannelSource, messageRepy, name); } -bool ChatReplyAction::HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name) +bool ChatReplyAction::HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource) { std::map placeholders; const auto thunderfury = sObjectMgr->GetItemTemplate(19019); @@ -248,7 +248,7 @@ bool ChatReplyAction::HandleThunderfuryReply(Player* bot, ChatChannelSource chat return true; } -bool ChatReplyAction::HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name) +bool ChatReplyAction::HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource) { //quests std::vector incompleteQuests; diff --git a/src/Ai/Base/Actions/SayAction.h b/src/Ai/Base/Actions/SayAction.h index 5bf9a8f044b..cae5ee44495 100644 --- a/src/Ai/Base/Actions/SayAction.h +++ b/src/Ai/Base/Actions/SayAction.h @@ -29,12 +29,12 @@ class ChatReplyAction : public Action { public: ChatReplyAction(PlayerbotAI* ai) : Action(ai, "chat message") {} - virtual bool Execute(Event event) { return true; } + virtual bool Execute(Event /*event*/) { return true; } bool isUseful() { return true; } - static void ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint32& guid2, std::string& msg, std::string& chanName, std::string& name); - static bool HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); - static bool HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); + static void ChatReplyDo(Player* bot, uint32& type, uint32& guid1, std::string& msg, std::string& chanName, std::string& name); + static bool HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource); + static bool HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource); static bool HandleWTBItemsReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); static bool HandleLFGQuestsReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); static bool SendGeneralResponse(Player* bot, ChatChannelSource chatChannelSource, std::string& responseMessage, std::string& name); diff --git a/src/Ai/Base/Actions/SeeSpellAction.cpp b/src/Ai/Base/Actions/SeeSpellAction.cpp index f7d7eab2398..d93b56b51a7 100644 --- a/src/Ai/Base/Actions/SeeSpellAction.cpp +++ b/src/Ai/Base/Actions/SeeSpellAction.cpp @@ -15,7 +15,7 @@ std::set const FISHING_SPELLS = {7620, 7731, 7732, 18248, 33095, 51294}; -Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp, +Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* /*lastWp*/, bool important) { float dist = wpOwner->GetDistance(x, y, z); diff --git a/src/Ai/Base/Actions/UseMeetingStoneAction.cpp b/src/Ai/Base/Actions/UseMeetingStoneAction.cpp index 0862be68f32..d6032acb12d 100644 --- a/src/Ai/Base/Actions/UseMeetingStoneAction.cpp +++ b/src/Ai/Base/Actions/UseMeetingStoneAction.cpp @@ -61,7 +61,7 @@ bool SummonAction::Execute(Event /*event*/) if (!master) return false; - if (Pet* pet = bot->GetPet()) + if (bot->GetPet()) botAI->PetFollow(); if (master->GetSession()->GetSecurity() >= SEC_PLAYER) diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index adcadb23366..8d5449ef3bb 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -11,7 +11,7 @@ class ChatCommandActionNodeFactoryInternal : public NamedObjectFactory AvoidAoeStrategy::getDefaultActions() }; } -void AvoidAoeStrategy::InitTriggers(std::vector& triggers) +void AvoidAoeStrategy::InitTriggers(std::vector& /*triggers*/) { } -void AvoidAoeStrategy::InitMultipliers(std::vector& multipliers) +void AvoidAoeStrategy::InitMultipliers(std::vector& /*multipliers*/) { } @@ -81,7 +81,7 @@ std::vector TankFaceStrategy::getDefaultActions() }; } -void TankFaceStrategy::InitTriggers(std::vector& triggers) +void TankFaceStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/src/Ai/Base/Strategy/DuelStrategy.cpp b/src/Ai/Base/Strategy/DuelStrategy.cpp index c7f7e9d2355..2a36a3cc66e 100644 --- a/src/Ai/Base/Strategy/DuelStrategy.cpp +++ b/src/Ai/Base/Strategy/DuelStrategy.cpp @@ -17,6 +17,6 @@ void DuelStrategy::InitTriggers(std::vector& triggers) DuelStrategy::DuelStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) {} -void StartDuelStrategy::InitTriggers(std::vector& triggers) {} +void StartDuelStrategy::InitTriggers(std::vector& /*triggers*/) {} StartDuelStrategy::StartDuelStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} diff --git a/src/Ai/Base/Strategy/FollowMasterStrategy.cpp b/src/Ai/Base/Strategy/FollowMasterStrategy.cpp index 6701fdcac2e..5e836cf62e2 100644 --- a/src/Ai/Base/Strategy/FollowMasterStrategy.cpp +++ b/src/Ai/Base/Strategy/FollowMasterStrategy.cpp @@ -12,6 +12,6 @@ std::vector FollowMasterStrategy::getDefaultActions() }; } -void FollowMasterStrategy::InitTriggers(std::vector& triggers) +void FollowMasterStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/src/Ai/Base/Strategy/GuardStrategy.cpp b/src/Ai/Base/Strategy/GuardStrategy.cpp index 96017365f88..914cacf03c3 100644 --- a/src/Ai/Base/Strategy/GuardStrategy.cpp +++ b/src/Ai/Base/Strategy/GuardStrategy.cpp @@ -12,4 +12,4 @@ std::vector GuardStrategy::getDefaultActions() }; } -void GuardStrategy::InitTriggers(std::vector& triggers) {} +void GuardStrategy::InitTriggers(std::vector& /*triggers*/) {} diff --git a/src/Ai/Base/Strategy/NonCombatStrategy.cpp b/src/Ai/Base/Strategy/NonCombatStrategy.cpp index cb32233f467..05163208aa9 100644 --- a/src/Ai/Base/Strategy/NonCombatStrategy.cpp +++ b/src/Ai/Base/Strategy/NonCombatStrategy.cpp @@ -17,7 +17,7 @@ void CollisionStrategy::InitTriggers(std::vector& triggers) new TriggerNode("collision", { NextAction("move out of collision", 2.0f) })); } -void MountStrategy::InitTriggers(std::vector& triggers) +void MountStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/src/Ai/Base/Strategy/RTSCStrategy.cpp b/src/Ai/Base/Strategy/RTSCStrategy.cpp index 525338c156d..75d07706b9d 100644 --- a/src/Ai/Base/Strategy/RTSCStrategy.cpp +++ b/src/Ai/Base/Strategy/RTSCStrategy.cpp @@ -7,4 +7,4 @@ RTSCStrategy::RTSCStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} -void RTSCStrategy::InitTriggers(std::vector& triggers) {} +void RTSCStrategy::InitTriggers(std::vector& /*triggers*/) {} diff --git a/src/Ai/Base/Strategy/RacialsStrategy.cpp b/src/Ai/Base/Strategy/RacialsStrategy.cpp index ae45cdaaff6..b5a84bbce8c 100644 --- a/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -11,7 +11,7 @@ class RacialsStrategyActionNodeFactory : public NamedObjectFactory RacialsStrategyActionNodeFactory() { creators["lifeblood"] = &lifeblood; } private: - static ActionNode* lifeblood(PlayerbotAI* botAI) + static ActionNode* lifeblood(PlayerbotAI* /*botAI*/) { return new ActionNode("lifeblood", /*P*/ {}, diff --git a/src/Ai/Base/Strategy/UsePotionsStrategy.cpp b/src/Ai/Base/Strategy/UsePotionsStrategy.cpp index 55827fbd201..27f690cf1e5 100644 --- a/src/Ai/Base/Strategy/UsePotionsStrategy.cpp +++ b/src/Ai/Base/Strategy/UsePotionsStrategy.cpp @@ -11,7 +11,7 @@ class UsePotionsStrategyActionNodeFactory : public NamedObjectFactoryGetBot(); if (!botAI->CanCastSpell(spell, creature)) diff --git a/src/Ai/Base/Value/CurrentCcTargetValue.cpp b/src/Ai/Base/Value/CurrentCcTargetValue.cpp index b095c09d728..39fb7edd2bf 100644 --- a/src/Ai/Base/Value/CurrentCcTargetValue.cpp +++ b/src/Ai/Base/Value/CurrentCcTargetValue.cpp @@ -13,7 +13,7 @@ class FindCurrentCcTargetStrategy : public FindTargetStrategy { } - void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (botAI->HasAura(spell, attacker)) result = attacker; diff --git a/src/Ai/Base/Value/DpsTargetValue.cpp b/src/Ai/Base/Value/DpsTargetValue.cpp index 010e4719105..7099a13fed9 100644 --- a/src/Ai/Base/Value/DpsTargetValue.cpp +++ b/src/Ai/Base/Value/DpsTargetValue.cpp @@ -50,7 +50,7 @@ class CasterFindTargetSmartStrategy : public FindTargetStrategy result = nullptr; } - void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (Group* group = botAI->GetBot()->GetGroup()) { diff --git a/src/Ai/Base/Value/ItemCountValue.cpp b/src/Ai/Base/Value/ItemCountValue.cpp index 9ea7da2e066..46ebc1dfa34 100644 --- a/src/Ai/Base/Value/ItemCountValue.cpp +++ b/src/Ai/Base/Value/ItemCountValue.cpp @@ -11,8 +11,6 @@ std::vector InventoryItemValueBase::Find(std::string const qualifier) { std::vector result; - Player* bot = InventoryAction::botAI->GetBot(); - std::vector items = InventoryAction::parseItems(qualifier); for (Item* item : items) result.push_back(item); diff --git a/src/Ai/Base/Value/ItemCountValue.h b/src/Ai/Base/Value/ItemCountValue.h index 6f7c593b239..7e47565fc58 100644 --- a/src/Ai/Base/Value/ItemCountValue.h +++ b/src/Ai/Base/Value/ItemCountValue.h @@ -17,7 +17,7 @@ class InventoryItemValueBase : public InventoryAction public: InventoryItemValueBase(PlayerbotAI* botAI) : InventoryAction(botAI, "empty") {} - bool Execute(Event event) override { return false; } + bool Execute(Event /*event*/) override { return false; } protected: std::vector Find(std::string const qualifier); diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index b651af95620..88937212705 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -864,8 +864,6 @@ bool ItemUsageValue::SpellGivesSkillUp(uint32 spellId, Player* bot) { uint32 SkillValue = bot->GetPureSkillValue(skill->SkillLine); - uint32 craft_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_CRAFTING); - if (SkillGainChance(SkillValue, skill->TrivialSkillLineRankHigh, (skill->TrivialSkillLineRankHigh + skill->TrivialSkillLineRankLow) / 2, skill->TrivialSkillLineRankLow) > 0) diff --git a/src/Ai/Base/Value/LeastHpTargetValue.cpp b/src/Ai/Base/Value/LeastHpTargetValue.cpp index c185628fad4..d8579f0dded 100644 --- a/src/Ai/Base/Value/LeastHpTargetValue.cpp +++ b/src/Ai/Base/Value/LeastHpTargetValue.cpp @@ -13,7 +13,7 @@ class FindLeastHpTargetStrategy : public FindNonCcTargetStrategy public: FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (IsCcTarget(attacker)) return; diff --git a/src/Ai/Base/Value/LootStrategyValue.cpp b/src/Ai/Base/Value/LootStrategyValue.cpp index 6a4f9b9b48e..3b0b81a19d0 100644 --- a/src/Ai/Base/Value/LootStrategyValue.cpp +++ b/src/Ai/Base/Value/LootStrategyValue.cpp @@ -60,7 +60,7 @@ class DisenchantLootStrategy : public NormalLootStrategy class AllLootStrategy : public LootStrategy { public: - bool CanLoot(ItemTemplate const* proto, AiObjectContext* context) override { return true; } + bool CanLoot(ItemTemplate const* /*proto*/, AiObjectContext* /*context*/) override { return true; } std::string const GetName() override { return "all"; } }; diff --git a/src/Ai/Base/Value/NearestCorpsesValue.cpp b/src/Ai/Base/Value/NearestCorpsesValue.cpp index 6091c32419c..99b42662cfa 100644 --- a/src/Ai/Base/Value/NearestCorpsesValue.cpp +++ b/src/Ai/Base/Value/NearestCorpsesValue.cpp @@ -28,4 +28,4 @@ void NearestCorpsesValue::FindUnits(std::list& targets) Cell::VisitObjects(bot, searcher, range); } -bool NearestCorpsesValue::AcceptUnit(Unit* unit) { return true; } +bool NearestCorpsesValue::AcceptUnit(Unit* /*unit*/) { return true; } diff --git a/src/Ai/Base/Value/PartyMemberValue.cpp b/src/Ai/Base/Value/PartyMemberValue.cpp index 53a93b2b43d..b4f44fdf45b 100644 --- a/src/Ai/Base/Value/PartyMemberValue.cpp +++ b/src/Ai/Base/Value/PartyMemberValue.cpp @@ -27,7 +27,7 @@ Unit* PartyMemberValue::FindPartyMember(std::vector* party, FindPlayerP return nullptr; } -Unit* PartyMemberValue::FindPartyMember(FindPlayerPredicate& predicate, bool ignoreOutOfGroup) +Unit* PartyMemberValue::FindPartyMember(FindPlayerPredicate& predicate, bool /*ignoreOutOfGroup*/) { Player* master = GetMaster(); // GuidVector nearestPlayers; diff --git a/src/Ai/Base/Value/TankTargetValue.cpp b/src/Ai/Base/Value/TankTargetValue.cpp index 80def1cf97a..74a4046a37c 100644 --- a/src/Ai/Base/Value/TankTargetValue.cpp +++ b/src/Ai/Base/Value/TankTargetValue.cpp @@ -49,7 +49,7 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy public: FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} - void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (Group* group = botAI->GetBot()->GetGroup()) { diff --git a/src/Ai/Base/Value/TargetValue.cpp b/src/Ai/Base/Value/TargetValue.cpp index 19578daf4fb..9b8fde5bdd3 100644 --- a/src/Ai/Base/Value/TargetValue.cpp +++ b/src/Ai/Base/Value/TargetValue.cpp @@ -161,7 +161,7 @@ Unit* FindTargetValue::Calculate() return nullptr; } -void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* threatManager) +void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* /*threatManager*/) { UnitAI* unitAI = attacker->GetAI(); BossAI* bossAI = dynamic_cast(unitAI); diff --git a/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp b/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp index 0d3a43b7953..28179d74ece 100644 --- a/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp +++ b/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp @@ -47,7 +47,7 @@ void GenericDKNonCombatStrategy::InitTriggers(std::vector& trigger new TriggerNode("bone shield", { NextAction("bone shield", 21.0f) })); } -void DKBuffDpsStrategy::InitTriggers(std::vector& triggers) +void DKBuffDpsStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp index b1a4685b13b..f7a76b0fc8c 100644 --- a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp @@ -311,4 +311,4 @@ void CatDpsDruidStrategy::InitTriggers(std::vector& triggers) ); } -void CatAoeDruidStrategy::InitTriggers(std::vector& triggers) {} +void CatAoeDruidStrategy::InitTriggers(std::vector& /*triggers*/) {} diff --git a/src/Ai/Class/Hunter/Action/HunterActions.h b/src/Ai/Class/Hunter/Action/HunterActions.h index a67f17780b5..4c7c0851bf0 100644 --- a/src/Ai/Class/Hunter/Action/HunterActions.h +++ b/src/Ai/Class/Hunter/Action/HunterActions.h @@ -392,7 +392,7 @@ class CastExplosiveShotRank4Action : public CastExplosiveShotBaseAction public: CastExplosiveShotRank4Action(PlayerbotAI* botAI) : CastExplosiveShotBaseAction(botAI) {} - bool Execute(Event event) override + bool Execute(Event /*event*/) override { return botAI->CastSpell(60053, GetTarget()); } @@ -412,7 +412,7 @@ class CastExplosiveShotRank3Action : public CastExplosiveShotBaseAction public: CastExplosiveShotRank3Action(PlayerbotAI* botAI) : CastExplosiveShotBaseAction(botAI) {} - bool Execute(Event event) override + bool Execute(Event /*event*/) override { return botAI->CastSpell(60052, GetTarget()); } @@ -432,7 +432,7 @@ class CastExplosiveShotRank2Action : public CastExplosiveShotBaseAction public: CastExplosiveShotRank2Action(PlayerbotAI* botAI) : CastExplosiveShotBaseAction(botAI) {} - bool Execute(Event event) override + bool Execute(Event /*event*/) override { return botAI->CastSpell(60051, GetTarget()); } @@ -452,7 +452,7 @@ class CastExplosiveShotRank1Action : public CastExplosiveShotBaseAction public: CastExplosiveShotRank1Action(PlayerbotAI* botAI) : CastExplosiveShotBaseAction(botAI) {} - bool Execute(Event event) override + bool Execute(Event /*event*/) override { return botAI->CastSpell(53301, GetTarget()); } diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index c4edc28fd73..315b4a96f42 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -54,7 +54,7 @@ void PaladinCureStrategy::InitTriggers(std::vector& triggers) { NextAction("cleanse magic on party", ACTION_DISPEL + 1) })); } -void PaladinBoostStrategy::InitTriggers(std::vector& triggers) +void PaladinBoostStrategy::InitTriggers(std::vector& /*triggers*/) { // triggers.push_back(new TriggerNode("divine favor", { NextAction("divine favor", diff --git a/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp b/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp index 9c85f786125..d5e06b54ca1 100644 --- a/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp +++ b/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp @@ -124,12 +124,12 @@ void AoEWarlockStrategy::InitTriggers(std::vector& triggers) ); } -void WarlockBoostStrategy::InitTriggers(std::vector& triggers) +void WarlockBoostStrategy::InitTriggers(std::vector& /*triggers*/) { // Placeholder for future boost triggers } -void WarlockPetStrategy::InitTriggers(std::vector& triggers) +void WarlockPetStrategy::InitTriggers(std::vector& /*triggers*/) { // Placeholder for future pet triggers } diff --git a/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp b/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp index 5c922af90dc..ce10e6b9cf6 100644 --- a/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp +++ b/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp @@ -41,6 +41,6 @@ std::vector TankWarlockStrategy::getDefaultActions() }; } -void TankWarlockStrategy::InitTriggers(std::vector& triggers) +void TankWarlockStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp b/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp index 6d6e0b65540..bbae89d20ef 100644 --- a/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp @@ -21,7 +21,7 @@ class ArmsWarriorStrategyActionNodeFactory : public NamedObjectFactory orbs; Unit* closestOrb = nullptr; diff --git a/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp b/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp index be36e480f27..00de758c1a0 100644 --- a/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp +++ b/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp @@ -27,7 +27,7 @@ float IckAndKrickMultiplier::GetValue(Action* action) return 1.0f; } -float GarfrostMultiplier::GetValue(Action* action) +float GarfrostMultiplier::GetValue(Action* /*action*/) { Unit* boss = AI_VALUE2(Unit*, "find target", "garfrost"); if (!boss) diff --git a/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp b/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp index 323970d52d0..0912a912c87 100644 --- a/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp +++ b/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp @@ -15,6 +15,6 @@ void WotlkDungeonToCStrategy::InitTriggers(std::vector &triggers) } -void WotlkDungeonToCStrategy::InitMultipliers(std::vector &multipliers) +void WotlkDungeonToCStrategy::InitMultipliers(std::vector &/*multipliers*/) { } diff --git a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp index 6604fc5250b..a38f8a29181 100644 --- a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp +++ b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp @@ -57,7 +57,7 @@ float HighKingMaulgarDisableArcaneShotOnKroshMultiplier::GetValue(Action* action float HighKingMaulgarDisableMageTankAOEMultiplier::GetValue(Action* action) { - if (IsKroshMageTank(botAI, bot) && + if (IsKroshMageTank(bot) && (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action))) diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp index 4bc5efe99cb..3caadb38411 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp @@ -31,14 +31,14 @@ bool HighKingMaulgarIsMageTankTrigger::IsActive() { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - return IsKroshMageTank(botAI, bot) && krosh; + return IsKroshMageTank(bot) && krosh; } bool HighKingMaulgarIsMoonkinTankTrigger::IsActive() { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - return IsKigglerMoonkinTank(botAI, bot) && kiggler; + return IsKigglerMoonkinTank(bot) && kiggler; } bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() @@ -53,8 +53,8 @@ bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() !(botAI->IsMainTank(bot) && maulgar) && !(botAI->IsAssistTankOfIndex(bot, 0, false) && olm) && !(botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye) && - !(IsKroshMageTank(botAI, bot) && krosh) && - !(IsKigglerMoonkinTank(botAI, bot) && kiggler); + !(IsKroshMageTank(bot) && krosh) && + !(IsKigglerMoonkinTank(bot) && kiggler); } bool HighKingMaulgarHealerInDangerTrigger::IsActive() diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp index b29549b82c9..7195f0ebd01 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp @@ -39,7 +39,7 @@ namespace GruulsLairHelpers return false; } - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) + bool IsKroshMageTank(Player* bot) { Group* group = bot->GetGroup(); if (!group) @@ -79,7 +79,7 @@ namespace GruulsLairHelpers return highestHpMage == bot; } - bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot) + bool IsKigglerMoonkinTank(Player* bot) { Group* group = bot->GetGroup(); if (!group) diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h index 4615a9b7acd..f9315565b66 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h @@ -29,8 +29,8 @@ namespace GruulsLairHelpers constexpr uint32 GRUULS_LAIR_MAP_ID = 565; bool IsAnyOgreBossAlive(PlayerbotAI* botAI); - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot); - bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot); + bool IsKroshMageTank(Player* bot); + bool IsKigglerMoonkinTank(Player* bot); bool IsPositionSafe(PlayerbotAI* botAI, Player* bot, Position pos); bool TryGetNewSafePosition(PlayerbotAI* botAI, Player* bot, Position& outPos); diff --git a/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp b/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp index 336bd45e281..0ae27ffabbb 100644 --- a/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp +++ b/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp @@ -1998,10 +1998,10 @@ bool IccRotfaceGroupPositionAction::PositionRangedAndHealers(Unit* boss,Unit *sm if (!isHeroic) return false; - return FindAndMoveFromClosestMember(boss, smallOoze); + return FindAndMoveFromClosestMember(smallOoze); } -bool IccRotfaceGroupPositionAction::FindAndMoveFromClosestMember(Unit* boss, Unit* smallOoze) +bool IccRotfaceGroupPositionAction::FindAndMoveFromClosestMember(Unit* smallOoze) { const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); @@ -6925,7 +6925,7 @@ bool IccLichKingWinterAction::Execute(Event /*event*/) isVictim = true; // First priority: Get out of Defile if we're in one - if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), 3.0f)) { // Find nearest safe position (use tank position as fallback) const Position* safePos = botAI->IsTank(bot) ? GetMainTankPosition() : GetMainTankRangedPosition(); @@ -7124,7 +7124,7 @@ const Position* IccLichKingWinterAction::GetMainTankRangedPosition() return &ICC_LK_FROSTR3_POSITION; } -bool IccLichKingWinterAction::IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance) +bool IccLichKingWinterAction::IsPositionSafeFromDefile(float x, float y, float minSafeDistance) { Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); if (!boss) @@ -7190,7 +7190,7 @@ bool IccLichKingWinterAction::TryMoveToPosition(float targetX, float targetY, fl dy /= distance; // First check if direct path is safe - if (bot->IsWithinLOS(targetX, targetY, targetZ) && IsPositionSafeFromDefile(targetX, targetY, targetZ, 3.0f)) + if (bot->IsWithinLOS(targetX, targetY, targetZ) && IsPositionSafeFromDefile(targetX, targetY, 3.0f)) { if (isForced) botAI->Reset(); @@ -7221,7 +7221,7 @@ bool IccLichKingWinterAction::TryMoveToPosition(float targetX, float targetY, fl float testY = currentY + dy * attemptDistance + offsetY * direction; float testZ = targetZ; - if (bot->IsWithinLOS(testX, testY, testZ) && IsPositionSafeFromDefile(testX, testY, testZ, 3.0f)) + if (bot->IsWithinLOS(testX, testY, testZ) && IsPositionSafeFromDefile(testX, testY, 3.0f)) { if (isForced) botAI->Reset(); @@ -7271,7 +7271,7 @@ void IccLichKingWinterAction::HandleTankPositioning() const Position* targetPos = GetMainTankPosition(); // First check if current position is safe - if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), 3.0f)) { // If in defile, prioritize getting out TryMoveToPosition(targetPos->GetPositionX(), targetPos->GetPositionY(), 840.857f, true); @@ -7295,7 +7295,7 @@ void IccLichKingWinterAction::HandleTankPositioning() } // Once in position, handle add management from tank position - HandleMainTankAddManagement(boss, targetPos); + HandleMainTankAddManagement(targetPos); } // ASSIST TANK: More flexible positioning based on add collection else if (botAI->IsAssistTank(bot)) @@ -7312,7 +7312,7 @@ void IccLichKingWinterAction::HandleTankPositioning() } // Handle assist tank add collection and positioning - HandleAssistTankAddManagement(boss, targetPos); + HandleAssistTankAddManagement(targetPos); } } @@ -7405,7 +7405,7 @@ void IccLichKingWinterAction::HandleRangedPositioning() const Position* targetPos = GetMainTankRangedPosition(); // First check if current position is safe - if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), 3.0f)) { // If in defile, prioritize getting out TryMoveToPosition(targetPos->GetPositionX(), targetPos->GetPositionY(), 840.857f, true); @@ -7484,7 +7484,7 @@ void IccLichKingWinterAction::HandleRangedPositioning() } } -void IccLichKingWinterAction::HandleMainTankAddManagement(Unit* boss, const Position* tankPos) +void IccLichKingWinterAction::HandleMainTankAddManagement(const Position* tankPos) { if (!botAI->IsMainTank(bot)) return; @@ -7607,7 +7607,7 @@ void IccLichKingWinterAction::HandleMainTankAddManagement(Unit* boss, const Posi } } -void IccLichKingWinterAction::HandleAssistTankAddManagement(Unit* boss, const Position* tankPos) +void IccLichKingWinterAction::HandleAssistTankAddManagement(const Position* tankPos) { if (!botAI->IsAssistTank(bot)) return; @@ -7812,7 +7812,7 @@ bool IccLichKingAddsAction::Execute(Event /*event*/) return true; // Handle shambling horror interactions - HandleShamblingHorrors(boss, hasPlague); + HandleShamblingHorrors(); // Handle assist tank add management if (HandleAssistTankAddManagement(boss, diff)) @@ -8251,7 +8251,7 @@ bool IccLichKingAddsAction::HandleQuakeMechanics(Unit* boss) return false; } -void IccLichKingAddsAction::HandleShamblingHorrors(Unit* boss, bool hasPlague) +void IccLichKingAddsAction::HandleShamblingHorrors() { // Find closest shambling horror GuidVector npcs2 = AI_VALUE(GuidVector, "nearest hostile npcs"); diff --git a/src/Ai/Raid/Icecrown/Action/RaidIccActions.h b/src/Ai/Raid/Icecrown/Action/RaidIccActions.h index 9ac2a48a042..1d63a4e3696 100644 --- a/src/Ai/Raid/Icecrown/Action/RaidIccActions.h +++ b/src/Ai/Raid/Icecrown/Action/RaidIccActions.h @@ -286,7 +286,7 @@ class IccRotfaceGroupPositionAction : public AttackAction bool HandleOozeTargeting(); bool HandleOozeMemberPositioning(); bool PositionRangedAndHealers(Unit* boss,Unit* smallOoze); - bool FindAndMoveFromClosestMember(Unit* boss, Unit* smallOoze); + bool FindAndMoveFromClosestMember(Unit* smallOoze); }; class IccRotfaceMoveAwayFromExplosionAction : public MovementAction @@ -623,12 +623,12 @@ class IccLichKingWinterAction : public AttackAction void HandlePositionCorrection(); bool IsValidCollectibleAdd(Unit* unit); - bool IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance); + bool IsPositionSafeFromDefile(float x, float y, float minSafeDistance); void HandleTankPositioning(); void HandleMeleePositioning(); void HandleRangedPositioning(); - void HandleMainTankAddManagement(Unit* boss, const Position* tankPos); - void HandleAssistTankAddManagement(Unit* boss, const Position* tankPos); + void HandleMainTankAddManagement(const Position* tankPos); + void HandleAssistTankAddManagement(const Position* tankPos); private: const Position* GetMainTankPosition(); @@ -648,7 +648,7 @@ class IccLichKingAddsAction : public AttackAction void HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenasMenethilHC); void HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenasMenethilHC); bool HandleQuakeMechanics(Unit* boss); - void HandleShamblingHorrors(Unit* boss, bool hasPlague); + void HandleShamblingHorrors(); bool HandleAssistTankAddManagement(Unit* boss, Difficulty diff); void HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff); void HandleMainTankTargeting(Unit* boss, Difficulty diff); diff --git a/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp b/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp index 40ee354e21f..bc304ec0c61 100644 --- a/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp +++ b/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp @@ -559,7 +559,7 @@ bool IccBqlVampiricBiteTrigger::IsActive() bool IccValkyreSpearTrigger::IsActive() { // Check if there's a spear nearby - if (Creature* spear = bot->FindNearestCreature(NPC_SPEAR, 100.0f)) + if (bot->FindNearestCreature(NPC_SPEAR, 100.0f)) return true; return false; diff --git a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp index 4e175f0f698..69ef73987d4 100644 --- a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp +++ b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp @@ -744,7 +744,7 @@ bool NetherspiteBlockBlueBeamAction::Execute(Event /*event*/) float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) + if (!IsSafePosition(candidateX, candidateY, voidZones, 4.0f)) continue; float distToIdeal = fabs(dist - idealDistance); @@ -836,7 +836,7 @@ bool NetherspiteBlockGreenBeamAction::Execute(Event /*event*/) float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) + if (!IsSafePosition(candidateX, candidateY, voidZones, 4.0f)) continue; float distToIdeal = fabs(dist - 18.0f); @@ -873,7 +873,7 @@ bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event /*event*/) std::vector voidZones = GetAllVoidZones(botAI, bot); bool nearVoidZone = !IsSafePosition(bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), voidZones, 4.0f); + voidZones, 4.0f); std::vector beams; Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); @@ -922,7 +922,7 @@ bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event /*event*/) float cy = botY + std::sin(angle) * dist; float cz = netherspiteZ; - if (!IsSafePosition(cx, cy, cz, voidZones, 4.0f) || + if (!IsSafePosition(cx, cy, voidZones, 4.0f) || !IsAwayFromBeams(cx, cy, beams, netherspite)) continue; diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp index 513920e1117..82ecbdb7b37 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp @@ -272,7 +272,7 @@ namespace KarazhanHelpers return voidZones; } - bool IsSafePosition(float x, float y, float z, const std::vector& hazards, float hazardRadius) + bool IsSafePosition(float x, float y, const std::vector& hazards, float hazardRadius) { for (Unit* hazard : hazards) { @@ -351,7 +351,7 @@ namespace KarazhanHelpers destX, destY, destZ, true)) continue; - if (!IsSafePosition(destX, destY, destZ, hazards, safeDistance)) + if (!IsSafePosition(destX, destY, hazards, safeDistance)) continue; if (requireSafePath) diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h index 885af774d2e..055a93a581d 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h @@ -116,7 +116,7 @@ namespace KarazhanHelpers 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); + bool IsSafePosition (float x, float y, const std::vector& hazards, float hazardRadius); std::vector GetSpawnedInfernals(PlayerbotAI* botAI); bool IsStraightPathSafe( const Position& start, const Position& target, diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp index ffe446965d6..024ffe4d617 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp @@ -499,7 +499,7 @@ bool MagtheridonUseManticronCubeAction::Execute(Event /*event*/) return false; // Release cubes after Blast Nova is interrupted - if (HandleCubeRelease(magtheridon, cube)) + if (HandleCubeRelease(magtheridon)) return true; // Check if cube logic should be active (49+ second rule) @@ -520,7 +520,7 @@ bool MagtheridonUseManticronCubeAction::Execute(Event /*event*/) return false; } -bool MagtheridonUseManticronCubeAction::HandleCubeRelease(Unit* magtheridon, GameObject* cube) +bool MagtheridonUseManticronCubeAction::HandleCubeRelease(Unit* magtheridon) { if (bot->HasAura(SPELL_SHADOW_GRASP) && !(magtheridon->HasUnitState(UNIT_STATE_CASTING) && diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h index d47d06459ab..7abc493c2cf 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h @@ -81,7 +81,7 @@ class MagtheridonUseManticronCubeAction : public MovementAction bool Execute(Event event) override; private: - bool HandleCubeRelease(Unit* magtheridon, GameObject* cube); + bool HandleCubeRelease(Unit* magtheridon); bool ShouldActivateCubeLogic(Unit* magtheridon); bool HandleWaitingPhase(const MagtheridonHelpers::CubeInfo& cubeInfo); bool HandleCubeInteraction(const MagtheridonHelpers::CubeInfo& cubeInfo, GameObject* cube); diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp index 344dda5ba72..e51ad24e5cf 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp @@ -118,7 +118,7 @@ namespace MagtheridonHelpers std::unordered_map spreadWaitTimer; std::unordered_map dpsWaitTimer; - bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z) + bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* /*bot*/, float x, float y, float /*z*/) { // Debris std::vector debrisHazards; diff --git a/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp b/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp index b1217f59eb6..bb61bc00443 100644 --- a/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp +++ b/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp @@ -24,7 +24,7 @@ void RaidOnyxiaStrategy::InitTriggers(std::vector& triggers) "ony whelps spawn", { NextAction("ony kill whelps", ACTION_RAID + 1) })); } -void RaidOnyxiaStrategy::InitMultipliers(std::vector& multipliers) +void RaidOnyxiaStrategy::InitMultipliers(std::vector& /*multipliers*/) { // Empty for now } diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp index 0b31a1c13d8..68dbc8b621f 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp @@ -1466,16 +1466,16 @@ bool MorogrimTidewalkerMoveBossToTankPositionAction::Execute(Event /*event*/) if (tidewalker->GetVictim() == bot && bot->IsWithinMeleeRange(tidewalker)) { if (tidewalker->GetHealthPct() > 26.0f) - return MoveToPhase1TankPosition(tidewalker); + return MoveToPhase1TankPosition(); else - return MoveToPhase2TankPosition(tidewalker); + return MoveToPhase2TankPosition(); } return false; } // Phase 1: tank position is up against the Northeast pillar -bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition(Unit* tidewalker) +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition() { const Position& phase1 = TIDEWALKER_PHASE_1_TANK_POSITION; float distToPhase1 = bot->GetExactDist2d(phase1.GetPositionX(), phase1.GetPositionY()); @@ -1495,7 +1495,7 @@ bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition(Un } // Phase 2: move in two steps to get around the pillar and back up into the Northeast corner -bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase2TankPosition(Unit* tidewalker) +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase2TankPosition() { const Position& phase2 = TIDEWALKER_PHASE_2_TANK_POSITION; const Position& transition = TIDEWALKER_PHASE_TRANSITION_WAYPOINT; @@ -2151,7 +2151,7 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) { // Passer order: HealAssistantOfIndex 0, 1, 2, then RangedDpsAssistantOfIndex 0 if (bot == firstCorePasser && - LineUpFirstCorePasser(designatedLooter, closestTrigger)) + LineUpFirstCorePasser(designatedLooter)) { return true; } @@ -2176,7 +2176,7 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) // Designated core looter logic // Applicable only if cheat mode is on and thus looter is a bot if (bot == designatedLooter && - IsFirstCorePasserInPosition(designatedLooter, firstCorePasser, closestTrigger)) + IsFirstCorePasserInPosition(firstCorePasser)) { const time_t now = std::time(nullptr); auto it = lastImbueAttempt.find(instanceId); @@ -2192,7 +2192,7 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) // First core passer: receive core from looter at the top of the stairs, // pass to second core passer else if (bot == firstCorePasser && - IsSecondCorePasserInPosition(firstCorePasser, secondCorePasser, closestTrigger)) + IsSecondCorePasserInPosition(secondCorePasser)) { const time_t now = std::time(nullptr); auto it = lastImbueAttempt.find(instanceId); @@ -2209,7 +2209,7 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) // of the first passer, move to the generator; otherwise, move as close as // possible to the generator while staying in passing range else if (bot == secondCorePasser && !UseCoreOnNearestGenerator(instanceId) && - IsThirdCorePasserInPosition(secondCorePasser, thirdCorePasser, closestTrigger)) + IsThirdCorePasserInPosition(thirdCorePasser)) { const time_t now = std::time(nullptr); auto it = lastImbueAttempt.find(instanceId); @@ -2226,7 +2226,7 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) // of the second passer, move to the generator; otherwise, move as close as // possible to the generator while staying in passing range else if (bot == thirdCorePasser && !UseCoreOnNearestGenerator(instanceId) && - IsFourthCorePasserInPosition(thirdCorePasser, fourthCorePasser, closestTrigger)) + IsFourthCorePasserInPosition(fourthCorePasser)) { const time_t now = std::time(nullptr); auto it = lastImbueAttempt.find(instanceId); @@ -2249,7 +2249,7 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) } bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( - Player* designatedLooter, Unit* closestTrigger) + Player* designatedLooter) { const float centerX = VASHJ_PLATFORM_CENTER_POSITION.GetPositionX(); const float centerY = VASHJ_PLATFORM_CENTER_POSITION.GetPositionY(); @@ -2288,8 +2288,6 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpSecondCorePasser( if (itFirst == intendedLineup.end()) return false; - const Position& firstLineup = itFirst->second; - auto itSecond = intendedLineup.find(bot->GetGUID()); if (itSecond == intendedLineup.end()) { @@ -2343,9 +2341,9 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpThirdCorePasser( Player* secondCorePasser, Unit* closestTrigger) { bool needThirdPasser = - (IsFirstCorePasserInPosition(designatedLooter, firstCorePasser, closestTrigger) && + (IsFirstCorePasserInPosition(firstCorePasser) && firstCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || - (IsSecondCorePasserInPosition(firstCorePasser, secondCorePasser, closestTrigger) && + (IsSecondCorePasserInPosition(secondCorePasser) && secondCorePasser->GetExactDist2d(closestTrigger) > 4.0f); if (!needThirdPasser) @@ -2408,9 +2406,9 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpFourthCorePasser( Player* thirdCorePasser, Unit* closestTrigger) { bool needFourthPasser = - (IsSecondCorePasserInPosition(firstCorePasser, secondCorePasser, closestTrigger) && + (IsSecondCorePasserInPosition(secondCorePasser) && secondCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || - (IsThirdCorePasserInPosition(secondCorePasser, thirdCorePasser, closestTrigger) && + (IsThirdCorePasserInPosition(thirdCorePasser) && thirdCorePasser->GetExactDist2d(closestTrigger) > 4.0f); if (!needFourthPasser) @@ -2460,8 +2458,7 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpFourthCorePasser( // The next four functions check if the respective passer is <= 2 yards of their intended // position and are used to determine when the prior bot in the chain can pass the core -bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInPosition( - Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInPosition(Player* firstCorePasser) { auto itSnap = intendedLineup.find(firstCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2474,8 +2471,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInPosition( - Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInPosition(Player* secondCorePasser) { auto itSnap = intendedLineup.find(secondCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2488,8 +2484,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInPosition( - Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInPosition(Player* thirdCorePasser) { auto itSnap = intendedLineup.find(thirdCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2502,8 +2497,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInPosition( - Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInPosition(Player* fourthCorePasser) { auto itSnap = intendedLineup.find(fourthCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h index 08ad4c48a82..9443354c437 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h @@ -312,8 +312,8 @@ class MorogrimTidewalkerMoveBossToTankPositionAction : public AttackAction bool Execute(Event event) override; private: - bool MoveToPhase1TankPosition(Unit* tidewalker); - bool MoveToPhase2TankPosition(Unit* tidewalker); + bool MoveToPhase1TankPosition(); + bool MoveToPhase2TankPosition(); }; class MorogrimTidewalkerPhase2RepositionRangedAction : public MovementAction @@ -414,14 +414,14 @@ class LadyVashjPassTheTaintedCoreAction : public MovementAction bool Execute(Event event) override; private: - bool LineUpFirstCorePasser(Player* designatedLooter, Unit* closestTrigger); + bool LineUpFirstCorePasser(Player* designatedLooter); bool LineUpSecondCorePasser(Player* firstCorePasser, Unit* closestTrigger); bool LineUpThirdCorePasser(Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); bool LineUpFourthCorePasser(Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); - bool IsFirstCorePasserInPosition(Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger); - bool IsSecondCorePasserInPosition(Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); - bool IsThirdCorePasserInPosition(Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); - bool IsFourthCorePasserInPosition(Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger); + bool IsFirstCorePasserInPosition(Player* firstCorePasser); + bool IsSecondCorePasserInPosition(Player* secondCorePasser); + bool IsThirdCorePasserInPosition(Player* thirdCorePasser); + bool IsFourthCorePasserInPosition(Player* fourthCorePasser); void ScheduleTransferCoreAfterImbue(PlayerbotAI* botAI, Player* giver, Player* receiver); bool UseCoreOnNearestGenerator(const uint32 instanceId); }; diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp index c841c7dcb5e..f43ef466704 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp @@ -690,13 +690,11 @@ float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* acti auto coreHandlers = GetCoreHandlers(botAI, bot); bool isCoreHandler = false; - int myIndex = -1; for (int i = 0; i < static_cast(coreHandlers.size()); ++i) { if (coreHandlers[i] && coreHandlers[i] == bot) { isCoreHandler = true; - myIndex = i; } } if (!isCoreHandler) diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp index edcc7ad58f5..c31a5e7ad48 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -213,7 +213,7 @@ bool AlarAssistTanksPickUpEmbersAction::Execute(Event /*event*/) if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) return HandlePhase1Embers(alar); else - return HandlePhase2Embers(alar); + return HandlePhase2Embers(); } // Embers will be tanked by only the second assist tank in Phase 1 @@ -275,7 +275,7 @@ bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) // One Ember will be tanked by the second assist tank in Phase 2, and the other by // the main tank or first assist tank (whichever is not tanking Al'ar) -bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) +bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers() { auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h index 8b021304ee1..41b087e3c39 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h @@ -66,7 +66,7 @@ class AlarAssistTanksPickUpEmbersAction : public AttackAction private: bool HandlePhase1Embers(Unit* alar); - bool HandlePhase2Embers(Unit* alar); + bool HandlePhase2Embers(); }; class AlarRangedDpsPrioritizeEmbersAction : public AttackAction diff --git a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp index 798b77904d6..217ab2c06e2 100644 --- a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp +++ b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp @@ -2332,7 +2332,6 @@ bool MimironRocketStrikeAction::isUseful() bool MimironRocketStrikeAction::Execute(Event /*event*/) { - Unit* leviathanMkII = nullptr; Unit* vx001 = nullptr; Unit* aerialCommandUnit = nullptr; @@ -2344,9 +2343,7 @@ bool MimironRocketStrikeAction::Execute(Event /*event*/) if (!target || !target->IsAlive()) continue; - if (target->GetEntry() == NPC_LEVIATHAN_MKII) - leviathanMkII = target; - else if (target->GetEntry() == NPC_VX001) + if (target->GetEntry() == NPC_VX001) vx001 = target; else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) aerialCommandUnit = target; diff --git a/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp b/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp index e7d16920fc9..b727681a465 100644 --- a/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp +++ b/src/Ai/Raid/ZulAman/Multiplier/RaidZulAmanMultipliers.cpp @@ -157,7 +157,7 @@ float JanalaiStayAwayFromFireBombsMultiplier::GetValue(Action* action) if (!AI_VALUE2(Unit*, "find target", "jan'alai")) return 1.0f; - if (!HasFireBombNearby(botAI, bot)) + if (!HasFireBombNearby(bot)) return 1.0f; if (dynamic_cast(action) || diff --git a/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp b/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp index ad338a5a7ad..22b072702e5 100644 --- a/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp +++ b/src/Ai/Raid/ZulAman/Trigger/RaidZulAmanTriggers.cpp @@ -108,7 +108,7 @@ bool JanalaiBossEngagedByTanksTrigger::IsActive() !AI_VALUE2(Unit*, "find target", "jan'alai")) return false; - return !HasFireBombNearby(botAI, bot); + return !HasFireBombNearby(bot); } bool JanalaiBossCastsFlameBreathTrigger::IsActive() @@ -118,13 +118,13 @@ bool JanalaiBossCastsFlameBreathTrigger::IsActive() AI_VALUE2(Unit*, "find target", "amani dragonhawk hatchling")) return false; - return !HasFireBombNearby(botAI, bot); + return !HasFireBombNearby(bot); } bool JanalaiBossSummoningFireBombsTrigger::IsActive() { return AI_VALUE2(Unit*, "find target", "jan'alai") && - HasFireBombNearby(botAI, bot); + HasFireBombNearby(bot); } bool JanalaiAmanishiHatchersSpawnedTrigger::IsActive() diff --git a/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp index 77c268817b9..eeff879a4d6 100644 --- a/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp +++ b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.cpp @@ -143,7 +143,7 @@ namespace ZulAmanHelpers // Jan'alai const Position JANALAI_TANK_POSITION = { -33.873f, 1149.571f, 19.146f }; - bool HasFireBombNearby(PlayerbotAI* botAI, Player* bot) + bool HasFireBombNearby(Player* bot) { constexpr float searchRadius = 30.0f; std::list creatureList; diff --git a/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h index 4c27f0238e0..5da447a7e39 100644 --- a/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h +++ b/src/Ai/Raid/ZulAman/Util/RaidZulAmanHelpers.h @@ -97,7 +97,7 @@ namespace ZulAmanHelpers // Jan'alai extern const Position JANALAI_TANK_POSITION; - bool HasFireBombNearby(PlayerbotAI* botAI, Player* bot); + bool HasFireBombNearby(Player* bot); std::pair GetAmanishiHatcherPair(PlayerbotAI* botAI); // Halazzi diff --git a/src/Bot/Engine/Engine.cpp b/src/Bot/Engine/Engine.cpp index 3131f7514dc..90a90fdb97a 100644 --- a/src/Bot/Engine/Engine.cpp +++ b/src/Bot/Engine/Engine.cpp @@ -138,7 +138,7 @@ void Engine::Init() } } -bool Engine::DoNextAction(Unit* unit, uint32 depth, bool minimal) +bool Engine::DoNextAction(Unit* /*unit*/, uint32 /*depth*/, bool minimal) { LogAction("--- AI Tick ---"); diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 9dfae8987e5..e7021e3723a 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -2230,8 +2230,6 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (!CanEquipUnseenItem(slot, dest, bestItemForSlot)) continue; - Item* newItem = bot->EquipNewItem(dest, bestItemForSlot, true); - bot->EquipNewItem(dest, bestItemForSlot, true); bot->AutoUnequipOffhandIfNeed(); } @@ -2393,7 +2391,7 @@ void PlayerbotFactory::InitBags(bool destroyOld) if (old_bag) continue; - Item* newItem = bot->EquipNewItem(dest, newItemId, true); + bot->EquipNewItem(dest, newItemId, true); // if (newItem) // { // newItem->AddToWorld(); @@ -4729,7 +4727,7 @@ void PlayerbotFactory::ApplyEnchantTemplate(uint8 spec) // const SpellItemEnchantmentEntry* a = sSpellItemEnchantmentStore.LookupEntry(1); } -void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) +void PlayerbotFactory::ApplyEnchantAndGemsNew(bool /*destroyOld*/) { //int32 bestGemEnchantId[4] = {-1, -1, -1, -1}; // 1, 2, 4, 8 color //not used, line marked for removal. //float bestGemScore[4] = {0, 0, 0, 0}; //not used, line marked for removal. diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 25c93676f70..98175a1dc9a 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -491,7 +491,7 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal continue; } - ChatReplyAction::ChatReplyDo(bot, it->m_type, it->m_guid1, it->m_guid2, it->m_msg, it->m_chanName, it->m_name); + ChatReplyAction::ChatReplyDo(bot, it->m_type, it->m_guid1, it->m_msg, it->m_chanName, it->m_name); it = chatReplies.erase(it); } @@ -1811,7 +1811,7 @@ Strategy* PlayerbotAI::GetStrategy(std::string const name, BotState type) return engines[type] ? engines[type]->GetStrategy(name) : nullptr; } -void PlayerbotAI::ResetStrategies(bool load) +void PlayerbotAI::ResetStrategies(bool /*load*/) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) engines[i]->removeAllStrategies(); diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index 8327e2f3674..68eb2fd1ed7 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -1667,7 +1667,7 @@ void PlayerbotMgr::TellError(std::string const botName, std::string const text) errors[text] = names; } -void PlayerbotMgr::CheckTellErrors(uint32 elapsed) +void PlayerbotMgr::CheckTellErrors(uint32 /*elapsed*/) { time_t now = time(nullptr); if ((now - lastErrorTell) < sPlayerbotAIConfig.errorDelay / 1000) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index ee06c58f899..9e915098994 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -278,7 +278,7 @@ void RandomPlayerbotMgr::LogPlayerLocation() } } -void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) +void RandomPlayerbotMgr::UpdateAIInternal(uint32 /*elapsed*/, bool /*minimal*/) { if (totalPmo) totalPmo->finish(); diff --git a/src/Db/PlayerbotRepository.cpp b/src/Db/PlayerbotRepository.cpp index 731534edd63..115a4e95f17 100644 --- a/src/Db/PlayerbotRepository.cpp +++ b/src/Db/PlayerbotRepository.cpp @@ -65,7 +65,7 @@ void PlayerbotRepository::Save(PlayerbotAI* botAI) SaveValue(guid, "dead", FormatStrategies("dead", botAI->GetStrategies(BOT_STATE_DEAD))); } -std::string const PlayerbotRepository::FormatStrategies(std::string const type, std::vector strategies) +std::string const PlayerbotRepository::FormatStrategies(std::string const /*type*/, std::vector strategies) { std::ostringstream out; for (std::vector::iterator i = strategies.begin(); i != strategies.end(); ++i) diff --git a/src/Mgr/Item/ItemVisitors.h b/src/Mgr/Item/ItemVisitors.h index 930aa1f4a3d..dd581ddba7a 100644 --- a/src/Mgr/Item/ItemVisitors.h +++ b/src/Mgr/Item/ItemVisitors.h @@ -325,9 +325,6 @@ class FindMountVisitor : public FindUsableItemVisitor FindMountVisitor(Player* bot) : FindUsableItemVisitor(bot) {} bool Accept(ItemTemplate const* proto) override; - -private: - uint32 effectId; }; class FindPetVisitor : public FindUsableItemVisitor diff --git a/src/Mgr/Item/RandomItemMgr.cpp b/src/Mgr/Item/RandomItemMgr.cpp index d2a27972745..e08f2c38567 100644 --- a/src/Mgr/Item/RandomItemMgr.cpp +++ b/src/Mgr/Item/RandomItemMgr.cpp @@ -176,7 +176,7 @@ RandomItemMgr::~RandomItemMgr() predicates.clear(); } -bool RandomItemMgr::HandleConsoleCommand(ChatHandler* handler, char const* args) +bool RandomItemMgr::HandleConsoleCommand(ChatHandler* /*handler*/, char const* args) { if (!args || !*args) { @@ -1823,7 +1823,7 @@ uint32 RandomItemMgr::GetUpgrade(Player* player, std::string spec, uint8 slot, u } std::vector RandomItemMgr::GetUpgradeList(Player* player, std::string spec, uint8 slot, uint32 quality, - uint32 itemId, uint32 amount) + uint32 itemId, uint32 /*amount*/) { std::vector listItems; if (!player) diff --git a/src/Mgr/Item/StatsWeightCalculator.cpp b/src/Mgr/Item/StatsWeightCalculator.cpp index 361e4a5f722..b3b5936063f 100644 --- a/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/src/Mgr/Item/StatsWeightCalculator.cpp @@ -517,7 +517,7 @@ void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate con weight_ *= multiplier; } -void StatsWeightCalculator::CalculateSocketBonus(Player* player, ItemTemplate const* proto) +void StatsWeightCalculator::CalculateSocketBonus(Player* /*player*/, ItemTemplate const* proto) { uint32 socketNum = 0; for (uint32 enchant_slot = SOCK_ENCHANTMENT_SLOT; enchant_slot < SOCK_ENCHANTMENT_SLOT + MAX_GEM_SOCKETS; diff --git a/src/Mgr/Talent/Talentspec.cpp b/src/Mgr/Talent/Talentspec.cpp index 7ee760ed46e..2cb6ce5c4ad 100644 --- a/src/Mgr/Talent/Talentspec.cpp +++ b/src/Mgr/Talent/Talentspec.cpp @@ -142,7 +142,7 @@ bool TalentSpec::CheckTalents(uint32 level, std::ostringstream* out) } // Set the talents for the bots to the current spec. -void TalentSpec::ApplyTalents(Player* bot, std::ostringstream* out) +void TalentSpec::ApplyTalents(Player* bot, std::ostringstream* /*out*/) { for (auto& entry : talents) { @@ -397,7 +397,7 @@ uint32 TalentSpec::highestTree() return 0; } -std::string const TalentSpec::FormatSpec(Player* bot) +std::string const TalentSpec::FormatSpec(Player* /*bot*/) { // uint8 cls = bot->getClass(); //not used, (used in lined 403), line marked for removal. diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp index 7db91eb40ae..5be7e8855dc 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -226,7 +226,7 @@ class PlayerbotsPlayerScript : public PlayerScript return true; } - bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* guild) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* /*guild*/) override { if (type != CHAT_MSG_GUILD) return true; @@ -445,7 +445,7 @@ class PlayerbotsScript : public PlayerbotScript playerbotMgr->HandleMasterOutgoingPacket(*packet); } - void OnPlayerbotUpdate(uint32 diff) override + void OnPlayerbotUpdate(uint32 /*diff*/) override { sRandomPlayerbotMgr.UpdateSessions(); // Per-bot updates only } diff --git a/src/Util/ServerFacade.cpp b/src/Util/ServerFacade.cpp index 1ba1eb5ea5b..bc17e5fe3f2 100644 --- a/src/Util/ServerFacade.cpp +++ b/src/Util/ServerFacade.cpp @@ -39,7 +39,7 @@ bool ServerFacade::IsDistanceGreaterOrEqualThan(float dist1, float dist2) { retu bool ServerFacade::IsDistanceLessOrEqualThan(float dist1, float dist2) { return !IsDistanceGreaterThan(dist1, dist2); } -void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool force) +void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool /*force*/) { if (!bot) return; From 04f8b0dd13d914d4bc4fbbaba4f388b4beced895 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 24 Apr 2026 23:03:57 +0200 Subject: [PATCH 79/87] Stat weights fix (#2313) ## Pull Request Description Added support for Warlock stat weights when he dont have Fel Armor. Fixed Mage weights when he dont have Molten Armor ## How to Test the Changes 1. Invite mage which dont have Molten Armor (level < 62) or warlock which dont have Fel Armor (level < 62) 2. Give him 2 items for same slot one with spirit one with intellect and unequip item on this slot and destroy 3. Bot should equip this with intellect (if other stats are same) ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) Mage and Warlock before getting Molten Armor/Fel Armor dont prioritize Spirit before Intellect - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Mgr/Item/StatsWeightCalculator.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Mgr/Item/StatsWeightCalculator.cpp b/src/Mgr/Item/StatsWeightCalculator.cpp index b3b5936063f..faa06ff0fe6 100644 --- a/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/src/Mgr/Item/StatsWeightCalculator.cpp @@ -25,6 +25,10 @@ namespace constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_1 = 30482; constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_2 = 43045; constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_3 = 43046; +constexpr uint32 SPELL_FEL_ARMOR_RANK_1 = 28176; +constexpr uint32 SPELL_FEL_ARMOR_RANK_2 = 28189; +constexpr uint32 SPELL_FEL_ARMOR_RANK_3 = 47892; +constexpr uint32 SPELL_FEL_ARMOR_RANK_4 = 47893; } StatsWeightCalculator::StatsWeightCalculator(Player* player) : player_(player) @@ -467,10 +471,18 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player) && !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_2) && !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_3)) { - stats_weights_[STATS_TYPE_INTELLECT] += 0.2f; - stats_weights_[STATS_TYPE_SPIRIT] -= 0.0f; + if (tab != MAGE_TAB_FIRE) + stats_weights_[STATS_TYPE_SPIRIT] -= 0.6f; + else + stats_weights_[STATS_TYPE_SPIRIT] -= 0.7f; } } + else if (cls == CLASS_WARLOCK) + { + if (!player->HasSpell(SPELL_FEL_ARMOR_RANK_1) && !player->HasSpell(SPELL_FEL_ARMOR_RANK_2) && + !player->HasSpell(SPELL_FEL_ARMOR_RANK_3) && !player->HasSpell(SPELL_FEL_ARMOR_RANK_4)) + stats_weights_[STATS_TYPE_SPIRIT] -= 0.4f; + } } void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto) From 1967b63bc11aa4ed2c3b6b3fc8ffbc45377345eb Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 24 Apr 2026 16:04:21 -0500 Subject: [PATCH 80/87] Cleanups for Shaman weapon enchant refactor (#2315) ## Pull Request Description I forgot to include some clean-ups relating to the recent commit to refactor Shaman weapon enchants. This is just deleting some now unneeded code and cleaning up a bit of other code. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Base/Value/ItemForSpellValue.cpp | 17 ----------------- src/Ai/Base/Value/SpellCastUsefulValue.cpp | 12 ++++++------ 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/Ai/Base/Value/ItemForSpellValue.cpp b/src/Ai/Base/Value/ItemForSpellValue.cpp index e14549da415..2cefe906293 100644 --- a/src/Ai/Base/Value/ItemForSpellValue.cpp +++ b/src/Ai/Base/Value/ItemForSpellValue.cpp @@ -48,23 +48,6 @@ Item* ItemForSpellValue::Calculate() } } - // Workaround as some spells have no item mask (e.g. shaman weapon enhancements) - if (!strcmpi(spellInfo->SpellName[0], "rockbiter weapon") || - !strcmpi(spellInfo->SpellName[0], "flametongue weapon") || - !strcmpi(spellInfo->SpellName[0], "earthliving weapon") || - !strcmpi(spellInfo->SpellName[0], "frostbrand weapon") || !strcmpi(spellInfo->SpellName[0], "windfury weapon")) - { - itemForSpell = GetItemFitsToSpellRequirements(EQUIPMENT_SLOT_MAINHAND, spellInfo); - if (itemForSpell && itemForSpell->GetTemplate()->Class == ITEM_CLASS_WEAPON) - return itemForSpell; - - itemForSpell = GetItemFitsToSpellRequirements(EQUIPMENT_SLOT_OFFHAND, spellInfo); - if (itemForSpell && itemForSpell->GetTemplate()->Class == ITEM_CLASS_WEAPON) - return itemForSpell; - - return nullptr; - } - if (!(spellInfo->Targets & TARGET_FLAG_ITEM)) return nullptr; diff --git a/src/Ai/Base/Value/SpellCastUsefulValue.cpp b/src/Ai/Base/Value/SpellCastUsefulValue.cpp index 9fa85b3a135..6841335d5ea 100644 --- a/src/Ai/Base/Value/SpellCastUsefulValue.cpp +++ b/src/Ai/Base/Value/SpellCastUsefulValue.cpp @@ -40,13 +40,13 @@ bool SpellCastUsefulValue::Calculate() return false; } - // TODO: workaround - if (qualifier == "windfury weapon" || qualifier == "flametongue weapon" || qualifier == "frostbrand weapon" || - qualifier == "rockbiter weapon" || qualifier == "earthliving weapon" || qualifier == "spellstone") + if (qualifier == "windfury weapon" || qualifier == "flametongue weapon" || + qualifier == "frostbrand weapon" || qualifier == "rockbiter weapon" || + qualifier == "earthliving weapon" || qualifier == "spellstone") { - if (Item* item = AI_VALUE2(Item*, "item for spell", spellid)) - if (item->IsInWorld() && item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) - return false; + if (Item* item = AI_VALUE2(Item*, "item for spell", spellid); + item && item->IsInWorld() && item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + return false; } std::set& skipSpells = AI_VALUE(std::set&, "skip spells list"); From b6408ca602c377da163a63cdfec1e0bd2d3d3ead Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:04:40 -0600 Subject: [PATCH 81/87] Fix: Prevent infantry auto attack when IsInVehicle (#2319) ## Pull Request Description While testing vehicle combat in Wintergrasp, I was near two opposing vehicles. They were right on top of each other, and I was hearing the sounds of infantry melee attack. It looks like their auto-attack was on. I had thought the check in [GenericActions](https://github.com/mod-playerbots/mod-playerbots/blob/0c205b8cef5a541cabe080aaf9653adb25695c40/src/Ai/Base/Actions/GenericActions.cpp#L46) would prevent that, but I guess we need the extra defence. Note that IsInVehicle first three parameters are canControl/canCast/canAttack ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes There isn't vehicle combat scenarios (particularly vehicle on vehicle) in Playerbots right now that allow for obvious testing. I only learned about this through testing my unpublished Wintergrasp implementation. Regardless, the change is simple and the effect on code should be clear. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) No more infantry combat of any kind for drivers. - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Actions/AttackAction.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Ai/Base/Actions/AttackAction.cpp b/src/Ai/Base/Actions/AttackAction.cpp index af964f3608e..a537dd218a5 100644 --- a/src/Ai/Base/Actions/AttackAction.cpp +++ b/src/Ai/Base/Actions/AttackAction.cpp @@ -114,6 +114,11 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) return false; } + // Infantry attacks are not allowed from vehicles drivers. + // Check is needed to stop some auto-attack situations. + if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true)) + return false; + Unit* oldTarget = context->GetValue("current target")->Get(); bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); From ed0a21eefaf9fd91014fc2ec9877b760b010a084 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 24 Apr 2026 23:04:59 +0200 Subject: [PATCH 82/87] GetGrave fix (#2320) ## Pull Request Description Added missing races in GetGrave method Related with: #2220 ## How to Test the Changes 1. Group with bot in starting zone for dranei or blood elf 2. Kill bot. 3. Use command `release` and `revive` 4. Watch which graveyard will be used ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers Stability test: obraz --- src/Ai/Base/Actions/ReviveFromCorpseAction.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp b/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp index 0051a5a49b3..2599fb1cff7 100644 --- a/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp +++ b/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp @@ -251,9 +251,9 @@ GraveyardStruct const* SpiritHealerAction::GetGrave(bool startZone) std::vector races; if (bot->GetTeamId() == TEAM_ALLIANCE) - races = {RACE_HUMAN, RACE_DWARF, RACE_GNOME, RACE_NIGHTELF}; + races = {RACE_HUMAN, RACE_DWARF, RACE_GNOME, RACE_NIGHTELF, RACE_DRAENEI}; else - races = {RACE_ORC, RACE_TROLL, RACE_TAUREN, RACE_UNDEAD_PLAYER}; + races = {RACE_ORC, RACE_TROLL, RACE_TAUREN, RACE_UNDEAD_PLAYER, RACE_BLOODELF}; float graveDistance = -1; From eb268c7507789301cbbf4a32059e397bb3017234 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:05:35 -0700 Subject: [PATCH 83/87] Init guilds on login. (#2325) ## Pull Request Description Guild solution to #2148 ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [ ] No, not at all - - [x] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) Minimal cost on bot login. - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Bot/Factory/PlayerbotFactory.h | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 7 +++++++ src/Mgr/Guild/PlayerbotGuildMgr.cpp | 11 ++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.h b/src/Bot/Factory/PlayerbotFactory.h index 1962c0428c5..0e18e6b83ba 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -88,6 +88,7 @@ class PlayerbotFactory void InitKeyring(); void InitReputation(); void InitAttunementQuests(); + void InitGuild(); private: enum class ProfessionSpecializationSpell : uint32 @@ -199,7 +200,6 @@ class PlayerbotFactory void InitInventoryEquip(); void InitInventorySkill(); Item* StoreItem(uint32 itemId, uint32 count); - void InitGuild(); void InitArenaTeam(); void InitImmersive(); static void AddPrevQuests(uint32 questId, std::list& questIds); diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 9e915098994..110a1942e9c 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -2532,6 +2532,13 @@ void RandomPlayerbotMgr::OnBotLoginInternal(Player* const bot) } } + // Run guild recovery/assignment at login to handle empty guild tables after restart. + if (sPlayerbotAIConfig.randomBotGuildCount > 0) + { + PlayerbotFactory factory(bot, bot->GetLevel()); + factory.InitGuild(); + } + if (sPlayerbotAIConfig.randomBotFixedLevel) { bot->SetPlayerFlag(PLAYER_FLAGS_NO_XP_GAIN); diff --git a/src/Mgr/Guild/PlayerbotGuildMgr.cpp b/src/Mgr/Guild/PlayerbotGuildMgr.cpp index 001a438cbbc..5dc1095dee1 100644 --- a/src/Mgr/Guild/PlayerbotGuildMgr.cpp +++ b/src/Mgr/Guild/PlayerbotGuildMgr.cpp @@ -150,13 +150,10 @@ void PlayerbotGuildMgr::OnGuildUpdate(Guild* guild) void PlayerbotGuildMgr::ResetGuildCache() { - for (auto it = _guildCache.begin(); it != _guildCache.end();) - { - GuildCache& cached = it->second; - cached.memberCount = 0; - cached.faction = 2; - cached.status = 0; - } + _guildCache.clear(); + + for (auto& nameEntry : _guildNames) + nameEntry.second = true; } void PlayerbotGuildMgr::LoadGuildNames() From ad8e8444d18a289a66ba75acdd77f7bc9212e75f Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:22:13 -0700 Subject: [PATCH 84/87] clean up for DropQuestAction (#2326) ## Pull Request Description I was getting annoyed by the constant "No event owner detected" message after I enabled bot debugging messaging. And then I figured that there is no reason that maintenance should trigger this action every 5 seconds. So I swapped it to seldom, so it runs every 5 mins. More Performance! ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Actions/DropQuestAction.cpp | 3 --- src/Ai/Base/Strategy/MaintenanceStrategy.cpp | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Ai/Base/Actions/DropQuestAction.cpp b/src/Ai/Base/Actions/DropQuestAction.cpp index 61ab4254a79..f6712abf3c7 100644 --- a/src/Ai/Base/Actions/DropQuestAction.cpp +++ b/src/Ai/Base/Actions/DropQuestAction.cpp @@ -62,10 +62,7 @@ bool CleanQuestLogAction::Execute(Event event) { Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); if (!requester) - { - botAI->TellMaster("No event owner detected"); return false; - } if (!sPlayerbotAIConfig.dropObsoleteQuests) return false; diff --git a/src/Ai/Base/Strategy/MaintenanceStrategy.cpp b/src/Ai/Base/Strategy/MaintenanceStrategy.cpp index 855555eddab..428d09a0024 100644 --- a/src/Ai/Base/Strategy/MaintenanceStrategy.cpp +++ b/src/Ai/Base/Strategy/MaintenanceStrategy.cpp @@ -13,7 +13,7 @@ void MaintenanceStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode( - "random", + "seldom", { NextAction("clean quest log", 6.0f) } From 605f1d7aaa8823d70422583cb226d9a155885edb Mon Sep 17 00:00:00 2001 From: ThePenguinMan96 Date: Fri, 24 Apr 2026 14:22:52 -0700 Subject: [PATCH 85/87] PvP Gear, Autogear Tuning, and Stat Weight Corrections (#2322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description Hello playerbots community! I have been working diligently whilst on vacation to help get pvp gear up and running for pvp specs. Throughout this process, I have looked at our current autogear system, tested it through and through, and made some changes to make gearing more appropriate per spec. _I am going to have my description of the changes in italics_, **and the AI description overview will be bolded.** Let's begin! **This PR makes some improvements to the bot autogear system across item scoring, spec tracking(pvp specs and gear), and stat weights. Changes are split between those that are always active and those controlled by new config options.** **Mandatory Changes:** **PvP Spec Detection (IsSpecPvp) A new method RandomPlayerbotMgr::IsSpecPvp(botGuid, cls) checks the bot's stored specNo against the spec name string defined in config. If the name contains "pvp", the bot is treated as a PvP spec throughout the entire gear pipeline. This is the single source of truth used by both InitEquipment() and ItemUsageValue. In the future this detection can be expanded to drive bot behavior decisions — such as prioritizing dueling players in the world, joining Wintergrasp, or preferring BG and Arena queues over PvE content.** _This is scalable, so if someone were to create their own pvp spec in the config, it would still be tracked if the name contains "pvp". I like the idea of pvp specced random bots having an identifier for pvp events._ **PvP Weights Applied During Loot Evaluation ItemUsageValue::QueryItemUsageForEquip() now calls IsSpecPvp() before scoring a looted item. If the bot is on a PvP spec, it passes SetPvpSpec(true) to the StatsWeightCalculator, ensuring looted items are evaluated with PvP stat priorities (including resilience weighting) rather than PvE weights. Previously, a PvP-specced bot would score loot identically to a PvE bot.** _So, during autogear and upgrade equips, pvp specced bots will now heavily prioritize resilience. On the flip side, pve bots really don't want resilience gear, so a negative weight modifier (penalty for resilience items) has been applied to pve autogearing and upgrade equips. This is important, because you can switch a bot from a pve spec to a pvp spec, and it will automatically consider resilience items in it's inventory as upgrades, and equip them. Same for when you switch a bot from a pvp spec back to a pve spec - the resilience penalty will encourage the bot to switch back to the best available pve gear._ **Resilience Weighting After all per-spec weights are generated in GenerateBasicWeights(), a global resilience modifier is applied unconditionally:** **PvP specs: +7.0 resilience weight — strongly prioritizes resilience gear Non-PvP specs: −3.0 resilience weight — actively discourages resilience gear Resilience is additionally excluded entirely from trinket slot scoring via SetExcludeResilience(true), preventing the PvP resilience bonus from inflating the scores of non-CC trinkets.** _I tried several different numbers here - as high as 10 and as low as 3 for resilience. I ended up with 7 so nearly all specs will slot resilience in every slot EXCEPT for trinkets. I stopped weighing resilience on trinkets because they ended up being garbage trinkets for the most part - other endgame pve trinkets were way more impactful. In my testing, the only class/specs that wont use 100% resilience gears are the tanks, since defense rating/parry/block/dodge weights are so high._ **CC-Break Trinket Cache At server startup, PlayerbotFactory::BuildCcBreakTrinketCache() queries the world database for all trinkets (InventoryType=12, Quality≥2) whose spell IDs include spell 42292 — the CC-break / PvP trinket effect shared by items like Medallion of the Alliance/Horde. Results are sorted by item level descending and cached in a static vector, ready for fast lookup during gearing.** _This creates a cache of cc trinkets on startup, for this:_ **CC-Break Trinket Force-Equip During InitEquipment(), PvP-specced bots at level 50 or higher (level minimum for autogear to apply trinkets) run a pre-selection pass over ccBreakTrinketCache to find the best CC-break trinket they meet the level requirement and quality limit for. Human and Undead bots are excluded from this — they have racial abilities (Every Man for Himself, Will of the Forsaken) that share the PvP trinket cooldown, making a dedicated trinket redundant.** **If a suitable trinket is found, it is stored as pvpTrinket1 and force-equipped into TRINKET1 before the main gear loop runs. If an item already occupies the slot, it is moved to bags first. The second-chance pass also skips TRINKET1 when pvpTrinket1 is set, so the CC trinket is never overwritten.** _This is the catch-all forced pvp trinket for trinket slot 1. In my testing, I really found out how few cc trinkets there are - most of them are epic, and blue ones start showing up super late in the game. An heirloom patch would really help the lower levels, being able to equip a pvp trinket at level 10 or something. Keep in mind, that if your bot isn't getting a pvp trinket with autogear, make sure they aren't human or undead, and check your config for what quality items are allowed with autogear. NOTE - PVP TRINKET STRATEGIES ARE NOT CURRENTLY CODED, SAME WITH CC RACIALS. They will not break out of stun/cc currently. This is for future updates if/when I make a trinketstrategy._ **Enhancement Shaman Dual Wield Fix Classes like Rogues, Frost DKs, and Fury Warriors have their dual wield capability established through class initialization code in the core. Enhancement Shamans acquire Dual Wield only through a specific talent (spell 30798, learned around level 40), and the bot factory had no code to detect and apply this. The result was that Enhancement Shaman bots would sometimes have their offhand weapon unequipped — despite having the talent. After talents are applied in both InitTalentsTree() and InitTalentsBySpecNo(), the code now checks for spell 30798 and explicitly grants SKILL_DUAL_WIELD and SetCanDualWield(true) when present.** _When testing the weapon speed preferences, I noticed that randombot enhancement shamans were unequipping their offhand randomly. They would just walk around with a single 1-hand weapon. This is because they were not considered in the system as dual wielding, so when initequipment or autoequipupgrades was ran, it would unequip the offhand through a function, despite having the dual wield talent. Looking at the code, the other classes already have this flag (warriors, rogues, dks, hunters) because they didn't acquire it through talents._ **CalculateItem() Slot Awareness StatsWeightCalculator::CalculateItem() now accepts an optional slot parameter (default -1). When provided and the item is a weapon, ApplyWeaponSpeedGovernance() can be called. Both item scoring calls inside InitEquipment() — the candidate scoring loop and the incremental old-item comparison — now pass the current equipment slot.** _This change allows the calculate item function to know what slot it's working with, and that's how it modifies it's decision making for some of the optional features below._ **Holy Paladin Weapon Scoring Fix Prior to this change, Holy Paladin could end up equipping 2H weapons because haste and crit sticks (2H weapons) were outscoring appropriate 1H caster weapons — the item type penalty was not catching them correctly. Holy Paladin is now explicitly added to the dual-wield penalty group (preventing 2H weapons from being viable), excluded from the generic caster 1H penalty (since they use 1H + shield rather than a staff), and given a 0.8x soft preference for 1H weapons.** _In autogear testing, sometimes 2h weps with high crit/haste would win over caster gear - this is especially noticeable at lower levels, with shallower item pools (greens only). You'd hit autogear and the holy paladin would equip a 2h axe with crit :( So this makes it so holy paladins only use 1h weapons. They can use either a shield or an offhand, depending on stat weights._ **PvP Spec Slots Added for All Classes The existing RandomClassSpecProb / RandomClassSpecIndex config entries control what percentage of random bots in the world are assigned each spec. Previously only PvE specs (indices 0–2, or 0–3 for Druids) were defined, giving server operators no way to introduce PvP-specced random bots into the world population. This PR adds PvP spec slots for every class (indices 3–6 depending on class), all defaulting to 0 probability. Server operators can raise these values to spawn PvP-specced random bots — e.g., setting RandomClassSpecProb.1.3 = 20 would make 20% of Warrior bots run Arms PvP. Two additional PvE specs have also been added: Death Knight index 3: Double-aura Blood (a hybrid Blood/Frost PvE tank variant) Mage index 3: Frostfire (a PvE hybrid spec) All existing spec entries have been annotated with comments identifying each one (e.g., # arms pve, # holy pve) for readability.** _This change was actually added at the start - I realized that there was no way for pvp-specced randombots to spawn naturally, so I added optional probabilities to the config. They are currently set at 0% by default, but giving the user the option I feel is necessary. Also, it would have been impossible for me to test the init on randombots with pvp gear otherwise. Also, I noticed that the frostfire mage and the dual-aura dk didn't have an option, so I added them in as well, as well as names above each option for quality of life._ **Stat Weight Corrections The following per-spec stat weights were adjusted to better reflect actual WotLK priorities. Entries marked NEW did not previously exist; unmarked rows show old → new values.** arms warrior _Arms warriors would prefer leather/ap gear about half of the time - the combined weights of both would often beat strength gear, especially at lower levels, or where the item pool was shallow. Also, they continued to spawn with spell power gear and defense gear occasionally, especially on gear with resilience (resilience, spell power, crit, haste, stam items)._ fury warrior _Fury warriors had the same issues as arms warrior, but really can't afford to lose a strength item - beserker stance increases strength by 20%. Also had to reduce haste here because haste really isn't nearly as important as strength, crit, arp. Haste items would win often over strength/crit/arp gear._ prot tanks _So, prot paladins and prot warriors currently are weighed identically fyi. Look at that whopping 2.0 agility - twice as important as strength? I noticed that my prot paladins/warriors were equipping agility/haste/crit items instead of defense gear on their neck, rings, trinkets, and back. This adjustment pretty much ensures that defense gear takes those slots if it's available. Removed the crit/haste weightings, because realistically if a tank wants more damage, it will just get strength. Lastly added the spell power penalty because prot paladins would spawn in fully holy gear if they were pvp specced (resilience is weighted so high, resilience/spellpower/stam/haste gear would often win). This aims to prevent that._ dps dks _Same issue with plate dps as the warriors had. Spell power gear would occasionally spawn on crit/hit items in pve, and a ton of spell power resilience gear would spawn. There is no scenario where a DK wants spell power, this isn't patch 3.0.1..._ blood dk _Similar issues to prot paladin/warrior. I was really tired of seeing block rating/value gear as a result of getting gear with defense/stam. This results in a lot more defense rating/expertise/hit/dodge/parry gear, and basically makes shield stats nearly non-existent (unless the upgrade is good enough, it could still win)_ ret paladin _Prior to this PR, the positive spellpower and int weights were enough for ret paladins to spawn with spellpower/int/haste/crit gear. This is unlikely now. And agility/ap was reduced to favor more strength gear._ Enhancement Shaman _While spell power is a decent stat on enhancment shamans, it was appearing on too much gear, especially on items that were haste/crit/spell power. And for elemental shamans, they were getting agi/haste/crit gear, so this aims to get rid of those items entirely without reducing haste/crit._ shaman pally _Holy paladins and resto shamans are scored the same, but this prevents attack power/haste/crit gear, since haste and crit are weighted high._ mage _Prevents mages from equipping/autogearing items with attack power, some attack power/crit/haste/hit items were winning with shallow item pools._ hunter rogue _Prevents hunters and rogues from getting spell power leather gear with hit/crit. Crit is very heavy for hunters so this was decently common._ **Optional Changes (Config-Controlled)** **AiPlayerbot.PreferClassArmorType (default: 0) Applies a 3x score multiplier to armor matching the bot's class-appropriate type (plate/mail/leather/cloth). A significantly better off-type item can still win — this is a soft preference, not a hard filter.** _Are you tired of your fury warrior being a leather daddy? Are you tired of your holy paladin running around in cloth lingerie? This will fix that. For mail classes (hunters/shamans) and plate classes, this only kicks in after level 40. But it really helps adhere to the highest armor class available. This would be the perfect solution to the quarterly question "Why is my paladin wearing leather?". This definitely should remain optional, as quite a few BIS lists would disagree with it. Leather at certain stages is great for hunters/shamans/warriors/dks._ **AiPlayerbot.AutogearAllowsQuestRewards (default: 0) Builds a cache of equippable armor and weapon quest rewards at startup. Bots can then equip these items during autogear, using the quest's minimum level as the effective required level gate.** _So, I noticed that autogear didn't allow items without a level requirement (quest rewards), because it didn't know how to handle that when giving out gear. It would previously just flat out reject all quest rewards, as they wouldn't be a part of the item pool. This option enables quest rewards to be considered in the item pool, and the level correlates to the lowest level you could get the quest. I have tested this for about 3 hours across all specs and using blue/green gear, it seems like a really nice bonus. Keep in mind that I do 0 quests on my way to 80, so players like me could still benefit from those items. I think this should remain optional._ **AiPlayerbot.EquipAllSlotsAtAnyLevel (default: 0) Bypasses the low-level slot restrictions in InitEquipment(): Trinkets normally locked until level 50 Head/Neck until level 30 Rings until level 20 All other non-weapon slots until level 5** _Autogear currently has level floors for slots - they will not ever give items below the above thresholds. This config option bypasses that. I have not tested this as much as I should have, so as people test this, they could let us know of items that should be blacklisted._ **AiPlayerbot.WeaponSpeedGovernance (default: 0) When enabled, ApplyWeaponSpeedGovernance() applies a 3x score multiplier to weapons matching the spec's ideal attack speed profile. Applies to mainhand, offhand, and ranged slots only. Per-spec preferences: Arms Warrior: Slow 2H (>=3400ms) in mainhand; poleaxes and axes preferred (Axe Specialization) Ret Paladin / Blood & Unholy DK: Slow 2H (>=3400ms) in mainhand Prot Warrior & Paladin: Slow 1H (>=2600ms) in mainhand Fury Warrior dual wield: Slow 1H (>=2600ms) in both hands Fury Warrior titan's grip: Slow 2H (>=3400ms) in both hands Frost DK: Slow 1H (>=2600ms) in both hands; 2H excluded Enhancement Shaman (dual wield): Slow 1H (>=2600ms) in both hands; synchronized MH/OH speeds for flurry procs Enhancement Shaman (pre-dual wield): Slow 2H (>=3400ms) in mainhand Combat Rogue: Slow MH (>=2600ms) + Fast OH (<=1500ms) Assassination / Subtlety Rogue: Slow dagger MH (>=1700ms) + Fast dagger OH (<=1500ms) Hunter: Slow ranged (>=2600ms); melee is a stat stick, speed ignored Feral Druid: No preference (forms normalize attack speed)** _Besides pvp gearing for pvp specs, I feel like this is one of the nicest additions. It was really frustrating to see an enhancement shaman put windfury on a 1.5 dagger. Without this, weights for melee dps are calculated on dps alone, not weapon speed. You'll see 2h specs use fast 2h weapons (3.0), rogues use 2 fast weapons or slow weapons, frost dks occasionally using 2h weapons while having dual wield talents. I tested this for about 6 hours across all mentioned specs at levels 20, 30, 40, 50, 60, 65, 70, 75, and 80, with 3 quality types (greens, blues, purples). I would actually consider making this mandatory, simply because of the impact I saw in the dps charts. Super happy and proud of this._ files changes ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. _Two caches are built upon startup - the pvp trinket cache and the quest reward cache. From there, this directly modifies the stat weight calculations involving initequipement (autogear) and autoequipupgrades, as both go off of stat weight calculations. I tried to implement these changes with as little custom functions and coding as possible, and relied as much as I could on the pre-existing framework._ - Describe the **processing cost** when this logic executes across many bots. _Fortunately most of the gates are boolean so it shouldn't impact performance much at all. I ran these changes on my local server with stock 500 bots, noticed no pmon difference from the main branch. Did a 24h stress test on my server yesterday, stats looked consistent with the stress test I did prior to making any changes on 3-31-26._ _It helps that it uses pre-existing functions such as initequipment and autoequipupgrades, and it really just modifies them with slightly more logic. That being said, autogear didn't lag my server at all, nor did the bots equipping upgrades._ ## How to Test the Changes _So, with the basic stock playerbots config (do not forget to copy the new config!), the only thing that should change is the pvp gear appearing on pvp specs, and the classes preferring more appropriate stats across the board. You can load into the game, level a bot to 20, autogear, and notice the difference. Same at level 40, 60, 75, or whatever. You could add in the optional config settings to further streamline the gear you want. I currently run with all 4 enabled, 2 of which increase the item pool, and 2 of which help guide them to more appropriate gear (armor/weps)._ ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) _The code is only used on startup (cache generation) and when autogear/autoequipupgrades is called. Not all the time, and not per tick. I noticed no performance impact after these changes._ - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) _It modifies the decision making as far as equipment goes, but as far as priority/strategies, this does not affect that._ - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) _Not to my knowledge, but I'll rely on testers and the community to let me know if it does._ ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) _AI was used in the research of the initequipment system, stat weights, and cache building. As far as generating the code, using AI was 2 steps forward, 1 step back. I used Claude Code with Sonnet 4.6 (high) and had gemini/copilot review the work. **AI did generate a large portion of the code being used.** I have personally reviewed every line, and a lot was removed out of being obsolete/new system that copied an old one/too many comments. I don't think anything else can be trimmed, though. I also used AI in the PR description, and made my own comments in italics below each entry. I hate explaining/writing._ ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers _I would like atleast 5-10 people to review this over the next 1-6 months. The big problem I used to have with my PRs was I was acting like they were a sprint, when it's really a marathon - good changes take time, and I was too quick to bust out new content. The old PRs I made introduced just as many new bugs as they did features. I learned my lesson, and have tested this extensively (code was pretty much complete on 4-10-26, been testing alone for the last 11 days) and it's ready for the test realm for others to try out. I think it's going to be a good step forward when it comes to gear decision making for bots as a whole. PvPers have come and gone too much from this project due to the lack of options, and this helps captivate that audience. Please reach out to me on discord at Zhur#4391, I am happy to hear results/suggestions there as well as here._ --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> --- conf/playerbots.conf.dist | 148 ++++++++++++++ src/Ai/Base/Value/ItemUsageValue.cpp | 5 + src/Bot/Factory/PlayerbotFactory.cpp | 172 +++++++++++++++- src/Bot/Factory/PlayerbotFactory.h | 5 +- src/Bot/RandomPlayerbotMgr.cpp | 10 + src/Bot/RandomPlayerbotMgr.h | 1 + src/Mgr/Item/StatsWeightCalculator.cpp | 261 +++++++++++++++++++++---- src/Mgr/Item/StatsWeightCalculator.h | 7 +- src/PlayerbotAIConfig.cpp | 2 + src/PlayerbotAIConfig.h | 2 + 10 files changed, 565 insertions(+), 48 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 5ce5485d094..3d0866f44e0 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -806,6 +806,27 @@ AiPlayerbot.RandomGearQualityLimit = 3 # Default: 0 (no limit) AiPlayerbot.RandomGearScoreLimit = 0 +# Prefer armor of the class's ideal type: apply 3x score multiplier to class-appropriate armor. +# When enabled, Warriors strongly prefer plate, Shamans prefer mail, etc. +# A truly superior item can still win (no hard filtering), but same-quality +# armor of the preferred type will score 3x higher and be equipped instead. +# +# ARMOR TYPE PREFERENCES: +# Plate: Warriors, Paladins, Death Knights +# Mail: Hunters, Shamans +# Leather: Rogues, Druids +# Cloth: Priests, Mages, Warlocks +# +# Default: 0 (disabled) +AiPlayerbot.PreferClassArmorType = 0 + + +# When enabled, bots prefer spec-appropriate weapons based on speed and weapon type during autogear. +# Examples: Arms Warriors favor slow 2H axes/polearms (Axe Specialization), Combat Rogues +# favor a slow MH with a fast OH, and Enhancement Shamans favor synchronized slow 1H weapons. +# Default: 0 (disabled) +AiPlayerbot.PreferredSpecWeapons = 0 + # If disabled, random bots can only upgrade equipment through looting and quests # Default: 1 (enabled) AiPlayerbot.IncrementalGearInit = 1 @@ -1836,12 +1857,24 @@ AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIO # # +# arms pve AiPlayerbot.RandomClassSpecProb.1.0 = 20 AiPlayerbot.RandomClassSpecIndex.1.0 = 0 +# fury pve AiPlayerbot.RandomClassSpecProb.1.1 = 40 AiPlayerbot.RandomClassSpecIndex.1.1 = 1 +# prot pve AiPlayerbot.RandomClassSpecProb.1.2 = 40 AiPlayerbot.RandomClassSpecIndex.1.2 = 2 +# arms pvp +AiPlayerbot.RandomClassSpecProb.1.3 = 0 +AiPlayerbot.RandomClassSpecIndex.1.3 = 3 +# fury pvp +AiPlayerbot.RandomClassSpecProb.1.4 = 0 +AiPlayerbot.RandomClassSpecIndex.1.4 = 4 +# prot pvp +AiPlayerbot.RandomClassSpecProb.1.5 = 0 +AiPlayerbot.RandomClassSpecIndex.1.5 = 5 # # @@ -1853,12 +1886,24 @@ AiPlayerbot.RandomClassSpecIndex.1.2 = 2 # # +# holy pve AiPlayerbot.RandomClassSpecProb.2.0 = 30 AiPlayerbot.RandomClassSpecIndex.2.0 = 0 +# prot pve AiPlayerbot.RandomClassSpecProb.2.1 = 40 AiPlayerbot.RandomClassSpecIndex.2.1 = 1 +# ret pve AiPlayerbot.RandomClassSpecProb.2.2 = 30 AiPlayerbot.RandomClassSpecIndex.2.2 = 2 +# holy pvp +AiPlayerbot.RandomClassSpecProb.2.3 = 0 +AiPlayerbot.RandomClassSpecIndex.2.3 = 3 +# prot pvp +AiPlayerbot.RandomClassSpecProb.2.4 = 0 +AiPlayerbot.RandomClassSpecIndex.2.4 = 4 +# ret pvp +AiPlayerbot.RandomClassSpecProb.2.5 = 0 +AiPlayerbot.RandomClassSpecIndex.2.5 = 5 # # @@ -1870,12 +1915,24 @@ AiPlayerbot.RandomClassSpecIndex.2.2 = 2 # # +# bm pve AiPlayerbot.RandomClassSpecProb.3.0 = 33 AiPlayerbot.RandomClassSpecIndex.3.0 = 0 +# mm pve AiPlayerbot.RandomClassSpecProb.3.1 = 33 AiPlayerbot.RandomClassSpecIndex.3.1 = 1 +# surv pve AiPlayerbot.RandomClassSpecProb.3.2 = 33 AiPlayerbot.RandomClassSpecIndex.3.2 = 2 +# bm pvp +AiPlayerbot.RandomClassSpecProb.3.3 = 0 +AiPlayerbot.RandomClassSpecIndex.3.3 = 3 +# mm pvp +AiPlayerbot.RandomClassSpecProb.3.4 = 0 +AiPlayerbot.RandomClassSpecIndex.3.4 = 4 +# surv pvp +AiPlayerbot.RandomClassSpecProb.3.5 = 0 +AiPlayerbot.RandomClassSpecIndex.3.5 = 5 # # @@ -1887,12 +1944,24 @@ AiPlayerbot.RandomClassSpecIndex.3.2 = 2 # # +# as pve AiPlayerbot.RandomClassSpecProb.4.0 = 45 AiPlayerbot.RandomClassSpecIndex.4.0 = 0 +# combat pve AiPlayerbot.RandomClassSpecProb.4.1 = 45 AiPlayerbot.RandomClassSpecIndex.4.1 = 1 +# subtlety pve AiPlayerbot.RandomClassSpecProb.4.2 = 10 AiPlayerbot.RandomClassSpecIndex.4.2 = 2 +# as pvp +AiPlayerbot.RandomClassSpecProb.4.3 = 0 +AiPlayerbot.RandomClassSpecIndex.4.3 = 3 +# combat pvp +AiPlayerbot.RandomClassSpecProb.4.4 = 0 +AiPlayerbot.RandomClassSpecIndex.4.4 = 4 +# subtlety pvp +AiPlayerbot.RandomClassSpecProb.4.5 = 0 +AiPlayerbot.RandomClassSpecIndex.4.5 = 5 # # @@ -1904,12 +1973,24 @@ AiPlayerbot.RandomClassSpecIndex.4.2 = 2 # # +# disc pve AiPlayerbot.RandomClassSpecProb.5.0 = 40 AiPlayerbot.RandomClassSpecIndex.5.0 = 0 +# holy pve AiPlayerbot.RandomClassSpecProb.5.1 = 35 AiPlayerbot.RandomClassSpecIndex.5.1 = 1 +# shadow pve AiPlayerbot.RandomClassSpecProb.5.2 = 25 AiPlayerbot.RandomClassSpecIndex.5.2 = 2 +# disc pvp +AiPlayerbot.RandomClassSpecProb.5.3 = 0 +AiPlayerbot.RandomClassSpecIndex.5.3 = 3 +# holy pvp +AiPlayerbot.RandomClassSpecProb.5.4 = 0 +AiPlayerbot.RandomClassSpecIndex.5.4 = 4 +# shadow pvp +AiPlayerbot.RandomClassSpecProb.5.5 = 0 +AiPlayerbot.RandomClassSpecIndex.5.5 = 5 # # @@ -1921,12 +2002,27 @@ AiPlayerbot.RandomClassSpecIndex.5.2 = 2 # # +# blood pve AiPlayerbot.RandomClassSpecProb.6.0 = 30 AiPlayerbot.RandomClassSpecIndex.6.0 = 0 +# frost pve AiPlayerbot.RandomClassSpecProb.6.1 = 40 AiPlayerbot.RandomClassSpecIndex.6.1 = 1 +# unholy pve AiPlayerbot.RandomClassSpecProb.6.2 = 30 AiPlayerbot.RandomClassSpecIndex.6.2 = 2 +# double aura blood pve +AiPlayerbot.RandomClassSpecProb.6.3 = 0 +AiPlayerbot.RandomClassSpecIndex.6.3 = 3 +# blood pvp +AiPlayerbot.RandomClassSpecProb.6.4 = 0 +AiPlayerbot.RandomClassSpecIndex.6.4 = 4 +# frost pvp +AiPlayerbot.RandomClassSpecProb.6.5 = 0 +AiPlayerbot.RandomClassSpecIndex.6.5 = 5 +# unholy pvp +AiPlayerbot.RandomClassSpecProb.6.6 = 0 +AiPlayerbot.RandomClassSpecIndex.6.6 = 6 # # @@ -1938,12 +2034,24 @@ AiPlayerbot.RandomClassSpecIndex.6.2 = 2 # # +# ele pve AiPlayerbot.RandomClassSpecProb.7.0 = 33 AiPlayerbot.RandomClassSpecIndex.7.0 = 0 +# enh pve AiPlayerbot.RandomClassSpecProb.7.1 = 33 AiPlayerbot.RandomClassSpecIndex.7.1 = 1 +# resto pve AiPlayerbot.RandomClassSpecProb.7.2 = 33 AiPlayerbot.RandomClassSpecIndex.7.2 = 2 +# ele pvp +AiPlayerbot.RandomClassSpecProb.7.3 = 0 +AiPlayerbot.RandomClassSpecIndex.7.3 = 3 +# enh pvp +AiPlayerbot.RandomClassSpecProb.7.4 = 0 +AiPlayerbot.RandomClassSpecIndex.7.4 = 4 +# resto pvp +AiPlayerbot.RandomClassSpecProb.7.5 = 0 +AiPlayerbot.RandomClassSpecIndex.7.5 = 5 # # @@ -1955,12 +2063,27 @@ AiPlayerbot.RandomClassSpecIndex.7.2 = 2 # # +# arcane pve AiPlayerbot.RandomClassSpecProb.8.0 = 30 AiPlayerbot.RandomClassSpecIndex.8.0 = 0 +# fire pve AiPlayerbot.RandomClassSpecProb.8.1 = 30 AiPlayerbot.RandomClassSpecIndex.8.1 = 1 +# frost pve AiPlayerbot.RandomClassSpecProb.8.2 = 40 AiPlayerbot.RandomClassSpecIndex.8.2 = 2 +# frostfire pve +AiPlayerbot.RandomClassSpecProb.8.3 = 0 +AiPlayerbot.RandomClassSpecIndex.8.3 = 3 +# arcane pvp +AiPlayerbot.RandomClassSpecProb.8.4 = 0 +AiPlayerbot.RandomClassSpecIndex.8.4 = 4 +# fire pvp +AiPlayerbot.RandomClassSpecProb.8.5 = 0 +AiPlayerbot.RandomClassSpecIndex.8.5 = 5 +# frost pvp +AiPlayerbot.RandomClassSpecProb.8.6 = 0 +AiPlayerbot.RandomClassSpecIndex.8.6 = 6 # # @@ -1972,12 +2095,24 @@ AiPlayerbot.RandomClassSpecIndex.8.2 = 2 # # +# affli pve AiPlayerbot.RandomClassSpecProb.9.0 = 33 AiPlayerbot.RandomClassSpecIndex.9.0 = 0 +# demo pve AiPlayerbot.RandomClassSpecProb.9.1 = 34 AiPlayerbot.RandomClassSpecIndex.9.1 = 1 +# destro pve AiPlayerbot.RandomClassSpecProb.9.2 = 33 AiPlayerbot.RandomClassSpecIndex.9.2 = 2 +# affli pvp +AiPlayerbot.RandomClassSpecProb.9.3 = 0 +AiPlayerbot.RandomClassSpecIndex.9.3 = 3 +# demo pvp +AiPlayerbot.RandomClassSpecProb.9.4 = 0 +AiPlayerbot.RandomClassSpecIndex.9.4 = 4 +# destro pvp +AiPlayerbot.RandomClassSpecProb.9.5 = 0 +AiPlayerbot.RandomClassSpecIndex.9.5 = 5 # # @@ -1989,14 +2124,27 @@ AiPlayerbot.RandomClassSpecIndex.9.2 = 2 # # +# balance pve AiPlayerbot.RandomClassSpecProb.11.0 = 20 AiPlayerbot.RandomClassSpecIndex.11.0 = 0 +# bear pve AiPlayerbot.RandomClassSpecProb.11.1 = 25 AiPlayerbot.RandomClassSpecIndex.11.1 = 1 +# resto pve AiPlayerbot.RandomClassSpecProb.11.2 = 35 AiPlayerbot.RandomClassSpecIndex.11.2 = 2 +# cat pve AiPlayerbot.RandomClassSpecProb.11.3 = 20 AiPlayerbot.RandomClassSpecIndex.11.3 = 3 +# balance pvp +AiPlayerbot.RandomClassSpecProb.11.4 = 0 +AiPlayerbot.RandomClassSpecIndex.11.4 = 4 +# cat pvp +AiPlayerbot.RandomClassSpecProb.11.5 = 0 +AiPlayerbot.RandomClassSpecIndex.11.5 = 5 +# resto pvp +AiPlayerbot.RandomClassSpecProb.11.6 = 0 +AiPlayerbot.RandomClassSpecIndex.11.6 = 6 # # diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 88937212705..c3d976f0fd0 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -234,6 +234,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, calculator.SetItemSetBonus(false); calculator.SetOverflowPenalty(false); + // Apply PvP weights if the bot is specced for PvP + bool isPvp = sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass()); + if (isPvp) + calculator.SetPvpSpec(true); + float itemScore = calculator.CalculateItem(itemProto->ItemId, randomPropertyId); if (itemScore) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index e7021e3723a..f8e0e5dfe27 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -59,6 +59,7 @@ std::list PlayerbotFactory::specialQuestIds; std::vector PlayerbotFactory::enchantSpellIdCache; std::vector PlayerbotFactory::enchantGemIdCache; std::unordered_map> PlayerbotFactory::trainerIdCache; +std::vector PlayerbotFactory::ccBreakTrinketCache; bool PlayerbotFactory::IsPrimaryTradeSkill(uint16 skillId) { @@ -460,6 +461,69 @@ void PlayerbotFactory::Init() enchantGemIdCache.push_back(gemId); } LOG_INFO("playerbots", "Loading {} enchantment gems", enchantGemIdCache.size()); + + BuildCcBreakTrinketCache(); +} + +void PlayerbotFactory::BuildCcBreakTrinketCache() +{ + ccBreakTrinketCache.clear(); + // Spell 42292: removes all movement-impairing and loss-of-control effects — the PvP trinket spell. + QueryResult result = WorldDatabase.Query( + "SELECT entry, ItemLevel FROM item_template " + "WHERE Quality >= 2 AND InventoryType = 12 " + "AND (FlagsExtra & 8192) = 0 " + "AND (spellid_1 = 42292 OR spellid_2 = 42292 OR spellid_3 = 42292 " + " OR spellid_4 = 42292 OR spellid_5 = 42292)"); + + if (!result) + { + LOG_INFO("playerbots", "CC-break trinket cache: no items found."); + return; + } + + struct CcItem { uint32 itemId; uint16 itemLevel; }; + std::vector tmp; + do + { + Field* f = result->Fetch(); + tmp.push_back({f[0].Get(), f[1].Get()}); + } while (result->NextRow()); + + std::sort(tmp.begin(), tmp.end(), [](const CcItem& a, const CcItem& b) { + return a.itemLevel > b.itemLevel; + }); + for (auto& c : tmp) + ccBreakTrinketCache.push_back(c.itemId); + + LOG_INFO("playerbots", "CC-break trinket cache: {} items.", ccBreakTrinketCache.size()); +} + +uint8 PlayerbotFactory::GetPreferredArmorType(uint8 cls) +{ + switch (cls) + { + case CLASS_WARRIOR: + case CLASS_PALADIN: + case CLASS_DEATH_KNIGHT: + return ITEM_SUBCLASS_ARMOR_PLATE; + + case CLASS_HUNTER: + case CLASS_SHAMAN: + return ITEM_SUBCLASS_ARMOR_MAIL; + + case CLASS_ROGUE: + case CLASS_DRUID: + return ITEM_SUBCLASS_ARMOR_LEATHER; + + case CLASS_PRIEST: + case CLASS_MAGE: + case CLASS_WARLOCK: + return ITEM_SUBCLASS_ARMOR_CLOTH; + + default: + return 0; + } } void PlayerbotFactory::Prepare() @@ -561,9 +625,9 @@ void PlayerbotFactory::Randomize(bool incremental) if (!incremental || !sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) { - InitTalentsTree(); + uint32 specIndex = InitTalentsTree(); + sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", specIndex + 1); } - sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", 0); if (botAI) { PlayerbotRepository::instance().Reset(botAI); @@ -1359,7 +1423,7 @@ void PlayerbotFactory::ResetQuests() } } -void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/) +uint32 PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/) { uint32 specTab; uint8 cls = bot->getClass(); @@ -1427,7 +1491,14 @@ void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_templa if (bot->GetFreeTalentPoints()) InitTalents((specTab + 2) % 3); + if (bot->getClass() == CLASS_SHAMAN && bot->HasSpell(30798)) + { + bot->SetSkill(SKILL_DUAL_WIELD, 0, 1, 1); + bot->SetCanDualWield(true); + } + bot->SendTalentsInfoData(false); + return sPlayerbotAIConfig.randomClassSpecIndex[cls][specTab]; } void PlayerbotFactory::InitTalentsBySpecNo(Player* bot, int specNo, bool reset) @@ -1505,7 +1576,15 @@ void PlayerbotFactory::InitTalentsBySpecNo(Player* bot, int specNo, bool reset) break; } } + + if (bot->getClass() == CLASS_SHAMAN && bot->HasSpell(30798)) + { + bot->SetSkill(SKILL_DUAL_WIELD, 0, 1, 1); + bot->SetCanDualWield(true); + } + bot->SendTalentsInfoData(false); + sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", (uint32)specNo + 1); } void PlayerbotFactory::InitTalentsByParsedSpecLink(Player* bot, std::vector> parsedSpecLink, @@ -2008,7 +2087,35 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) uint32 blevel = bot->GetLevel(); int32 delta = std::min(blevel, 10u); + bool isPvp = sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass()); + StatsWeightCalculator calculator(bot); + if (isPvp) + calculator.SetPvpSpec(true); + + // Pre-select CC-break trinket for PvP specs: best available by item level + // that the bot meets the level requirement for. + // Humans (Every Man for Himself) and Undead (Will of the Forsaken) have a + // racial that shares the PvP trinket cooldown, so they don't need one. + bool racialHasCcBreak = (bot->getRace() == RACE_HUMAN || bot->getRace() == RACE_UNDEAD_PLAYER); + uint32 pvpTrinket1 = 0; + if (isPvp && level >= 50 && !racialHasCcBreak) + { + for (uint32 itemId : ccBreakTrinketCache) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) continue; + // Respect gear quality limit: trinket must not exceed itemQuality setting + if (static_cast(proto->Quality) > itemQuality) continue; + if (proto->RequiredLevel > level) continue; + if (!CanEquipItem(proto)) continue; + uint16 dest; + if (!CanEquipUnseenItem(EQUIPMENT_SLOT_TRINKET1, dest, itemId)) continue; + pvpTrinket1 = itemId; + break; + } + } + for (int32 slot : initSlotsOrder) { if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY) @@ -2028,6 +2135,10 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) (slot != EQUIPMENT_SLOT_RANGED)) continue; + // Exclude resilience weighting for trinkets + bool isTrinketSlot = (slot == EQUIPMENT_SLOT_TRINKET1 || slot == EQUIPMENT_SLOT_TRINKET2); + calculator.SetExcludeResilience(isTrinketSlot); + Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); if (second_chance && oldItem) @@ -2037,6 +2148,28 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + // PvP specs: force TRINKET1 to the best available CC-break trinket. + if (slot == EQUIPMENT_SLOT_TRINKET1 && pvpTrinket1 != 0) + { + if (oldItem) + { + uint8 bagIndex = oldItem->GetBagSlot(); + uint8 oldSlot = oldItem->GetSlot(); + uint8 dstBag = NULL_BAG; + WorldPacket packet(CMSG_AUTOSTORE_BAG_ITEM, 3); + packet << bagIndex << oldSlot << dstBag; + WorldPackets::Item::AutoStoreBagItem nicePacket(std::move(packet)); + nicePacket.Read(); + bot->GetSession()->HandleAutoStoreBagItemOpcode(nicePacket); + oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + if (oldItem) continue; + } + uint16 dest; + if (CanEquipUnseenItem(slot, dest, pvpTrinket1)) + bot->EquipNewItem(dest, pvpTrinket1, true); + continue; + } + int32 desiredQuality = itemQuality; if (urand(0, 100) < 100 * sPlayerbotAIConfig.randomGearLoweringChance && desiredQuality > ITEM_QUALITY_NORMAL) desiredQuality--; @@ -2054,6 +2187,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (urand(1, 100) <= skipProb) continue; + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); // disable next expansion gear if (sPlayerbotAIConfig.limitGearExpansion && bot->GetLevel() <= 60 && itemId >= 23728) continue; @@ -2064,7 +2198,6 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) // wearable TBC items above 35570 but nothing of significance continue; - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (!proto) continue; @@ -2115,7 +2248,15 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) ItemTemplate const* proto = sObjectMgr->GetItemTemplate(newItemId); - float cur_score = calculator.CalculateItem(newItemId); + float cur_score = calculator.CalculateItem(newItemId, 0, slot); + + if (cur_score > 0.0f && proto && proto->Class == ITEM_CLASS_ARMOR && sPlayerbotAIConfig.preferClassArmorType) + { + uint8 preferredArmorType = GetPreferredArmorType(bot->getClass()); + if (preferredArmorType != 0 && proto->SubClass == preferredArmorType) + cur_score *= 3.0f; // 3x multiplier for preferred armor type + } + if (cur_score > bestScoreForSlot) { // delay heavy check to here @@ -2141,7 +2282,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (incremental && oldItem) { - float old_score = calculator.CalculateItem(oldItem->GetEntry(), oldItem->GetItemRandomPropertyId()); + float old_score = calculator.CalculateItem(oldItem->GetEntry(), oldItem->GetItemRandomPropertyId(), slot); if (bestScoreForSlot < 1.2f * old_score) continue; } @@ -2194,7 +2335,14 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) (slot != EQUIPMENT_SLOT_RANGED)) continue; - if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot) != nullptr) + // CC-break trinket was force-equipped in the main pass; leave it alone. + if (slot == EQUIPMENT_SLOT_TRINKET1 && pvpTrinket1 != 0) + continue; + + bool isTrinketSlot = (slot == EQUIPMENT_SLOT_TRINKET1 || slot == EQUIPMENT_SLOT_TRINKET2); + calculator.SetExcludeResilience(isTrinketSlot); + + if (Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); std::vector& ids = items[slot]; @@ -2209,7 +2357,15 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) ItemTemplate const* proto = sObjectMgr->GetItemTemplate(newItemId); - float cur_score = calculator.CalculateItem(newItemId); + float cur_score = calculator.CalculateItem(newItemId, 0, slot); + + if (cur_score > 0.0f && proto && proto->Class == ITEM_CLASS_ARMOR && sPlayerbotAIConfig.preferClassArmorType) + { + uint8 preferredArmorType = GetPreferredArmorType(bot->getClass()); + if (preferredArmorType != 0 && proto->SubClass == preferredArmorType) + cur_score *= 3.0f; // 3x multiplier for preferred armor type + } + if (cur_score > bestScoreForSlot) { // delay heavy check to here diff --git a/src/Bot/Factory/PlayerbotFactory.h b/src/Bot/Factory/PlayerbotFactory.h index 0e18e6b83ba..ba32e6a118d 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -63,7 +63,7 @@ class PlayerbotFactory static uint32 tradeSkills[]; static float CalculateEnchantScore(uint32 enchant_id, Player* bot); - void InitTalentsTree(bool incremental = false, bool use_template = true, bool reset = false); + uint32 InitTalentsTree(bool incremental = false, bool use_template = true, bool reset = false); static void InitTalentsBySpecNo(Player* bot, int specNo, bool reset); static void InitTalentsByParsedSpecLink(Player* bot, std::vector> parsedSpecLink, bool reset); void InitAvailableSpells(); @@ -190,6 +190,8 @@ class PlayerbotFactory std::vector GetCurrentGemsCount(); bool CanEquipArmor(ItemTemplate const* proto); bool CanEquipWeapon(ItemTemplate const* proto); + static void BuildCcBreakTrinketCache(); + uint8 GetPreferredArmorType(uint8 cls); void EnchantItem(Item* item); void AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank); bool CheckItemStats(uint8 sp, uint8 ap, uint8 tank); @@ -220,6 +222,7 @@ class PlayerbotFactory static std::unordered_map> trainerIdCache; static std::vector enchantSpellIdCache; static std::vector enchantGemIdCache; + static std::vector ccBreakTrinketCache; protected: EnchantContainer m_EnchantContainer; diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 110a1942e9c..5c0922fb9ed 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -2266,6 +2266,16 @@ CachedEvent* RandomPlayerbotMgr::FindEvent(uint32 bot, std::string const& event) return &e; } +bool RandomPlayerbotMgr::IsSpecPvp(uint32 bot, uint8 cls) +{ + uint32 stored = GetValue(bot, "specNo"); + if (!stored) + return false; + uint32 specIndex = stored - 1; + std::string const& name = sPlayerbotAIConfig.premadeSpecName[cls][specIndex]; + return !name.empty() && name.find("pvp") != std::string::npos; +} + uint32 RandomPlayerbotMgr::GetEventValue(uint32 bot, std::string const& event) { if (CachedEvent* e = FindEvent(bot, event)) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index db74f2cbea6..b68c77e419a 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -140,6 +140,7 @@ class RandomPlayerbotMgr : public PlayerbotHolder std::string GetData(uint32 bot, std::string const& type); void SetValue(uint32 bot, std::string const& type, uint32 value, std::string const& data = ""); void SetValue(Player* bot, std::string const& type, uint32 value, std::string const& data = ""); + bool IsSpecPvp(uint32 bot, uint8 cls); void Remove(Player* bot); ObjectGuid GetBattleMasterGUID(Player* bot, BattlegroundTypeId bgTypeId); CreatureData const* GetCreatureDataByEntry(uint32 entry); diff --git a/src/Mgr/Item/StatsWeightCalculator.cpp b/src/Mgr/Item/StatsWeightCalculator.cpp index faa06ff0fe6..b232b1e6b64 100644 --- a/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/src/Mgr/Item/StatsWeightCalculator.cpp @@ -72,7 +72,7 @@ void StatsWeightCalculator::Reset() } } -float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyIds) +float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyIds, int32 slot) { ItemTemplate const* proto = &sObjectMgr->GetItemTemplateStore()->at(itemId); @@ -111,10 +111,12 @@ float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyId weight_ *= PlayerbotFactory::CalcMixedGearScore(lvl, ITEM_QUALITY_EPIC); else weight_ *= PlayerbotFactory::CalcMixedGearScore(proto->ItemLevel, proto->Quality); - - return weight_; } - // If quality/level blending is disabled, also return the calculated weight. + + // Apply weapon speed governance if slot is provided and this is a weapon + if (sPlayerbotAIConfig.preferredSpecWeapons && slot >= 0 && proto->Class == ITEM_CLASS_WEAPON) + weight_ *= ApplyPreferredSpecWeapons(proto, slot); + return weight_; } @@ -212,6 +214,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.7f; stats_weights_[STATS_TYPE_CRIT] += 1.4f; stats_weights_[STATS_TYPE_HASTE] += 1.6f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 7.5f; } else if (cls == CLASS_HUNTER && tab == HUNTER_TAB_MARKSMANSHIP) @@ -222,6 +225,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 2.0f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 10.0f; } else if (cls == CLASS_ROGUE && tab == ROGUE_TAB_COMBAT) @@ -233,6 +237,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.4f; stats_weights_[STATS_TYPE_HASTE] += 1.7f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } @@ -257,64 +262,69 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.1f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.1f; stats_weights_[STATS_TYPE_MELEE_DPS] += 5.0f; } else if (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY) { - stats_weights_[STATS_TYPE_AGILITY] += 1.8f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.6f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.8f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.8f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 2.1f; stats_weights_[STATS_TYPE_HIT] += 2.3f; stats_weights_[STATS_TYPE_CRIT] += 2.2f; - stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; + stats_weights_[STATS_TYPE_DEFENSE] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) { - stats_weights_[STATS_TYPE_AGILITY] += 1.6f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.3f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.8f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.8f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.7f; stats_weights_[STATS_TYPE_HIT] += 2.0f; stats_weights_[STATS_TYPE_CRIT] += 1.9f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; + stats_weights_[STATS_TYPE_DEFENSE] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 1.4f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_FROST) { - stats_weights_[STATS_TYPE_AGILITY] += 1.7f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.8f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 2.7f; stats_weights_[STATS_TYPE_HIT] += 2.3f; stats_weights_[STATS_TYPE_CRIT] += 2.2f; stats_weights_[STATS_TYPE_HASTE] += 2.1f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_UNHOLY) { - stats_weights_[STATS_TYPE_AGILITY] += 0.9f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.3f; stats_weights_[STATS_TYPE_HIT] += 2.2f; stats_weights_[STATS_TYPE_CRIT] += 1.7f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 1.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 5.0f; } else if (cls == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) { - stats_weights_[STATS_TYPE_AGILITY] += 1.6f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; - stats_weights_[STATS_TYPE_INTELLECT] += 0.1f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 0.3f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.5f; stats_weights_[STATS_TYPE_HIT] += 1.9f; stats_weights_[STATS_TYPE_CRIT] += 1.7f; @@ -328,7 +338,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_STRENGTH] += 1.1f; stats_weights_[STATS_TYPE_INTELLECT] += 0.3f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 0.95f; + stats_weights_[STATS_TYPE_SPELL_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 0.9f; stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.5f; @@ -347,6 +357,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.1f; stats_weights_[STATS_TYPE_CRIT] += 0.8f; stats_weights_[STATS_TYPE_HASTE] += 1.0f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if (cls == CLASS_MAGE && tab == MAGE_TAB_FIRE) @@ -357,15 +368,17 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.2f; stats_weights_[STATS_TYPE_CRIT] += 1.1f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if (cls == CLASS_SHAMAN && tab == SHAMAN_TAB_ELEMENTAL) { - stats_weights_[STATS_TYPE_INTELLECT] += 0.25f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 1.0f; + stats_weights_[STATS_TYPE_INTELLECT] += 0.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] += 1.2f; stats_weights_[STATS_TYPE_HIT] += 1.1f; stats_weights_[STATS_TYPE_CRIT] += 0.8f; stats_weights_[STATS_TYPE_HASTE] += 1.0f; + stats_weights_[STATS_TYPE_MANA_REGENERATION] += 0.5f; } else if ((cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY) || (cls == CLASS_SHAMAN && tab == SHAMAN_TAB_RESTORATION)) @@ -386,14 +399,15 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_MANA_REGENERATION] += 0.9f; stats_weights_[STATS_TYPE_CRIT] += 0.6f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if ((cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) || (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION)) { - stats_weights_[STATS_TYPE_AGILITY] += 2.0f; - stats_weights_[STATS_TYPE_STRENGTH] += 1.0f; - stats_weights_[STATS_TYPE_STAMINA] += 3.5f; + stats_weights_[STATS_TYPE_AGILITY] += 0.2f; + stats_weights_[STATS_TYPE_STRENGTH] += 1.3f; + stats_weights_[STATS_TYPE_STAMINA] += 3.0f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.2f; stats_weights_[STATS_TYPE_DEFENSE] += 2.5f; stats_weights_[STATS_TYPE_PARRY] += 2.0f; @@ -403,26 +417,26 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_BLOCK_VALUE] += 0.5f; stats_weights_[STATS_TYPE_ARMOR] += 0.15f; stats_weights_[STATS_TYPE_HIT] += 2.0f; - stats_weights_[STATS_TYPE_CRIT] += 0.2f; - stats_weights_[STATS_TYPE_HASTE] += 0.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 3.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 2.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_BLOOD) { - stats_weights_[STATS_TYPE_AGILITY] += 2.0f; - stats_weights_[STATS_TYPE_STRENGTH] += 1.0f; - stats_weights_[STATS_TYPE_STAMINA] += 3.5f; + stats_weights_[STATS_TYPE_AGILITY] += 0.2f; + stats_weights_[STATS_TYPE_STRENGTH] += 1.3f; + stats_weights_[STATS_TYPE_STAMINA] += 3.0f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.2f; - stats_weights_[STATS_TYPE_DEFENSE] += 3.5f; + stats_weights_[STATS_TYPE_DEFENSE] += 2.5f; stats_weights_[STATS_TYPE_PARRY] += 2.0f; stats_weights_[STATS_TYPE_DODGE] += 2.0f; + stats_weights_[STATS_TYPE_BLOCK_RATING] -= 2.0f; + stats_weights_[STATS_TYPE_BLOCK_VALUE] -= 2.0f; // stats_weights_[STATS_TYPE_RESILIENCE] += 2.0f; stats_weights_[STATS_TYPE_ARMOR] += 0.15f; stats_weights_[STATS_TYPE_HIT] += 2.0f; - stats_weights_[STATS_TYPE_CRIT] += 0.5f; - stats_weights_[STATS_TYPE_HASTE] += 0.5f; - stats_weights_[STATS_TYPE_EXPERTISE] += 3.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; + stats_weights_[STATS_TYPE_EXPERTISE] += 3.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 2.0f; } else @@ -483,6 +497,11 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player) !player->HasSpell(SPELL_FEL_ARMOR_RANK_3) && !player->HasSpell(SPELL_FEL_ARMOR_RANK_4)) stats_weights_[STATS_TYPE_SPIRIT] -= 0.4f; } + + if (pvpSpec_ && !exclude_resilience_) + stats_weights_[STATS_TYPE_RESILIENCE] += 7.0f; + else if (!pvpSpec_) + stats_weights_[STATS_TYPE_RESILIENCE] -= 3.0f; } void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto) @@ -573,7 +592,8 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY && !player_->CanTitanGrip() && player_->CanDualWield()) || (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) || - (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION))) + (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION) || + (cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY))) { weight_ *= 0.1; } @@ -592,11 +612,16 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) weight_ *= 0.1; } // caster's main hand (cannot duel weapon but can equip two-hands stuff) - if (cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID || - (cls == CLASS_SHAMAN && !player_->CanDualWield())) + if ((cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID || + (cls == CLASS_SHAMAN && !player_->CanDualWield())) && + !(cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY)) { weight_ *= 0.65; } + if (cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY) + { + weight_ *= 0.8; + } } // fury with titan's grip if ((!isDoubleHand || proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || @@ -767,3 +792,163 @@ void StatsWeightCalculator::ApplyWeightFinetune(Player* player) } } } + +float StatsWeightCalculator::ApplyPreferredSpecWeapons(ItemTemplate const* proto, int32 slot) +{ + // Multiply score by 3x when this weapon's delay matches the spec-ideal speed. + float weight = 2.0f; + + // Applies to mainhand, offhand, and ranged slots only. + if (slot != EQUIPMENT_SLOT_MAINHAND && + slot != EQUIPMENT_SLOT_OFFHAND && + slot != EQUIPMENT_SLOT_RANGED) + return 1.0f; + + uint32 delay = proto->Delay; // milliseconds + float boost = 1.0f + weight; // applied on a match + + // Hunter: melee weapons are stat sticks — speed irrelevant. + // Ranged weapons scale Aimed/Chimera/Explosive Shot from top-end damage, + // so a slow ranged weapon (>=2600 ms) is strongly preferred. + if (cls == CLASS_HUNTER) + { + if (slot == EQUIPMENT_SLOT_RANGED && delay >= 2600) + return boost; + return 1.0f; + } + + // Feral Druid: forms normalise attack speed; raw weapon Delay is irrelevant. + if (cls == CLASS_DRUID && tab == DRUID_TAB_FERAL) + return 1.0f; + + switch (cls) + { + case CLASS_WARRIOR: + if (tab == WARRIOR_TAB_ARMS) + { + // Arms: slow 2H axes or polearms in mainhand only (Axe Specialization: +5% crit). + bool isAxeOrPolearm = (proto->SubClass == ITEM_SUBCLASS_WEAPON_AXE2 || + proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM); + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400 && isAxeOrPolearm) + return boost; + } + else if (tab == WARRIOR_TAB_FURY) + { + if (!player_->CanDualWield()) + { + // Pre-DW: treat like Arms — slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (player_->CanTitanGrip()) + { + // Titan's Grip: slow 2H (>=3400) in both hands. + if (delay >= 3400) + return boost; + } + else + { + // 1H DW: slow 1H (>=2600) in both hands. + // 2H must be excluded — delay >= 2600 would otherwise pass + // for a 2H heirloom (~3600ms) just as it did for Enhancement. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + if (delay >= 2600) + return boost; + } + } + else if (tab == WARRIOR_TAB_PROTECTION) + { + // Prot: slow 1H (>=2600) in mainhand. Shield in offhand, no speed bonus. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + } + break; + + case CLASS_PALADIN: + if (tab == PALADIN_TAB_RETRIBUTION) + { + // Ret: slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (tab == PALADIN_TAB_PROTECTION) + { + // Prot: slow 1H (>=2600) in mainhand. Shield in offhand. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + } + break; + + case CLASS_DEATH_KNIGHT: + if (tab == DEATH_KNIGHT_TAB_BLOOD || tab == DEATH_KNIGHT_TAB_UNHOLY) + { + // Blood / Unholy: slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (tab == DEATH_KNIGHT_TAB_FROST) + { + // Frost DK has Dual Wield innately — always dual-wields 1H. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + if (delay >= 2600) + return boost; + } + break; + + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ENHANCEMENT) + { + if (!player_->CanDualWield()) + { + // Pre-Dual Wield: Enhancement plays like a 2H spec. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else + { + // Post-Dual Wield: slow 1H (>=2600) in both hands. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + + if (delay >= 2600) + { + float mult = boost; + if (slot == EQUIPMENT_SLOT_OFFHAND) + { + Item* mh = player_->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (mh && mh->GetTemplate() && mh->GetTemplate()->Delay == delay) + mult *= boost; // synchronized: ×(1+weight)² total = ×9 for 2.0f weight + } + return mult; + } + } + } + break; + + case CLASS_ROGUE: + if (tab == ROGUE_TAB_COMBAT) + { + // Combat: slow MH (>=2600), fast OH (<=1500). + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + if (slot == EQUIPMENT_SLOT_OFFHAND && delay <= 1500) + return boost; + } + else // Assassination or Subtlety: slow dagger MH, fast dagger OH. + { + bool isDagger = (proto->SubClass == ITEM_SUBCLASS_WEAPON_DAGGER); + if (slot == EQUIPMENT_SLOT_MAINHAND && isDagger && delay >= 1700) + return boost; + if (slot == EQUIPMENT_SLOT_OFFHAND && isDagger && delay <= 1500) + return boost; + } + break; + + default: + break; + } + + return 1.0f; +} diff --git a/src/Mgr/Item/StatsWeightCalculator.h b/src/Mgr/Item/StatsWeightCalculator.h index 4390e6af1ee..d97dafbb34a 100644 --- a/src/Mgr/Item/StatsWeightCalculator.h +++ b/src/Mgr/Item/StatsWeightCalculator.h @@ -28,12 +28,14 @@ class StatsWeightCalculator public: StatsWeightCalculator(Player* player); void Reset(); - float CalculateItem(uint32 itemId, int32 randomPropertyId = 0); + float CalculateItem(uint32 itemId, int32 randomPropertyId = 0, int32 slot = -1); float CalculateEnchant(uint32 enchantId); void SetOverflowPenalty(bool apply) { enable_overflow_penalty_ = apply; } void SetItemSetBonus(bool apply) { enable_item_set_bonus_ = apply; } void SetQualityBlend(bool apply) { enable_quality_blend_ = apply; } + void SetPvpSpec(bool isPvp) { pvpSpec_ = isPvp; } + void SetExcludeResilience(bool exclude) { exclude_resilience_ = exclude; } private: void GenerateWeights(Player* player); @@ -45,6 +47,7 @@ class StatsWeightCalculator void CalculateSocketBonus(Player* player, ItemTemplate const* proto); void CalculateItemTypePenalty(ItemTemplate const* proto); + float ApplyPreferredSpecWeapons(ItemTemplate const* proto, int32 slot); bool NotBestArmorType(uint32 item_subclass_armor); @@ -65,6 +68,8 @@ class StatsWeightCalculator float weight_; float stats_weights_[STATS_TYPE_MAX]; + bool pvpSpec_ = false; + bool exclude_resilience_ = false; }; #endif diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index a8daf972a9a..242febd17fb 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -125,6 +125,8 @@ bool PlayerbotAIConfig::Initialize() incrementalGearInit = sConfigMgr->GetOption("AiPlayerbot.IncrementalGearInit", true); randomGearQualityLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearQualityLimit", 3); randomGearScoreLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearScoreLimit", 0); + preferClassArmorType = sConfigMgr->GetOption("AiPlayerbot.PreferClassArmorType", false); + preferredSpecWeapons = sConfigMgr->GetOption("AiPlayerbot.PreferredSpecWeapons", false); randomBotMinLevelChance = sConfigMgr->GetOption("AiPlayerbot.RandomBotMinLevelChance", 0.1f); randomBotMaxLevelChance = sConfigMgr->GetOption("AiPlayerbot.RandomBotMaxLevelChance", 0.1f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 8776726789d..210e03ef95d 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -128,6 +128,8 @@ class PlayerbotAIConfig bool incrementalGearInit; int32 randomGearQualityLimit; int32 randomGearScoreLimit; + bool preferClassArmorType; + bool preferredSpecWeapons; float randomBotMinLevelChance, randomBotMaxLevelChance; float randomBotRpgChance; uint32 minRandomBots, maxRandomBots; From ed5791eabfb55fe0feb386c28a4444d681c322f5 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sat, 25 Apr 2026 23:40:53 +0200 Subject: [PATCH 86/87] Pull target overlap fix (#2335) ## Pull Request Description Fixed "pull target" value which was overlap with new pull strategy. Related with #2334 ## How to Test the Changes 1. Invite tank bot to party 2. Use `nc +debug` 3. Use command `do attack my target` 4. In debug shouldnt be `reach pull` or similar ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) To analyze problem with value ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Strategy/PullStrategy.cpp | 4 ++-- src/Ai/Base/Value/TargetValue.h | 9 +++++++++ src/Ai/Base/ValueContext.h | 2 ++ src/Bot/PlayerbotAI.cpp | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Ai/Base/Strategy/PullStrategy.cpp b/src/Ai/Base/Strategy/PullStrategy.cpp index 31351c57183..1237f4aa859 100644 --- a/src/Ai/Base/Strategy/PullStrategy.cpp +++ b/src/Ai/Base/Strategy/PullStrategy.cpp @@ -49,7 +49,7 @@ PullStrategy* PullStrategy::Get(PlayerbotAI* botAI) Unit* PullStrategy::GetTarget() const { - ObjectGuid const guid = botAI->GetAiObjectContext()->GetValue("pull target")->Get(); + ObjectGuid const guid = botAI->GetAiObjectContext()->GetValue("pull strategy target")->Get(); if (guid.IsEmpty()) return nullptr; @@ -66,7 +66,7 @@ bool PullStrategy::HasTarget() const { return GetTarget() != nullptr; } void PullStrategy::SetTarget(Unit* target) { - botAI->GetAiObjectContext()->GetValue("pull target")->Set(target ? target->GetGUID() : ObjectGuid::Empty); + botAI->GetAiObjectContext()->GetValue("pull strategy target")->Set(target ? target->GetGUID() : ObjectGuid::Empty); } std::string PullStrategy::GetPullActionName() const diff --git a/src/Ai/Base/Value/TargetValue.h b/src/Ai/Base/Value/TargetValue.h index 7d766578a53..94fcbdf8ae3 100644 --- a/src/Ai/Base/Value/TargetValue.h +++ b/src/Ai/Base/Value/TargetValue.h @@ -116,6 +116,15 @@ class PullTargetValue : public ManualSetValue } }; +class PullStrategyTargetValue : public ManualSetValue +{ +public: + PullStrategyTargetValue(PlayerbotAI* botAI, std::string const name = "pull strategy target") + : ManualSetValue(botAI, ObjectGuid::Empty, name) + { + } +}; + class FindTargetValue : public UnitCalculatedValue, public Qualified { public: diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index 77d25e060ff..bac5fd835c4 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -241,6 +241,7 @@ class ValueContext : public NamedObjectContext creators["travel target"] = &ValueContext::travel_target; creators["talk target"] = &ValueContext::talk_target; creators["pull target"] = &ValueContext::pull_target; + creators["pull strategy target"] = &ValueContext::pull_strategy_target; creators["focus heal targets"] = &ValueContext::focus_heal_targets; creators["group"] = &ValueContext::group; creators["range"] = &ValueContext::range; @@ -498,6 +499,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* next_rpg_action(PlayerbotAI* botAI) { return new NextRpgActionValue(botAI); } static UntypedValue* travel_target(PlayerbotAI* botAI) { return new TravelTargetValue(botAI); } static UntypedValue* pull_target(PlayerbotAI* botAI) { return new PullTargetValue(botAI); } + static UntypedValue* pull_strategy_target(PlayerbotAI* botAI) { return new PullStrategyTargetValue(botAI); } static UntypedValue* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetValue(botAI); } static UntypedValue* bg_master(PlayerbotAI* botAI) { return new BgMasterValue(botAI); } diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 98175a1dc9a..357678928fe 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -867,6 +867,7 @@ void PlayerbotAI::Reset(bool full) aiObjectContext->GetValue("current target")->Set(nullptr); aiObjectContext->GetValue("prioritized targets")->Reset(); aiObjectContext->GetValue("pull target")->Set(ObjectGuid::Empty); + aiObjectContext->GetValue("pull strategy target")->Set(ObjectGuid::Empty); aiObjectContext->GetValue("rpg target")->Set(GuidPosition()); aiObjectContext->GetValue("loot target")->Set(LootObject()); aiObjectContext->GetValue("lfg proposal")->Set(0); @@ -1476,6 +1477,7 @@ void PlayerbotAI::DoNextAction(bool min) aiObjectContext->GetValue("current target")->Set(nullptr); aiObjectContext->GetValue("enemy player target")->Set(nullptr); aiObjectContext->GetValue("pull target")->Set(ObjectGuid::Empty); + aiObjectContext->GetValue("pull strategy target")->Set(ObjectGuid::Empty); aiObjectContext->GetValue("loot target")->Set(LootObject()); ChangeEngine(BOT_STATE_DEAD); From 4bd5a9b89cbfbec21c89de34783f778d6c7e1b98 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:14:17 -0700 Subject: [PATCH 87/87] Crash Fix. Queue arena packet instead of handle directly. (#2331) ## Pull Request Description Have arenas follow the same path as battlegrounds when queueing . Intended to to resolve discord user crash. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Actions/BattleGroundJoinAction.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Ai/Base/Actions/BattleGroundJoinAction.cpp b/src/Ai/Base/Actions/BattleGroundJoinAction.cpp index fdc13120f6d..ab897a1b21a 100644 --- a/src/Ai/Base/Actions/BattleGroundJoinAction.cpp +++ b/src/Ai/Base/Actions/BattleGroundJoinAction.cpp @@ -534,21 +534,18 @@ bool BGJoinAction::JoinQueue(uint32 type) botAI->GetAiObjectContext()->GetValue("bg type")->Set(0); + WorldPacket* packet = nullptr; if (!isArena) { - WorldPacket* packet = new WorldPacket(CMSG_BATTLEMASTER_JOIN, 20); + packet = new WorldPacket(CMSG_BATTLEMASTER_JOIN, 20); *packet << bot->GetGUID() << bgTypeId_ << instanceId << joinAsGroup; - /// FIX race condition - // bot->GetSession()->HandleBattlemasterJoinOpcode(packet); - bot->GetSession()->QueuePacket(packet); } else { - WorldPacket arena_packet(CMSG_BATTLEMASTER_JOIN_ARENA, 20); - arena_packet << unit->GetGUID() << arenaslot << asGroup << uint8(isRated); - bot->GetSession()->HandleBattlemasterJoinArena(arena_packet); + packet = new WorldPacket(CMSG_BATTLEMASTER_JOIN_ARENA, 20); + *packet << unit->GetGUID() << arenaslot << asGroup << uint8(isRated); } - + bot->GetSession()->QueuePacket(packet); return true; }