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 01/22] 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 000000000..2ec7907c7 --- /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 000000000..4764efb65 --- /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 000000000..4eea58716 --- /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 000000000..95f15f16a --- /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 000000000..2f74a5a58 --- /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 000000000..df5de2318 --- /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 000000000..f975d46bb --- /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 000000000..ff82a0266 --- /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 000000000..372614d2f --- /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 000000000..1d3144194 --- /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 3311aeee2..07e5fe505 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 000000000..8c3547224 --- /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 000000000..9a680b7af --- /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 8fbb6c135..7e243eadb 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 02/22] 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 | 35 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Ai/Base/Actions/AttackAction.cpp b/src/Ai/Base/Actions/AttackAction.cpp index 96bf5c4d3..af964f360 100644 --- a/src/Ai/Base/Actions/AttackAction.cpp +++ b/src/Ai/Base/Actions/AttackAction.cpp @@ -53,22 +53,6 @@ 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 (verbose) - botAI->TellError("I cannot attack in flight"); - - return false; - } - if (!target) { if (verbose) @@ -85,6 +69,15 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) return false; } + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FLIGHT_MOTION_TYPE || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) + { + if (verbose) + botAI->TellError("I cannot attack in flight"); + + return false; + } + // Check if bot OR target is in prohibited zone/area (skip for duels) if ((target->IsPlayer() || target->IsPet()) && (!bot->duel || bot->duel->Opponent != target) && @@ -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 03/22] 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 c3b614a98..8327e2f36 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -64,7 +64,7 @@ private: }; 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 b80f6f236..316e34d47 100644 --- a/src/Bot/PlayerbotMgr.h +++ b/src/Bot/PlayerbotMgr.h @@ -57,7 +57,7 @@ protected: 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 04/22] 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 88920a050..d9f194747 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 8c8343db2..32443c46d 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 05/22] 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 | 136 ++++++++++++++++++++++++++-------------- src/Bot/PlayerbotAI.h | 6 +- 2 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 744fd7497..4c1fbb53d 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; - } - - bool isHeal = false; - bool isSingleTarget = true; - - for (uint8 i = 0; i < 3; ++i) - { - 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)) + Unit* spellTarget = currentSpell->m_targets.GetUnitTarget(); + // Interrupt if target is dead or spell can't target dead units + if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) { - isSingleTarget = false; + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; } - } - // Interrupt if target ally has full health (heal by other member) - if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) - { - InterruptSpell(); + GameObject* goSpellTarget = currentSpell->m_targets.GetGOTarget(); + + if (goSpellTarget && !goSpellTarget->isSpawned()) + { + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; + } + + bool isHeal = false; + bool isSingleTarget = true; + + for (uint8 i = 0; i < 3; ++i) + { + 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(); + 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 cfa27ed4e..5a0dc7485 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 @@ public: 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 @@ protected: 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 06/22] 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 | 70 ++++++++++++++++++++++++++++------------ 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 4c1fbb53d..f02a79e70 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 8327e2f36..fd205fe23 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)); + // 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 (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) - logout = true; + 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 (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!", {}); @@ -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 07/22] 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 d9f194747..b3a3d2ab0 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 11f301feb..9dfae8987 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 d943463ea..1962c0428 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 @@ public: 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 32443c46d..a8daf972a 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 4e758e79e..877672678 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -152,6 +152,7 @@ public: // 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 08/22] 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 b3a3d2ab0..5ce5485d0 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 09/22] 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 000000000..4e9c17583 --- /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 79b5b4985..3026cfd50 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 @@ public: 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 @@ private: 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 41911f62d..c81aca214 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 dc0785713..e9dacb7d2 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 000000000..805abd362 --- /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 000000000..299dd4a1e --- /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 6f11fb33c..af51c23ae 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 @@ public: 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 @@ private: 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 40316bd62..7742a9305 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -66,6 +66,9 @@ public: 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 @@ private: 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 77a8d9d0b..adcadb233 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 d0c7c9eac..fce917279 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 bdd7332f3..428699c56 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 @@ public: 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 a38140512..21950f00c 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 5386e872b..8dab9c40d 100644 --- a/src/Ai/Base/StrategyContext.h +++ b/src/Ai/Base/StrategyContext.h @@ -95,6 +95,7 @@ public: 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 @@ private: 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 000000000..84c550faf --- /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 000000000..d56036177 --- /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 bfdddecf7..54edbb017 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 @@ public: 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 @@ private: 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 9a8271aa7..85f8bc8cd 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 @@ public: 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 000000000..be643b50c --- /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 000000000..ce80c69f6 --- /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 3d9086cff..4d74d1db3 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 @@ public: 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 @@ public: 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 000000000..dc72b9e82 --- /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 000000000..9a52f262a --- /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 7edbf5c8f..a58ede3f8 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 @@ public: 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 @@ public: 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 000000000..ba0381b5a --- /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 000000000..43d014ec7 --- /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 000000000..cdae7e29c --- /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 000000000..c63bb21ee --- /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 22754beab..781307729 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 @@ public: 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 bb4f2eb35..3131f7514 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 8a7c34189..976252cc5 100644 --- a/src/Bot/Engine/Engine.h +++ b/src/Bot/Engine/Engine.h @@ -70,6 +70,7 @@ public: 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 a821886f9..6c638e807 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 f02a79e70..7edc48aff 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 5a0dc7485..dc7770ed8 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -405,6 +405,7 @@ public: 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 10/22] 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 fce917279..3d5e3e94a 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 11/22] 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 3d5e3e94a..31351c571 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 84c550faf..4d23b3896 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 12/22] Revert "Feat: Reintroduce timed logouts" (#2329) Reverts mod-playerbots/mod-playerbots#2289 --- src/Bot/PlayerbotAI.cpp | 47 ++++++++++++++++----------- src/Bot/PlayerbotMgr.cpp | 70 ++++++++++++---------------------------- 2 files changed, 50 insertions(+), 67 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 7edc48aff..25c93676f 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 fd205fe23..8327e2f36 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)))); + // 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 (!logout) - { - // Start the 20-second logout countdown. CancelLogout() can interrupt this. - WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); - botWorldSessionPtr->HandleLogoutRequestOpcode(data); - return; - } + 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 13/22] 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 72433c15f..75152d87b 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 4d8d6c4d8..5a4975f77 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 ed53b18a5..6cfc1e689 100644 --- a/src/Ai/Base/Actions/CastCustomSpellAction.h +++ b/src/Ai/Base/Actions/CastCustomSpellAction.h @@ -21,7 +21,7 @@ public: } 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 @@ public: 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 0caeb5e0d..b715cb2bb 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 76e6fe05f..8d15ae9be 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 b3cba9c56..61ab4254a 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 48ce74ad3..f82b05368 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 9693556d7..81fb0f4bd 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 6fd07e1e9..80f6002a3 100644 --- a/src/Ai/Base/Actions/MailAction.cpp +++ b/src/Ai/Base/Actions/MailAction.cpp @@ -78,7 +78,7 @@ private: 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 @@ public: { 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 @@ private: 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 @@ public: 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 85855f60d..18e76ca72 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 377e8360a..b1c566211 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -43,14 +43,13 @@ protected: 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 1c510d69d..625deb8a4 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 @@ public: 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 @@ public: 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 @@ public: 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 9a923d4fd..19bb870db 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 57851214a..af5be1da8 100644 --- a/src/Ai/Base/Actions/ReleaseSpiritAction.h +++ b/src/Ai/Base/Actions/ReleaseSpiritAction.h @@ -18,7 +18,7 @@ public: : 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 9580fe228..1e8af7605 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 5bf9a8f04..cae5ee444 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 f7d7eab23..d93b56b51 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 0862be68f..d6032acb1 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 adcadb233..8d5449ef3 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -11,7 +11,7 @@ public: ChatCommandActionNodeFactoryInternal() { creators["tank attack chat shortcut"] = &tank_attack_chat_shortcut; } private: - static ActionNode* tank_attack_chat_shortcut(PlayerbotAI* botAI) + static ActionNode* tank_attack_chat_shortcut(PlayerbotAI* /*botAI*/) { return new ActionNode("tank attack chat shortcut", /*P*/ {}, diff --git a/src/Ai/Base/Strategy/CombatStrategy.cpp b/src/Ai/Base/Strategy/CombatStrategy.cpp index 62972d4b1..f7e82102e 100644 --- a/src/Ai/Base/Strategy/CombatStrategy.cpp +++ b/src/Ai/Base/Strategy/CombatStrategy.cpp @@ -64,11 +64,11 @@ std::vector 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 c7f7e9d23..2a36a3cc6 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 6701fdcac..5e836cf62 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 96017365f..914cacf03 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 cb32233f4..05163208a 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 525338c15..75d07706b 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 ae45cdaaf..b5a84bbce 100644 --- a/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -11,7 +11,7 @@ public: 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 55827fbd2..27f690cf1 100644 --- a/src/Ai/Base/Strategy/UsePotionsStrategy.cpp +++ b/src/Ai/Base/Strategy/UsePotionsStrategy.cpp @@ -11,7 +11,7 @@ public: UsePotionsStrategyActionNodeFactory() { creators["healthstone"] = &healthstone; } private: - static ActionNode* healthstone(PlayerbotAI* botAI) + static ActionNode* healthstone(PlayerbotAI* /*botAI*/) { return new ActionNode("healthstone", /*P*/ {}, diff --git a/src/Ai/Base/Value/Arrow.cpp b/src/Ai/Base/Value/Arrow.cpp index c993275f5..1a4c4ff88 100644 --- a/src/Ai/Base/Value/Arrow.cpp +++ b/src/Ai/Base/Value/Arrow.cpp @@ -19,6 +19,7 @@ WorldLocation ArrowFormation::GetLocationInternal() uint32 tankLines = 1 + tanks.Size() / 6; uint32 meleeLines = 1 + melee.Size() / 6; uint32 rangedLines = 1 + ranged.Size() / 6; + //TODO Implement Healer Lines uint32 healerLines = 1 + healers.Size() / 6; float offset = 0.f; @@ -147,7 +148,7 @@ UnitPosition MultiLineUnitPlacer::Place(FormationUnit* unit, uint32 index, uint3 return placer.Place(unit, indexInLine, lineSize); } -UnitPosition SingleLineUnitPlacer::Place(FormationUnit* unit, uint32 index, uint32 count) +UnitPosition SingleLineUnitPlacer::Place(FormationUnit* /*unit*/, uint32 index, uint32 count) { float angle = orientation - M_PI / 2.0f; float x = cos(angle) * sPlayerbotAIConfig.followDistance * ((float)index - (float)count / 2); diff --git a/src/Ai/Base/Value/CcTargetValue.cpp b/src/Ai/Base/Value/CcTargetValue.cpp index a8de7a10e..4903a59f3 100644 --- a/src/Ai/Base/Value/CcTargetValue.cpp +++ b/src/Ai/Base/Value/CcTargetValue.cpp @@ -20,7 +20,7 @@ public: } public: - void CheckAttacker(Unit* creature, ThreatManager* 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 b095c09d7..39fb7edd2 100644 --- a/src/Ai/Base/Value/CurrentCcTargetValue.cpp +++ b/src/Ai/Base/Value/CurrentCcTargetValue.cpp @@ -13,7 +13,7 @@ public: { } - 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 010e47191..7099a13fe 100644 --- a/src/Ai/Base/Value/DpsTargetValue.cpp +++ b/src/Ai/Base/Value/DpsTargetValue.cpp @@ -50,7 +50,7 @@ public: 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 9ea7da2e0..46ebc1dfa 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 6f7c593b2..7e47565fc 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 b651af956..889372127 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 c185628fa..d8579f0dd 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 6a4f9b9b4..3b0b81a19 100644 --- a/src/Ai/Base/Value/LootStrategyValue.cpp +++ b/src/Ai/Base/Value/LootStrategyValue.cpp @@ -60,7 +60,7 @@ public: 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 6091c3241..99b42662c 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 53a93b2b4..b4f44fdf4 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 80def1cf9..74a4046a3 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 19578daf4..9b8fde5bd 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 0d3a43b79..28179d74e 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 b1a4685b1..f7a76b0fc 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 a67f17780..4c7c0851b 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 c4edc28fd..315b4a96f 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 9c85f7861..d5e06b54c 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 5c922af90..ce10e6b9c 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 6d6e0b655..bbae89d20 100644 --- a/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp @@ -21,7 +21,7 @@ public: } private: - static ActionNode* charge(PlayerbotAI* botAI) + static ActionNode* charge(PlayerbotAI* /*botAI*/) { return new ActionNode( "charge", @@ -31,7 +31,7 @@ private: ); } - static ActionNode* death_wish(PlayerbotAI* botAI) + static ActionNode* death_wish(PlayerbotAI* /*botAI*/) { return new ActionNode( "death wish", @@ -41,7 +41,7 @@ private: ); } - static ActionNode* piercing_howl(PlayerbotAI* botAI) + static ActionNode* piercing_howl(PlayerbotAI* /*botAI*/) { return new ActionNode( "piercing howl", @@ -51,7 +51,7 @@ private: ); } - static ActionNode* mocking_blow(PlayerbotAI* botAI) + static ActionNode* mocking_blow(PlayerbotAI* /*botAI*/) { return new ActionNode( "mocking blow", @@ -61,7 +61,7 @@ private: ); } - static ActionNode* heroic_strike(PlayerbotAI* botAI) + static ActionNode* heroic_strike(PlayerbotAI* /*botAI*/) { return new ActionNode( "heroic strike", @@ -71,7 +71,7 @@ private: ); } - static ActionNode* enraged_regeneration(PlayerbotAI* botAI) + static ActionNode* enraged_regeneration(PlayerbotAI* /*botAI*/) { return new ActionNode( "enraged regeneration", @@ -81,7 +81,7 @@ private: ); } - static ActionNode* retaliation(PlayerbotAI* botAI) + static ActionNode* retaliation(PlayerbotAI* /*botAI*/) { return new ActionNode( "retaliation", @@ -91,7 +91,7 @@ private: ); } - static ActionNode* shattering_throw(PlayerbotAI* botAI) + static ActionNode* shattering_throw(PlayerbotAI* /*botAI*/) { return new ActionNode( "shattering throw", diff --git a/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp b/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp index 67e5cb461..24e05ffe9 100644 --- a/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp @@ -18,7 +18,7 @@ public: } private: - static ActionNode* charge(PlayerbotAI* botAI) + static ActionNode* charge(PlayerbotAI* /*botAI*/) { return new ActionNode( "charge", @@ -28,7 +28,7 @@ private: ); } - static ActionNode* intercept(PlayerbotAI* botAI) + static ActionNode* intercept(PlayerbotAI* /*botAI*/) { return new ActionNode( "intercept", @@ -38,7 +38,7 @@ private: ); } - static ActionNode* piercing_howl(PlayerbotAI* botAI) + static ActionNode* piercing_howl(PlayerbotAI* /*botAI*/) { return new ActionNode( "piercing howl", @@ -48,7 +48,7 @@ private: ); } - static ActionNode* pummel(PlayerbotAI* botAI) + static ActionNode* pummel(PlayerbotAI* /*botAI*/) { return new ActionNode( "pummel", @@ -58,7 +58,7 @@ private: ); } - static ActionNode* enraged_regeneration(PlayerbotAI* botAI) + static ActionNode* enraged_regeneration(PlayerbotAI* /*botAI*/) { return new ActionNode( "enraged regeneration", diff --git a/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp b/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp index e6daa2646..f256f2093 100644 --- a/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp +++ b/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp @@ -24,7 +24,7 @@ public: } private: - static ActionNode* heroic_throw_taunt(PlayerbotAI* botAI) + static ActionNode* heroic_throw_taunt(PlayerbotAI* /*botAI*/) { return new ActionNode( "heroic throw", @@ -34,7 +34,7 @@ private: ); } - static ActionNode* heroic_throw_on_snare_target(PlayerbotAI* botAI) + static ActionNode* heroic_throw_on_snare_target(PlayerbotAI* /*botAI*/) { return new ActionNode( "heroic throw on snare target", @@ -44,7 +44,7 @@ private: ); } - static ActionNode* last_stand(PlayerbotAI* botAI) + static ActionNode* last_stand(PlayerbotAI* /*botAI*/) { return new ActionNode( "last stand", @@ -54,7 +54,7 @@ private: ); } - static ActionNode* devastate(PlayerbotAI* botAI) + static ActionNode* devastate(PlayerbotAI* /*botAI*/) { return new ActionNode( "devastate", @@ -64,7 +64,7 @@ private: ); } - static ActionNode* commanding_shout(PlayerbotAI* botAI) + static ActionNode* commanding_shout(PlayerbotAI* /*botAI*/) { return new ActionNode( "commanding shout", @@ -74,7 +74,7 @@ private: ); } - static ActionNode* sunder_armor(PlayerbotAI* botAI) + static ActionNode* sunder_armor(PlayerbotAI* /*botAI*/) { return new ActionNode( "sunder armor", @@ -84,7 +84,7 @@ private: ); } - static ActionNode* charge(PlayerbotAI* botAI) + static ActionNode* charge(PlayerbotAI* /*botAI*/) { return new ActionNode( "charge", @@ -94,7 +94,7 @@ private: ); } - static ActionNode* taunt(PlayerbotAI* botAI) + static ActionNode* taunt(PlayerbotAI* /*botAI*/) { return new ActionNode( "taunt", @@ -104,7 +104,7 @@ private: ); } - static ActionNode* vigilance(PlayerbotAI* botAI) + static ActionNode* vigilance(PlayerbotAI* /*botAI*/) { return new ActionNode( "vigilance", @@ -114,7 +114,7 @@ private: ); } - static ActionNode* enraged_regeneration(PlayerbotAI* botAI) + static ActionNode* enraged_regeneration(PlayerbotAI* /*botAI*/) { return new ActionNode( "enraged regeneration", diff --git a/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp b/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp index 27546e796..201a44199 100644 --- a/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp +++ b/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp @@ -134,7 +134,7 @@ bool IckAndKrickAction::PoisonNova(bool poisonNova, Unit* boss) return false; } -bool IckAndKrickAction::ExplosiveBarrage(bool explosiveBarrage, Unit* boss) +bool IckAndKrickAction::ExplosiveBarrage(bool /*explosiveBarrage*/, Unit* boss) { std::vector orbs; Unit* closestOrb = nullptr; diff --git a/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp b/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp index be36e480f..00de758c1 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 323970d52..0912a912c 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 6604fc525..a38f8a291 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 4bc5efe99..3caadb384 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 b29549b82..7195f0ebd 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 4615a9b7a..f9315565b 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 336bd45e2..0ae27ffab 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 9ac2a48a0..1d63a4e36 100644 --- a/src/Ai/Raid/Icecrown/Action/RaidIccActions.h +++ b/src/Ai/Raid/Icecrown/Action/RaidIccActions.h @@ -286,7 +286,7 @@ public: 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 40ee354e2..bc304ec0c 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 4e175f0f6..69ef73987 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 513920e11..82ecbdb7b 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 885af774d..055a93a58 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 ffe446965..024ffe4d6 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 d47d06459..7abc493c2 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h @@ -81,7 +81,7 @@ public: 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 344dda5ba..e51ad24e5 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 b1217f59e..bb61bc004 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 0b31a1c13..68dbc8b62 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 08ad4c48a..9443354c4 100644 --- a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h @@ -312,8 +312,8 @@ public: 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 @@ public: 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 c841c7dcb..f43ef4667 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 edcc7ad58..c31a5e7ad 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 8b021304e..41b087e3c 100644 --- a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h @@ -66,7 +66,7 @@ public: 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 798b77904..217ab2c06 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 e7d16920f..b727681a4 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 ad338a5a7..22b072702 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 77c268817..eeff879a4 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 4c27f0238..5da447a7e 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 3131f7514..90a90fdb9 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 9dfae8987..e7021e372 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 25c93676f..98175a1dc 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 8327e2f36..68eb2fd1e 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 ee06c58f8..9e9150989 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 731534edd..115a4e95f 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 930aa1f4a..dd581ddba 100644 --- a/src/Mgr/Item/ItemVisitors.h +++ b/src/Mgr/Item/ItemVisitors.h @@ -325,9 +325,6 @@ public: 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 d2a279727..e08f2c385 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 361e4a5f7..b3b593606 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 7ee760ed4..2cb6ce5c4 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 7db91eb40..5be7e8855 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -226,7 +226,7 @@ public: 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 @@ public: 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 1ba1eb5ea..bc17e5fe3 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 14/22] 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 b3b593606..faa06ff0f 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 15/22] 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 e14549da4..2cefe9062 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 9fa85b3a1..6841335d5 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 16/22] 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 af964f360..a537dd218 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 17/22] 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 0051a5a49..2599fb1cf 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 18/22] 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 1962c0428..0e18e6b83 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -88,6 +88,7 @@ public: void InitKeyring(); void InitReputation(); void InitAttunementQuests(); + void InitGuild(); private: enum class ProfessionSpecializationSpell : uint32 @@ -199,7 +200,6 @@ private: 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 9e9150989..110a1942e 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 001a438cb..5dc1095de 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 19/22] 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 61ab4254a..f6712abf3 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 855555edd..428d09a00 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 20/22] 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 5ce5485d0..3d0866f44 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 889372127..c3d976f0f 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 e7021e372..f8e0e5dfe 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 0e18e6b83..ba32e6a11 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -63,7 +63,7 @@ public: 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 @@ private: 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 @@ private: 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 110a1942e..5c0922fb9 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 db74f2cbe..b68c77e41 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -140,6 +140,7 @@ public: 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 faa06ff0f..b232b1e6b 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 4390e6af1..d97dafbb3 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 @@ public: 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 @@ private: 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 a8daf972a..242febd17 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 877672678..210e03ef9 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -128,6 +128,8 @@ public: 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 21/22] 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 31351c571..1237f4aa8 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 7d766578a..94fcbdf8a 100644 --- a/src/Ai/Base/Value/TargetValue.h +++ b/src/Ai/Base/Value/TargetValue.h @@ -116,6 +116,15 @@ public: } }; +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 77d25e060..bac5fd835 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -241,6 +241,7 @@ public: 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 @@ private: 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 98175a1dc..357678928 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 22/22] 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 fdc13120f..ab897a1b2 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; }