From 28a888b6e0d1be61f9b6d38a83f1a1dca9b3a41e Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 6 Mar 2026 16:56:53 +0100 Subject: [PATCH 01/13] Added unobtainable items to config (#2133) # Pull Request Moving hardcoded values to config ## How to Test the Changes - use maintenance command - unequip and destroy item get from this command - turn off server - add item to config - turn on server - use maintenace command - check that different item was provided ## Complexity & Impact Does this change add new decision branches? - - [X] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [X] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [X] No - - [ ] Yes (**explain why**) --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [X] No - - [ ] Yes (**explain below**) --- ## Final Checklist - - [X] Stability is not compromised - - [X] Performance impact is understood, tested, and acceptable - - [X] Added logic complexity is justified and explained - - [X] Documentation updated if needed --- ## Notes for Reviewers --- conf/playerbots.conf.dist | 6 +++++- src/Mgr/Item/RandomItemMgr.cpp | 5 +---- src/PlayerbotAIConfig.cpp | 5 +++++ src/PlayerbotAIConfig.h | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 5acf71ea..ede73583 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -796,6 +796,10 @@ AiPlayerbot.LimitGearExpansion = 1 # Set between 0 (0%) and 1 (100%) AiPlayerbot.RandomGearLoweringChance = 0 +# Unobtainable or unusable items (comma-separated list of item IDs) +# Default: Chilton Wand (12468), Totem of the Earthen Ring (46978) +AiPlayerbot.UnobtainableItems = 12468,46978 + # Randombots check player's gearscore level and deny the group invitation if it's too low # Default: 0 (disabled) AiPlayerbot.GearScoreCheck = 0 @@ -2213,4 +2217,4 @@ AiPlayerbot.SummonAtInnkeepersEnabled = 1 # 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for non tank bots, increased threat for tank bots. # Buffs will be applied on PP, Sindragosa and Lich King -AiPlayerbot.EnableICCBuffs = 1 \ No newline at end of file +AiPlayerbot.EnableICCBuffs = 1 diff --git a/src/Mgr/Item/RandomItemMgr.cpp b/src/Mgr/Item/RandomItemMgr.cpp index d2e7c866..d2a27972 100644 --- a/src/Mgr/Item/RandomItemMgr.cpp +++ b/src/Mgr/Item/RandomItemMgr.cpp @@ -2255,10 +2255,7 @@ void RandomItemMgr::BuildEquipCacheNew() continue; } - // Unobtainable or unusable items - if (itemId == 12468 || // Chilton Wand - itemId == 22784 || // Sunwell Orb - itemId == 46978) // Totem of the Earthen Ring + if (sPlayerbotAIConfig.unobtainableItems.find(itemId) != sPlayerbotAIConfig.unobtainableItems.end()) continue; equipCacheNew[proto->RequiredLevel][proto->InventoryType].push_back(itemId); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 211db9a0..356098d3 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -182,6 +182,11 @@ bool PlayerbotAIConfig::Initialize() LoadSet>( sConfigMgr->GetOption("AiPlayerbot.AttunementQuests", "10279,10277,10282,10283,10284,10285,10296,10297,10298,11481,11482,11488,11490,11492,10901"), attunementQuests); + + LoadSet>( + sConfigMgr->GetOption("AiPlayerbot.UnobtainableItems", "12468,46978"), + unobtainableItems); + botAutologin = sConfigMgr->GetOption("AiPlayerbot.BotAutologin", false); randomBotAutologin = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutologin", true); minRandomBots = sConfigMgr->GetOption("AiPlayerbot.MinRandomBots", 500); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index a5c6e461..efde98e1 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -99,6 +99,7 @@ public: bool tellWhenAvoidAoe; std::set disallowedGameObjects; std::set attunementQuests; + std::set unobtainableItems; uint32 openGoSpell; bool randomBotAutologin; From 18bd6558694ce626038a4d34c5aad9c42e9678f5 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:57:21 +0100 Subject: [PATCH 02/13] Restore Naxx Strategies without core dependencies (#2031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This PR restores the Naxxramas raid strategies that were removed in commit 686fe513b25bbb20ccfcc89f08ee8c4b498a263e . The reintroduced logic is core‑friendly (no AzerothCore script headers or internal boss AI/EventMap dependencies), and the Naxxramas actions have been refactored into per‑boss files for better maintainability. ### Motivation The previous removal was meant to avoid core modifications and unblock upstreaming. This PR brings the strategies back while adhering to that requirement, using only observable state and mod‑playerbots helpers. ### What’s included - Re‑enabled the Naxxramas strategies previously removed. - Replaced core script header dependencies with observable checks (auras, casts, unit flags, flight state, etc.). - Split the Naxxramas action logic into per‑boss source files to avoid a “god file” and ease future maintenance. - Minor, non‑intrusive behavior improvements aligned with existing helpers. ### Future work Some strategies may still require refinement or more advanced handling later. This PR focuses on restoring the baseline logic without core dependencies, while keeping changes minimal and safe. **Any contributions are welcome to further improve and fine‑tune the Naxxramas strategies.** ### Testing Tested in some Naxx boxx. No server crash and boss killed :D Note: I'll make another PR with revised scripts when this one are merged --------- 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/ChatShortcutActions.cpp | 14 + src/Ai/Base/Actions/ChatShortcutActions.h | 7 + src/Ai/Base/ChatActionContext.h | 2 + src/Ai/Base/ChatTriggerContext.h | 2 - .../Strategy/ChatCommandHandlerStrategy.cpp | 2 + .../Raid/Naxxramas/Action/RaidNaxxActions.h | 326 +++++++++++ .../Action/RaidNaxxActions_Anubrekhan.cpp | 81 +++ .../Action/RaidNaxxActions_Faerlina.cpp | 3 + .../Action/RaidNaxxActions_FourHorseman.cpp | 59 ++ .../Action/RaidNaxxActions_Gluth.cpp | 178 ++++++ .../Action/RaidNaxxActions_Gothik.cpp | 3 + .../Action/RaidNaxxActions_Grobbulus.cpp | 46 ++ .../Action/RaidNaxxActions_Heigan.cpp | 77 +++ .../Action/RaidNaxxActions_Kelthuzad.cpp | 177 ++++++ .../Action/RaidNaxxActions_Loatheb.cpp | 55 ++ .../Action/RaidNaxxActions_Maexxna.cpp | 3 + .../Naxxramas/Action/RaidNaxxActions_Noth.cpp | 3 + .../Action/RaidNaxxActions_Patchwerk.cpp | 31 + .../Action/RaidNaxxActions_Razuvious.cpp | 147 +++++ .../Action/RaidNaxxActions_Sapphiron.cpp | 104 ++++ .../Action/RaidNaxxActions_Shared.cpp | 18 + .../Action/RaidNaxxActions_Thaddius.cpp | 134 +++++ .../Multiplier/RaidNaxxMultipliers.cpp | 322 +++++++++++ .../Multiplier/RaidNaxxMultipliers.h | 115 ++++ src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h | 95 ++++ .../Raid/Naxxramas/RaidNaxxTriggerContext.h | 86 +++ .../Naxxramas/Strategy/RaidNaxxStrategy.cpp | 156 +++++ .../Naxxramas/Strategy/RaidNaxxStrategy.h | 18 + .../Naxxramas/Trigger/RaidNaxxTriggers.cpp | 257 +++++++++ .../Raid/Naxxramas/Trigger/RaidNaxxTriggers.h | 259 +++++++++ .../Raid/Naxxramas/Util/RaidNaxxBossHelper.h | 533 ++++++++++++++++++ src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h | 165 ++++++ src/Ai/Raid/RaidStrategyContext.h | 3 + src/Bot/Engine/AiObjectContext.cpp | 4 + src/Bot/PlayerbotAI.cpp | 3 + 35 files changed, 3486 insertions(+), 2 deletions(-) create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Faerlina.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gluth.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gothik.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Grobbulus.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Heigan.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Kelthuzad.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Loatheb.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Maexxna.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Noth.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Patchwerk.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Razuvious.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Shared.cpp create mode 100644 src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp create mode 100644 src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp create mode 100644 src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h create mode 100644 src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h create mode 100644 src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h create mode 100644 src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp create mode 100644 src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h create mode 100644 src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp create mode 100644 src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h create mode 100644 src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h create mode 100644 src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h diff --git a/src/Ai/Base/Actions/ChatShortcutActions.cpp b/src/Ai/Base/Actions/ChatShortcutActions.cpp index 84ed3584..0caeb5e0 100644 --- a/src/Ai/Base/Actions/ChatShortcutActions.cpp +++ b/src/Ai/Base/Actions/ChatShortcutActions.cpp @@ -237,6 +237,20 @@ bool MaxDpsChatShortcutAction::Execute(Event /*event*/) return true; } +bool NaxxChatShortcutAction::Execute(Event /*event*/) +{ + Player* master = GetMaster(); + if (!master) + return false; + + botAI->Reset(); + botAI->ChangeStrategy("+naxx", BOT_STATE_NON_COMBAT); + botAI->ChangeStrategy("+naxx", BOT_STATE_COMBAT); + botAI->TellMasterNoFacing("Add Naxx Strategies!"); + // bot->Say("Add Naxx Strategies!", LANG_UNIVERSAL); + return true; +} + bool BwlChatShortcutAction::Execute(Event /*event*/) { Player* master = GetMaster(); diff --git a/src/Ai/Base/Actions/ChatShortcutActions.h b/src/Ai/Base/Actions/ChatShortcutActions.h index b7e4a9e9..fa941a4c 100644 --- a/src/Ai/Base/Actions/ChatShortcutActions.h +++ b/src/Ai/Base/Actions/ChatShortcutActions.h @@ -85,6 +85,13 @@ public: bool Execute(Event event) override; }; +class NaxxChatShortcutAction : public Action +{ +public: + NaxxChatShortcutAction(PlayerbotAI* ai) : Action(ai, "naxx chat shortcut") {} + virtual bool Execute(Event event); +}; + class BwlChatShortcutAction : public Action { public: diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 4bfdcb04..edd111bb 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -187,6 +187,7 @@ public: creators["guild leave"] = &ChatActionContext::guild_leave; creators["rtsc"] = &ChatActionContext::rtsc; creators["bwl chat shortcut"] = &ChatActionContext::bwl_chat_shortcut; + creators["naxx chat shortcut"] = &ChatActionContext::naxx_chat_shortcut; creators["tell estimated dps"] = &ChatActionContext::tell_estimated_dps; creators["join"] = &ChatActionContext::join; creators["lfg"] = &ChatActionContext::lfg; @@ -298,6 +299,7 @@ private: static Action* guild_remove(PlayerbotAI* botAI) { return new GuildRemoveAction(botAI); } static Action* guild_leave(PlayerbotAI* botAI) { return new GuildLeaveAction(botAI); } static Action* rtsc(PlayerbotAI* botAI) { return new RTSCAction(botAI); } + static Action* naxx_chat_shortcut(PlayerbotAI* ai) { return new NaxxChatShortcutAction(ai); } static Action* bwl_chat_shortcut(PlayerbotAI* ai) { return new BwlChatShortcutAction(ai); } static Action* tell_estimated_dps(PlayerbotAI* ai) { return new TellEstimatedDpsAction(ai); } static Action* join(PlayerbotAI* ai) { return new JoinGroupAction(ai); } diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 3e71839b..54ae236b 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -127,7 +127,6 @@ public: creators["guild leave"] = &ChatTriggerContext::guild_leave; creators["rtsc"] = &ChatTriggerContext::rtsc; creators["drink"] = &ChatTriggerContext::drink; - // creators["bwl"] = &ChatTriggerContext::bwl; creators["dps"] = &ChatTriggerContext::dps; creators["disperse"] = &ChatTriggerContext::disperse; creators["calc"] = &ChatTriggerContext::calc; @@ -245,7 +244,6 @@ private: static Trigger* guild_leave(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "guild leave"); } static Trigger* rtsc(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "rtsc"); } static Trigger* drink(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "drink"); } - // static Trigger* bwl(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "bwl"); } static Trigger* dps(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "dps"); } static Trigger* disperse(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "disperse"); } static Trigger* calc(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "calc"); } diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index f67ae49c..7bc1d539 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -83,6 +83,8 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger new TriggerNode("target", { NextAction("tell target", relevance) })); triggers.push_back( new TriggerNode("ready", { NextAction("ready check", relevance) })); + triggers.push_back( + new TriggerNode("naxx", {NextAction("naxx chat shortcut", relevance)})); triggers.push_back( new TriggerNode("bwl", { NextAction("bwl chat shortcut", relevance) })); triggers.push_back( diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h new file mode 100644 index 00000000..d5bfe0bc --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions.h @@ -0,0 +1,326 @@ +#ifndef _PLAYERBOT_RAIDNAXXACTIONS_H +#define _PLAYERBOT_RAIDNAXXACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "GenericActions.h" +#include "MovementActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "RaidNaxxBossHelper.h" + +class GrobbulusGoBehindAction : public MovementAction +{ +public: + GrobbulusGoBehindAction(PlayerbotAI* ai, float distance = 24.0f, float delta_angle = M_PI / 8) + : MovementAction(ai, "grobbulus go behind") + { + this->distance = distance; + this->delta_angle = delta_angle; + } + virtual bool Execute(Event event); + +protected: + float distance, delta_angle; +}; + +class GrobbulusRotateAction : public RotateAroundTheCenterPointAction +{ +public: + GrobbulusRotateAction(PlayerbotAI* botAI) + : RotateAroundTheCenterPointAction(botAI, "rotate grobbulus", 3281.23f, -3310.38f, 35.0f, 8, true, M_PI) {} + virtual bool isUseful() override + { + return RotateAroundTheCenterPointAction::isUseful() && botAI->IsMainTank(bot) && + AI_VALUE2(bool, "has aggro", "boss target"); + } + uint32 GetCurrWaypoint() override; +}; + +class GrobblulusMoveCenterAction : public MoveInsideAction +{ +public: + GrobblulusMoveCenterAction(PlayerbotAI* ai) : MoveInsideAction(ai, 3281.23f, -3310.38f, 5.0f) {} +}; + +class GrobbulusMoveAwayAction : public MovementAction +{ +public: + GrobbulusMoveAwayAction(PlayerbotAI* ai, float distance = 18.0f) + : MovementAction(ai, "grobbulus move away"), distance(distance) + { + } + bool Execute(Event event) override; + +private: + float distance; +}; + +//class HeiganDanceAction : public MovementAction +//{ +//public: +// HeiganDanceAction(PlayerbotAI* ai) : MovementAction(ai, "heigan dance") +// { +// this->last_eruption_ms = 0; +// this->platform_phase = false; +// ResetSafe(); +// waypoints.push_back(std::make_pair(2794.88f, -3668.12f)); +// waypoints.push_back(std::make_pair(2775.49f, -3674.43f)); +// waypoints.push_back(std::make_pair(2762.30f, -3684.59f)); +// waypoints.push_back(std::make_pair(2755.99f, -3703.96f)); +// platform = std::make_pair(2794.26f, -3706.67f); +// } +// +//protected: +// bool CalculateSafe(); +// void ResetSafe() +// { +// curr_safe = 0; +// curr_dir = 1; +// } +// void NextSafe() +// { +// curr_safe += curr_dir; +// if (curr_safe == 3 || curr_safe == 0) +// { +// curr_dir = -curr_dir; +// } +// } +// uint32 last_eruption_ms; +// bool platform_phase; +// uint32 curr_safe, curr_dir; +// std::vector> waypoints; +// std::pair platform; +//}; +// +//class HeiganDanceMeleeAction : public HeiganDanceAction +//{ +//public: +// HeiganDanceMeleeAction(PlayerbotAI* ai) : HeiganDanceAction(ai) {} +// virtual bool Execute(Event event); +//}; +// +//class HeiganDanceRangedAction : public HeiganDanceAction +//{ +//public: +// HeiganDanceRangedAction(PlayerbotAI* ai) : HeiganDanceAction(ai) {} +// virtual bool Execute(Event event); +//}; + +class ThaddiusAttackNearestPetAction : public AttackAction +{ +public: + ThaddiusAttackNearestPetAction(PlayerbotAI* ai) : AttackAction(ai, "thaddius attack nearest pet"), helper(ai) {} + virtual bool Execute(Event event); + virtual bool isUseful(); + +private: + ThaddiusBossHelper helper; +}; + +// class ThaddiusMeleeToPlaceAction : public MovementAction +// { +// public: +// ThaddiusMeleeToPlaceAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius melee to place") {} +// virtual bool Execute(Event event); +// virtual bool isUseful(); +// }; + +// class ThaddiusRangedToPlaceAction : public MovementAction +// { +// public: +// ThaddiusRangedToPlaceAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius ranged to place") {} +// virtual bool Execute(Event event); +// virtual bool isUseful(); +// }; + +class ThaddiusMoveToPlatformAction : public MovementAction +{ +public: + ThaddiusMoveToPlatformAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius move to platform") {} + virtual bool Execute(Event event); + virtual bool isUseful(); +}; + +class ThaddiusMovePolarityAction : public MovementAction +{ +public: + ThaddiusMovePolarityAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius move polarity") {} + virtual bool Execute(Event event); + virtual bool isUseful(); +}; + +class RazuviousUseObedienceCrystalAction : public MovementAction +{ +public: + RazuviousUseObedienceCrystalAction(PlayerbotAI* ai) + : MovementAction(ai, "razuvious use obedience crystal"), helper(ai) + { + } + bool Execute(Event event) override; + +private: + RazuviousBossHelper helper; +}; + +class RazuviousTargetAction : public AttackAction +{ +public: + RazuviousTargetAction(PlayerbotAI* ai) : AttackAction(ai, "razuvious target"), helper(ai) {} + bool Execute(Event event) override; + +private: + RazuviousBossHelper helper; +}; + +class HorsemanAttractAlternativelyAction : public AttackAction +{ +public: + HorsemanAttractAlternativelyAction(PlayerbotAI* ai) : AttackAction(ai, "horseman attract alternatively"), helper(ai) + { + } + bool Execute(Event event) override; + +protected: + FourhorsemanBossHelper helper; +}; + +class HorsemanAttactInOrderAction : public AttackAction +{ +public: + HorsemanAttactInOrderAction(PlayerbotAI* ai) : AttackAction(ai, "horseman attact in order"), helper(ai) {} + bool Execute(Event event) override; + +protected: + FourhorsemanBossHelper helper; +}; + +// class SapphironGroundMainTankPositionAction : public MovementAction +// { +// public: +// SapphironGroundMainTankPositionAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron ground main tank +// position") {} virtual bool Execute(Event event); +// }; + +class SapphironGroundPositionAction : public MovementAction +{ +public: + SapphironGroundPositionAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron ground position"), helper(ai) {} + bool Execute(Event event) override; + +protected: + SapphironBossHelper helper; +}; + +class SapphironFlightPositionAction : public MovementAction +{ +public: + SapphironFlightPositionAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron flight position"), helper(ai) {} + bool Execute(Event event) override; + +protected: + SapphironBossHelper helper; + bool MoveToNearestIcebolt(); +}; + +// class SapphironAvoidChillAction : public MovementAction +// { +// public: +// SapphironAvoidChillAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron avoid chill") {} +// virtual bool Execute(Event event); +// }; + +class KelthuzadChooseTargetAction : public AttackAction +{ +public: + KelthuzadChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "kel'thuzad choose target"), helper(ai) {} + virtual bool Execute(Event event); + +private: + KelthuzadBossHelper helper; +}; + +class KelthuzadPositionAction : public MovementAction +{ +public: + KelthuzadPositionAction(PlayerbotAI* ai) : MovementAction(ai, "kel'thuzad position"), helper(ai) {} + virtual bool Execute(Event event); + +private: + KelthuzadBossHelper helper; +}; + +class AnubrekhanChooseTargetAction : public AttackAction +{ +public: + AnubrekhanChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "anub'rekhan choose target") {} + bool Execute(Event event) override; +}; + +class AnubrekhanPositionAction : public RotateAroundTheCenterPointAction +{ +public: + AnubrekhanPositionAction(PlayerbotAI* ai) + : RotateAroundTheCenterPointAction(ai, "anub'rekhan position", 3272.49f, -3476.27f, 45.0f, 16) {} + bool Execute(Event event) override; +}; + +class GluthChooseTargetAction : public AttackAction +{ +public: + GluthChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "gluth choose target"), helper(ai) {} + bool Execute(Event event) override; + +private: + GluthBossHelper helper; +}; + +class GluthPositionAction : public RotateAroundTheCenterPointAction +{ +public: + GluthPositionAction(PlayerbotAI* ai) + : RotateAroundTheCenterPointAction(ai, "gluth position", 3293.61f, -3149.01f, 12.0f, 12), helper(ai) {} + bool Execute(Event event) override; + +private: + GluthBossHelper helper; +}; + +class GluthSlowdownAction : public Action +{ +public: + GluthSlowdownAction(PlayerbotAI* ai) : Action(ai, "gluth slowdown"), helper(ai) {} + bool Execute(Event event) override; + +private: + GluthBossHelper helper; +}; + +class LoathebPositionAction : public MovementAction +{ +public: + LoathebPositionAction(PlayerbotAI* ai) : MovementAction(ai, "loatheb position"), helper(ai) {} + virtual bool Execute(Event event); + +private: + LoathebBossHelper helper; +}; + +class LoathebChooseTargetAction : public AttackAction +{ +public: + LoathebChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "loatheb choose target"), helper(ai) {} + virtual bool Execute(Event event); + +private: + LoathebBossHelper helper; +}; + +//class PatchwerkRangedPositionAction : public MovementAction +//{ +//public: +// PatchwerkRangedPositionAction(PlayerbotAI* ai) : MovementAction(ai, "patchwerk ranged position") {} +// bool Execute(Event event) override; +//}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp new file mode 100644 index 00000000..7ff77ead --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Anubrekhan.cpp @@ -0,0 +1,81 @@ +#include "RaidNaxxActions.h" + +#include "ObjectGuid.h" +#include "Playerbots.h" + +bool AnubrekhanChooseTargetAction::Execute(Event /*event*/) +{ + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + Unit* target_boss = nullptr; + std::vector target_guards; + for (ObjectGuid const guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit) + continue; + if (botAI->EqualLowercaseName(unit->GetName(), "crypt guard")) + target_guards.push_back(unit); + + if (botAI->EqualLowercaseName(unit->GetName(), "anub'rekhan")) + target_boss = unit; + } + if (botAI->IsMainTank(bot)) + target = target_boss; + else + { + if (target_guards.size() == 0) + target = target_boss; + else + { + if (botAI->IsAssistTank(bot)) + { + for (Unit* t : target_guards) + { + if (target == nullptr || (target->GetVictim() && target->GetVictim()->ToPlayer() && + botAI->IsTank(target->GetVictim()->ToPlayer()))) + target = t; + } + } + else + { + for (Unit* t : target_guards) + { + if (target == nullptr || target->GetHealthPct() > t->GetHealthPct()) + target = t; + } + } + } + } + if (context->GetValue("current target")->Get() == target) + return false; + + return Attack(target); +} + +bool AnubrekhanPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'rekhan"); + if (!boss) + return false; + + bool inPhase = botAI->HasAura("locust swarm", boss) || boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (inPhase) + { + if (botAI->IsMainTank(bot)) + { + uint32 nearest = FindNearestWaypoint(); + uint32 next_point; + if (inPhase) + next_point = (nearest + 1) % intervals; + else + next_point = nearest; + + return MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT); + } + else + return MoveInside(533, 3272.49f, -3476.27f, bot->GetPositionZ(), 3.0f, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Faerlina.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Faerlina.cpp new file mode 100644 index 00000000..0c52be78 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Faerlina.cpp @@ -0,0 +1,3 @@ +#include "RaidNaxxActions.h" + +// Reserved for Faerlina-specific actions. diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp new file mode 100644 index 00000000..56736388 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_FourHorseman.cpp @@ -0,0 +1,59 @@ +#include "RaidNaxxActions.h" + +#include "Playerbots.h" + +bool HorsemanAttractAlternativelyAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + helper.CalculatePosToGo(bot); + auto [posX, posY] = helper.CurrentAttractPos(); + if (MoveTo(bot->GetMapId(), posX, posY, helper.posZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + Unit* attackTarget = helper.CurrentAttackTarget(); + if (context->GetValue("current target")->Get() != attackTarget) + return Attack(attackTarget); + + return false; +} + +bool HorsemanAttactInOrderAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + Unit* target = nullptr; + Unit* thane = AI_VALUE2(Unit*, "find target", "thane korth'azz"); + Unit* lady = AI_VALUE2(Unit*, "find target", "lady blaumeux"); + Unit* sir = AI_VALUE2(Unit*, "find target", "sir zeliek"); + Unit* fourth = AI_VALUE2(Unit*, "find target", "baron rivendare"); + if (!fourth) + fourth = AI_VALUE2(Unit*, "find target", "highlord mograine"); + + std::vector attack_order; + if (botAI->IsAssistTank(bot)) + attack_order = {fourth, thane, lady, sir}; + else + attack_order = {thane, fourth, lady, sir}; + for (Unit* t : attack_order) + { + if (t && t->IsAlive()) + { + target = t; + break; + } + } + if (target) + { + if (context->GetValue("current target")->Get() == target && botAI->GetState() == BOT_STATE_COMBAT) + return false; + + if (!bot->IsWithinLOSInMap(target)) + return MoveNear(target, 22.0f, MovementPriority::MOVEMENT_COMBAT); + + return Attack(target); + } + return false; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gluth.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gluth.cpp new file mode 100644 index 00000000..f95f17ff --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gluth.cpp @@ -0,0 +1,178 @@ +#include "RaidNaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +bool GluthChooseTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + GuidVector attackers = context->GetValue("possible targets")->Get(); + Unit* target = nullptr; + Unit* target_boss = nullptr; + std::vector target_zombies; + for (GuidVector::iterator i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + + if (!unit->IsAlive()) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "zombie chow")) + target_zombies.push_back(unit); + + if (botAI->EqualLowercaseName(unit->GetName(), "gluth")) + target_boss = unit; + } + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0)) + target = target_boss; + else if (botAI->IsAssistTankOfIndex(bot, 1)) + { + for (Unit* t : target_zombies) + { + if (t->GetHealthPct() > helper.decimatedZombiePct && t->GetVictim() != bot && t->GetDistance2d(bot) <= 10.0f) + { + if (!target || t->GetDistance2d(bot) < target->GetDistance2d(bot)) + target = t; + } + } + } + else if (botAI->GetClassIndex(bot, CLASS_HUNTER) == 0 || botAI->GetClassIndex(bot, CLASS_HUNTER) == 1) + { + // prevent zombie go straight to gluth + for (Unit* t : target_zombies) + { + if (t->GetHealthPct() > helper.decimatedZombiePct && t->GetVictim() == target_boss && + t->GetDistance2d(bot) <= sPlayerbotAIConfig.spellDistance) + { + if (!target || t->GetDistance2d(bot) < target->GetDistance2d(bot)) + target = t; + } + } + if (!target) + target = target_boss; + } + else + { + for (Unit* t : target_zombies) + { + if (t->GetHealthPct() <= helper.decimatedZombiePct) + { + if (target == nullptr || + target->GetDistance2d(helper.mainTankPos25.first, helper.mainTankPos25.second) > + t->GetDistance2d(helper.mainTankPos25.first, helper.mainTankPos25.second)) + target = t; + } + } + if (target == nullptr) + target = target_boss; + } + if (!target || context->GetValue("current target")->Get() == target) + return false; + + if (target_boss && target == target_boss) + return Attack(target, true); + + return Attack(target, false); + // return Attack(target); +} + +bool GluthPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0)) + { + if (AI_VALUE2(bool, "has aggro", "boss target")) + { + if (raid25) + { + if (MoveTo(NAXX_MAP_ID, helper.mainTankPos25.first, helper.mainTankPos25.second, bot->GetPositionZ(), false, false, false, + false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(NAXX_MAP_ID, helper.mainTankPos25.first, helper.mainTankPos25.second, bot->GetPositionZ(), 2.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else + { + if (MoveTo(NAXX_MAP_ID, helper.mainTankPos10.first, helper.mainTankPos10.second, bot->GetPositionZ(), false, false, false, + false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(NAXX_MAP_ID, helper.mainTankPos10.first, helper.mainTankPos10.second, bot->GetPositionZ(), 2.0f, + MovementPriority::MOVEMENT_COMBAT); + } + } + } + else if (botAI->IsAssistTankOfIndex(bot, 1)) + { + if (helper.BeforeDecimate()) + { + if (MoveTo(bot->GetMapId(), helper.beforeDecimatePos.first, helper.beforeDecimatePos.second, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(bot->GetMapId(), helper.beforeDecimatePos.first, helper.beforeDecimatePos.second, bot->GetPositionZ(), 2.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else + { + if (AI_VALUE2(bool, "has aggro", "current target")) + { + uint32 nearest = FindNearestWaypoint(); + uint32 next_point = (nearest + 1) % intervals; + return MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + } + } + else if (botAI->IsRangedDps(bot)) + { + if (raid25) + { + if (botAI->GetClassIndex(bot, CLASS_HUNTER) == 0) + return MoveInside(NAXX_MAP_ID, helper.leftSlowDownPos.first, helper.leftSlowDownPos.second, bot->GetPositionZ(), 0.0f, + MovementPriority::MOVEMENT_COMBAT); + + if (botAI->GetClassIndex(bot, CLASS_HUNTER) == 1) + return MoveInside(NAXX_MAP_ID, helper.rightSlowDownPos.first, helper.rightSlowDownPos.second, bot->GetPositionZ(), 0.0f, + MovementPriority::MOVEMENT_COMBAT); + } + return MoveInside(NAXX_MAP_ID, helper.rangedPos.first, helper.rangedPos.second, bot->GetPositionZ(), 3.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsHeal(bot)) + return MoveInside(NAXX_MAP_ID, helper.healPos.first, helper.healPos.second, bot->GetPositionZ(), 0.0f, + MovementPriority::MOVEMENT_COMBAT); + return false; +} + +bool GluthSlowdownAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + if (!raid25) + return false; + + if (helper.JustStartCombat()) + return false; + + switch (bot->getClass()) + { + case CLASS_HUNTER: + return botAI->CastSpell("frost trap", bot); + break; + default: + break; + } + return false; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gothik.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gothik.cpp new file mode 100644 index 00000000..a33fef30 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Gothik.cpp @@ -0,0 +1,3 @@ +#include "RaidNaxxActions.h" + +// Reserved for Gothik-specific actions. diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Grobbulus.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Grobbulus.cpp new file mode 100644 index 00000000..1917c3e1 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Grobbulus.cpp @@ -0,0 +1,46 @@ +#include "RaidNaxxActions.h" + +#include "Playerbots.h" + +bool GrobbulusGoBehindAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE(Unit*, "boss target"); + if (!boss) + return false; + + // Position* pos = boss->GetPosition(); + float orientation = boss->GetOrientation() + M_PI + delta_angle; + float x = boss->GetPositionX(); + float y = boss->GetPositionY(); + float z = boss->GetPositionZ(); + float rx = x + cos(orientation) * distance; + float ry = y + sin(orientation) * distance; + return MoveTo(bot->GetMapId(), rx, ry, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +bool GrobbulusMoveAwayAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE(Unit*, "boss target"); + if (!boss) + return false; + + const float currentDistance = bot->GetExactDist2d(boss); + if (currentDistance >= distance) + return false; + + const float angle = boss->GetAngle(bot); + const float x = boss->GetPositionX() + cos(angle) * distance; + const float y = boss->GetPositionY() + sin(angle) * distance; + const float z = bot->GetPositionZ(); + + return MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +uint32 GrobbulusRotateAction::GetCurrWaypoint() +{ + uint32 current = FindNearestWaypoint(); + if (clockwise) + return (current + 1) % intervals; + + return (current + intervals - 1) % intervals; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Heigan.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Heigan.cpp new file mode 100644 index 00000000..9e622195 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Heigan.cpp @@ -0,0 +1,77 @@ +#include "Playerbots.h" +#include "RaidNaxxActions.h" +#include "RaidNaxxSpellIds.h" +#include "Spell.h" +#include "Timer.h" + +//bool HeiganDanceAction::CalculateSafe() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!boss) +// { +// return false; +// } +// uint32 now = getMSTime(); +// platform_phase = boss->IsWithinDist2d(platform.first, platform.second, 10.0f); +// if (last_eruption_ms != 0 && now - last_eruption_ms > 15000) +// { +// ResetSafe(); +// } +// if (boss->HasUnitState(UNIT_STATE_CASTING)) +// { +// Spell* spell = boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); +// if (!spell) +// { +// spell = boss->GetCurrentSpell(CURRENT_CHANNELED_SPELL); +// } +// if (spell) +// { +// SpellInfo const* info = spell->GetSpellInfo(); +// bool isEruption = NaxxSpellIds::MatchesAnySpellId(info, {NaxxSpellIds::Eruption10}); +// if (!isEruption && info && info->SpellName[LOCALE_enUS]) +// { +// // Fallback to name for custom spell data. +// isEruption = botAI->EqualLowercaseName(info->SpellName[LOCALE_enUS], "eruption"); +// } +// if (isEruption) +// { +// if (last_eruption_ms == 0 || now - last_eruption_ms > 500) +// { +// NextSafe(); +// } +// last_eruption_ms = now; +// } +// } +// } +// return true; +//} +// +//bool HeiganDanceMeleeAction::Execute(Event event) +//{ +// CalculateSafe(); +// if (!platform_phase && botAI->IsMainTank(bot) && !AI_VALUE2(bool, "has aggro", "boss target")) +// { +// return false; +// } +// assert(curr_safe >= 0 && curr_safe <= 3); +// return MoveInside(bot->GetMapId(), waypoints[curr_safe].first, waypoints[curr_safe].second, bot->GetPositionZ(), +// botAI->IsMainTank(bot) ? 0 : 0, MovementPriority::MOVEMENT_COMBAT); +//} +// +//bool HeiganDanceRangedAction::Execute(Event event) +//{ +// CalculateSafe(); +// if (!platform_phase) +// { +// if (MoveTo(bot->GetMapId(), platform.first, platform.second, 276.54f, false, false, false, false, +// MovementPriority::MOVEMENT_COMBAT)) +// { +// return true; +// } +// return MoveInside(bot->GetMapId(), platform.first, platform.second, 276.54f, 2.0f, +// MovementPriority::MOVEMENT_COMBAT); +// } +// botAI->InterruptSpell(); +// return MoveInside(bot->GetMapId(), waypoints[curr_safe].first, waypoints[curr_safe].second, bot->GetPositionZ(), 0, +// MovementPriority::MOVEMENT_COMBAT); +//} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Kelthuzad.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Kelthuzad.cpp new file mode 100644 index 00000000..5f774117 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Kelthuzad.cpp @@ -0,0 +1,177 @@ +#include "RaidNaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" + +bool KelthuzadChooseTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + Unit *target_soldier = nullptr, *target_weaver = nullptr, *target_abomination = nullptr, *target_kelthuzad = nullptr, + *target_guardian = nullptr; + for (auto i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "guardian of icecrown")) + { + if (!target_guardian) + target_guardian = unit; + else if (unit->GetVictim() && target_guardian->GetVictim() && unit->GetVictim()->ToPlayer() && + target_guardian->GetVictim()->ToPlayer() && !botAI->IsAssistTank(unit->GetVictim()->ToPlayer()) && + botAI->IsAssistTank(target_guardian->GetVictim()->ToPlayer())) + { + target_guardian = unit; + } + else if (unit->GetVictim() && target_guardian->GetVictim() && unit->GetVictim()->ToPlayer() && + target_guardian->GetVictim()->ToPlayer() && !botAI->IsAssistTank(unit->GetVictim()->ToPlayer()) && + !botAI->IsAssistTank(target_guardian->GetVictim()->ToPlayer()) && + target_guardian->GetDistance2d(helper.center.first, helper.center.second) > + bot->GetDistance2d(unit)) + { + target_guardian = unit; + } + } + + if (unit->GetDistance2d(helper.center.first, helper.center.second) > 30.0f) + continue; + + if (bot->GetDistance2d(unit) > sPlayerbotAIConfig.spellDistance) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "unstoppable abomination")) + { + if (target_abomination == nullptr || + target_abomination->GetDistance2d(helper.center.first, helper.center.second) > + unit->GetDistance2d(helper.center.first, helper.center.second)) + { + target_abomination = unit; + } + } + if (botAI->EqualLowercaseName(unit->GetName(), "soldier of the frozen wastes")) + { + if (target_soldier == nullptr || + target_soldier->GetDistance2d(helper.center.first, helper.center.second) > + unit->GetDistance2d(helper.center.first, helper.center.second)) + { + target_soldier = unit; + } + } + if (botAI->EqualLowercaseName(unit->GetName(), "soul weaver")) + { + if (target_weaver == nullptr || target_weaver->GetDistance2d(helper.center.first, helper.center.second) > + unit->GetDistance2d(helper.center.first, helper.center.second)) + target_weaver = unit; + } + + if (botAI->EqualLowercaseName(unit->GetName(), "kel'thuzad")) + target_kelthuzad = unit; + } + std::vector targets; + if (botAI->IsRanged(bot)) + { + if (botAI->GetRangedDpsIndex(bot) <= 1) + targets = {target_soldier, target_weaver, target_abomination, target_kelthuzad}; + else + targets = {target_weaver, target_soldier, target_abomination, target_kelthuzad}; + } + else if (botAI->IsAssistTank(bot)) + targets = {target_abomination, target_guardian, target_kelthuzad}; + else + targets = {target_abomination, target_kelthuzad}; + + for (Unit* t : targets) + { + if (t) + { + target = t; + break; + } + } + if (context->GetValue("current target")->Get() == target) + return false; + + if (target_kelthuzad && target == target_kelthuzad) + return Attack(target, true); + + return Attack(target, false); +} + +bool KelthuzadPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (helper.IsPhaseOne()) + { + if (AI_VALUE(Unit*, "current target") == nullptr) + return MoveInside(NAXX_MAP_ID, helper.center.first, helper.center.second, bot->GetPositionZ(), 3.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else if (helper.IsPhaseTwo()) + { + Unit* shadow_fissure = helper.GetAnyShadowFissure(); + if (!shadow_fissure || !bot->IsWithinDistInMap(shadow_fissure, 10.0f)) + { + float distance, angle; + if (botAI->IsMainTank(bot)) + { + if (AI_VALUE2(bool, "has aggro", "current target")) + return MoveTo(NAXX_MAP_ID, helper.tank_pos.first, helper.tank_pos.second, bot->GetPositionZ(), false, false, false, + false, MovementPriority::MOVEMENT_COMBAT); + else + return false; + } + else if (botAI->IsRanged(bot)) + { + uint32 index = botAI->GetRangedIndex(bot); + if (index < 8) + { + distance = 20.0f; + angle = index * M_PI / 4; + } + else + { + distance = 32.0f; + angle = (index - 8) * M_PI / 4; + } + float dx, dy; + dx = helper.center.first + cos(angle) * distance; + dy = helper.center.second + sin(angle) * distance; + return MoveTo(NAXX_MAP_ID, dx, dy, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsTank(bot)) + { + Unit* cur_tar = AI_VALUE(Unit*, "current target"); + if (cur_tar && cur_tar->GetVictim() && cur_tar->GetVictim()->ToPlayer() && + botAI->EqualLowercaseName(cur_tar->GetName(), "guardian of icecrown") && + botAI->IsAssistTank(cur_tar->GetVictim()->ToPlayer())) + { + return MoveTo(NAXX_MAP_ID, helper.assist_tank_pos.first, helper.assist_tank_pos.second, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + else + return false; + } + } + else + { + float dx, dy; + float angle; + if (!botAI->IsRanged(bot)) + angle = shadow_fissure->GetAngle(helper.center.first, helper.center.second); + else + angle = bot->GetAngle(shadow_fissure) + M_PI; + + dx = shadow_fissure->GetPositionX() + cos(angle) * 10.0f; + dy = shadow_fissure->GetPositionY() + sin(angle) * 10.0f; + return MoveTo(NAXX_MAP_ID, dx, dy, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + } + return false; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Loatheb.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Loatheb.cpp new file mode 100644 index 00000000..df43cabe --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Loatheb.cpp @@ -0,0 +1,55 @@ +#include "RaidNaxxActions.h" + +#include "Playerbots.h" + +bool LoathebPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (botAI->IsTank(bot)) + { + if (AI_VALUE2(bool, "has aggro", "boss target")) + return MoveTo(533, helper.mainTankPos.first, helper.mainTankPos.second, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsRanged(bot)) + return MoveInside(533, helper.rangePos.first, helper.rangePos.second, bot->GetPositionZ(), 1.0f, + MovementPriority::MOVEMENT_COMBAT); + return false; +} + +bool LoathebChooseTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + Unit* target_boss = nullptr; + Unit* target_spore = nullptr; + for (auto i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + + if (!unit->IsAlive()) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "spore")) + target_spore = unit; + + if (botAI->EqualLowercaseName(unit->GetName(), "loatheb")) + target_boss = unit; + } + if (target_spore && bot->GetDistance2d(target_spore) <= 1.0f) + target = target_spore; + else + target = target_boss; + + if (!target || context->GetValue("current target")->Get() == target) + return false; + + return Attack(target); +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Maexxna.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Maexxna.cpp new file mode 100644 index 00000000..cfb7d27d --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Maexxna.cpp @@ -0,0 +1,3 @@ +#include "RaidNaxxActions.h" + +// Reserved for Maexxna-specific actions. diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Noth.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Noth.cpp new file mode 100644 index 00000000..4f8fe24a --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Noth.cpp @@ -0,0 +1,3 @@ +#include "RaidNaxxActions.h" + +// Reserved for Noth-specific actions. diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Patchwerk.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Patchwerk.cpp new file mode 100644 index 00000000..e0e2ee3e --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Patchwerk.cpp @@ -0,0 +1,31 @@ +#include "RaidNaxxActions.h" + +#include +#include + +//bool PatchwerkRangedPositionAction::Execute(Event event) +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// return false; +// +// constexpr float minDistance = 12.0f; +// constexpr float maxDistance = 15.0f; +// const float distance = bot->GetExactDist2d(boss); +// +// if (distance >= minDistance && distance <= maxDistance) +// return false; +// +// const float desiredDistance = std::clamp(distance, minDistance, maxDistance); +// float angle = boss->GetAngle(bot); +// +// if (distance < 0.1f) +// angle = boss->GetOrientation(); +// +// const float x = boss->GetPositionX() + std::cos(angle) * desiredDistance; +// const float y = boss->GetPositionY() + std::sin(angle) * desiredDistance; +// const float z = bot->GetPositionZ(); +// +// return MoveTo(boss->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, +// false); +//} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Razuvious.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Razuvious.cpp new file mode 100644 index 00000000..ce0ca6ca --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Razuvious.cpp @@ -0,0 +1,147 @@ +#include "RaidNaxxActions.h" + +#include "ObjectGuid.h" +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +bool RazuviousUseObedienceCrystalAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + // bot->GetCharm + if (Unit* charm = bot->GetCharm()) + { + Unit* target = AI_VALUE2(Unit*, "find target", "instructor razuvious"); + if (!target) + return false; + + if (charm->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_ACTIVE) == NULL_MOTION_TYPE) + { + charm->GetMotionMaster()->Clear(); + charm->GetMotionMaster()->MoveChase(target); + charm->GetAI()->AttackStart(target); + } + Aura* forceObedience = botAI->GetAura("force obedience", charm); + uint32 duration_time; + if (!forceObedience) + { + forceObedience = botAI->GetAura("mind control", charm); + duration_time = 60000; + } + else + duration_time = 90000; + + if (!forceObedience) + return false; + + if (charm->GetDistance(target) <= 0.51f) + { + // taunt + bool tauntUseful = true; + if (forceObedience->GetDuration() <= (duration_time - 5000)) + { + if (target->GetVictim() && botAI->HasAura(29061, target->GetVictim())) + tauntUseful = false; + + if (forceObedience->GetDuration() <= 3000) + tauntUseful = false; + + } + if (forceObedience->GetDuration() >= (duration_time - 500)) + tauntUseful = false; + + if (tauntUseful && !charm->HasSpellCooldown(29060)) + { + // shield + if (!charm->HasSpellCooldown(29061)) + { + charm->CastSpell(charm, 29061, true); + charm->AddSpellCooldown(29061, 0, 30 * 1000); + } + charm->CastSpell(target, 29060, true); + charm->AddSpellCooldown(29060, 0, 20 * 1000); + } + // strike + if (!charm->HasSpellCooldown(61696)) + { + charm->CastSpell(target, 61696, true); + charm->AddSpellCooldown(61696, 0, 4 * 1000); + } + } + } + else + { + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_10MAN_NORMAL) + { + GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); + for (auto i = npcs.begin(); i != npcs.end(); i++) + { + Creature* unit = botAI->GetCreature(*i); + if (!unit) + continue; + + if (botAI->IsMainTank(bot) && unit->GetSpawnId() != 128352) + continue; + + if (!botAI->IsMainTank(bot) && unit->GetSpawnId() != 128353) + continue; + + if (MoveTo(unit, 0.0f, MovementPriority::MOVEMENT_COMBAT)) + return true; + + Creature* creature = bot->GetNPCIfCanInteractWith(*i, UNIT_NPC_FLAG_SPELLCLICK); + if (!creature) + continue; + + creature->HandleSpellClick(bot); + return true; + } + } + else + { + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + for (auto i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + if (botAI->EqualLowercaseName(unit->GetName(), "death knight understudy")) + { + target = unit; + break; + } + } + if (target) + { + if (bot->GetDistance2d(target) > sPlayerbotAIConfig.spellDistance) + return MoveNear(target, sPlayerbotAIConfig.spellDistance, MovementPriority::MOVEMENT_COMBAT); + else + return botAI->CastSpell("mind control", target); + } + } + } + return false; +} + +bool RazuviousTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + Unit* razuvious = AI_VALUE2(Unit*, "find target", "instructor razuvious"); + Unit* understudy = AI_VALUE2(Unit*, "find target", "death knight understudy"); + Unit* target = nullptr; + if (botAI->IsTank(bot)) + target = understudy; + else + target = razuvious; + + if (AI_VALUE(Unit*, "current target") == target) + return false; + + return Attack(target); +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp new file mode 100644 index 00000000..af906da8 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Sapphiron.cpp @@ -0,0 +1,104 @@ +#include "RaidNaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "RaidNaxxBossHelper.h" +#include "RaidNaxxSpellIds.h" + +bool SapphironGroundPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (botAI->IsMainTank(bot)) + { + if (AI_VALUE2(bool, "has aggro", "current target")) + return MoveTo(NAXX_MAP_ID, helper.mainTankPos.first, helper.mainTankPos.second, helper.GENERIC_HEIGHT, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT); + + return false; + } + if (helper.JustLanded()) + { + uint32 index = botAI->GetGroupSlotIndex(bot); + float start_angle = 0.85 * M_PI; + float offset_angle = M_PI * 0.02 * index; + float angle = start_angle + offset_angle; + float distance; + if (botAI->IsRanged(bot)) + distance = 35.0f; + else if (botAI->IsHeal(bot)) + distance = 30.0f; + else + distance = 5.0f; + + float posX = helper.center.first + cos(angle) * distance; + float posY = helper.center.second + sin(angle) * distance; + if (MoveTo(NAXX_MAP_ID, posX, posY, helper.GENERIC_HEIGHT, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(NAXX_MAP_ID, posX, posY, helper.GENERIC_HEIGHT, 2.0f, MovementPriority::MOVEMENT_COMBAT); + } + else + { + std::vector dest; + if (helper.FindPosToAvoidChill(dest)) + return MoveTo(NAXX_MAP_ID, dest[0], dest[1], dest[2], false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} + +bool SapphironFlightPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (helper.WaitForExplosion()) + return MoveToNearestIcebolt(); + else + { + std::vector dest; + if (helper.FindPosToAvoidChill(dest)) + return MoveTo(NAXX_MAP_ID, dest[0], dest[1], dest[2], false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} + +bool SapphironFlightPositionAction::MoveToNearestIcebolt() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Group::MemberSlotList const& slots = group->GetMemberSlots(); + Player* playerWithIcebolt = nullptr; + float minDistance; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (NaxxSpellIds::HasAnyAura(botAI, member, {NaxxSpellIds::Icebolt10, NaxxSpellIds::Icebolt25}) || + botAI->HasAura("icebolt", member, false, false, -1, true)) + { + if (!playerWithIcebolt || minDistance > bot->GetDistance(member)) + { + playerWithIcebolt = member; + minDistance = bot->GetDistance(member); + } + } + } + if (playerWithIcebolt) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "sapphiron"); + if (boss) + { + float angle = boss->GetAngle(playerWithIcebolt); + float posX = playerWithIcebolt->GetPositionX() + cos(angle) * 3.0f; + float posY = playerWithIcebolt->GetPositionY() + sin(angle) * 3.0f; + if (MoveTo(NAXX_MAP_ID, posX, posY, helper.GENERIC_HEIGHT, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveNear(playerWithIcebolt, 3.0f, MovementPriority::MOVEMENT_COMBAT); + } + } + return false; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Shared.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Shared.cpp new file mode 100644 index 00000000..50b3bc1d --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Shared.cpp @@ -0,0 +1,18 @@ +#include "RaidNaxxActions.h" + +uint32 RotateAroundTheCenterPointAction::FindNearestWaypoint() +{ + float minDistance = 0; + int ret = -1; + for (int i = 0; i < intervals; i++) + { + float w_x = waypoints[i].first, w_y = waypoints[i].second; + float dis = bot->GetDistance2d(w_x, w_y); + if (ret == -1 || dis < minDistance) + { + ret = i; + minDistance = dis; + } + } + return ret; +} diff --git a/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp new file mode 100644 index 00000000..0935f054 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Action/RaidNaxxActions_Thaddius.cpp @@ -0,0 +1,134 @@ +#include "RaidNaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "RaidNaxxSpellIds.h" + +bool ThaddiusAttackNearestPetAction::isUseful() +{ + if (!helper.UpdateBossAI()) + return false; + + if (!helper.IsPhasePet()) + return false; + + Unit* target = helper.GetNearestPet(); + if (!bot->IsWithinDistInMap(target, 50.0f)) + return false; + + return true; +} + +bool ThaddiusAttackNearestPetAction::Execute(Event /*event*/) +{ + Unit* target = helper.GetNearestPet(); + if (!bot->IsWithinLOSInMap(target)) + return MoveTo(target, 0, MovementPriority::MOVEMENT_COMBAT); + + if (AI_VALUE(Unit*, "current target") != target) + return Attack(target); + + if (botAI->IsTank(bot) && AI_VALUE2(bool, "has aggro", "current target")) + { + std::pair posForTank = helper.PetPhaseGetPosForTank(); + return MoveTo(533, posForTank.first, posForTank.second, helper.tankPosZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + if (botAI->IsRanged(bot)) + { + std::pair posForRanged = helper.PetPhaseGetPosForRanged(); + return MoveTo(533, posForRanged.first, posForRanged.second, helper.tankPosZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} + +bool ThaddiusMoveToPlatformAction::isUseful() { return true; } + +bool ThaddiusMoveToPlatformAction::Execute(Event /*event*/) +{ + std::vector> position = { + // high left + {3462.99f, -2918.90f}, + // high right + {3520.65f, -2976.51f}, + // low left + {3471.36f, -2910.65f}, + // low right + {3528.80f, -2967.04f}, + // center + {3512.19f, -2928.58f}, + }; + float high_z = 312.00f, low_z = 304.02f; + bool is_left = bot->GetDistance2d(position[0].first, position[0].second) < + bot->GetDistance2d(position[1].first, position[1].second); + if (bot->GetPositionZ() >= (high_z - 3.0f)) + { + if (is_left) + { + if (!MoveTo(bot->GetMapId(), position[0].first, position[0].second, high_z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + { + float distance = bot->GetExactDist2d(position[0].first, position[0].second); + if (distance < sPlayerbotAIConfig.contactDistance) + JumpTo(bot->GetMapId(), position[2].first, position[2].second, low_z, MovementPriority::MOVEMENT_COMBAT); + // bot->TeleportTo(bot->GetMapId(), position[2].first, position[2].second, low_z, bot->GetOrientation()); + } + } + else + { + if (!MoveTo(bot->GetMapId(), position[1].first, position[1].second, high_z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + { + float distance = bot->GetExactDist2d(position[1].first, position[1].second); + if (distance < sPlayerbotAIConfig.contactDistance) + JumpTo(bot->GetMapId(), position[3].first, position[3].second, low_z, MovementPriority::MOVEMENT_COMBAT); + // bot->TeleportTo(bot->GetMapId(), position[3].first, position[3].second, low_z, bot->GetOrientation()); + } + } + } + else + return MoveTo(bot->GetMapId(), position[4].first, position[4].second, low_z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + + return true; +} + +bool ThaddiusMovePolarityAction::isUseful() +{ + return !botAI->IsMainTank(bot) || AI_VALUE2(bool, "has aggro", "current target"); +} + +bool ThaddiusMovePolarityAction::Execute(Event /*event*/) +{ + std::vector> position = { + // left melee + {3508.29f, -2920.12f}, + // left ranged + {3501.72f, -2913.36f}, + // right melee + {3519.74f, -2931.69f}, + // right ranged + {3524.32f, -2936.26f}, + // center melee + {3512.19f, -2928.58f}, + // center ranged + {3504.68f, -2936.68f}, + }; + uint32 idx; + if (NaxxSpellIds::HasAnyAura( + botAI, bot, + {NaxxSpellIds::NegativeCharge10, NaxxSpellIds::NegativeCharge25, NaxxSpellIds::NegativeChargeStack}) || + botAI->HasAura("negative charge", bot, false, false, -1, true)) + { + idx = 0; + } + else if (NaxxSpellIds::HasAnyAura( + botAI, bot, + {NaxxSpellIds::PositiveCharge10, NaxxSpellIds::PositiveCharge25, NaxxSpellIds::PositiveChargeStack}) || + botAI->HasAura("positive charge", bot, false, false, -1, true)) + { + idx = 1; + } + else + { + idx = 2; + } + idx = idx * 2 + botAI->IsRanged(bot); + return MoveTo(bot->GetMapId(), position[idx].first, position[idx].second, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} diff --git a/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp new file mode 100644 index 00000000..d34b48e6 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.cpp @@ -0,0 +1,322 @@ +#include "RaidNaxxMultipliers.h" + +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "FollowActions.h" +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "MovementActions.h" +#include "PaladinActions.h" +#include "PriestActions.h" +#include "RaidNaxxActions.h" +#include "RaidNaxxSpellIds.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ScriptedCreature.h" +#include "ShamanActions.h" +#include "Spell.h" +#include "UseMeetingStoneAction.h" +#include "WarriorActions.h" + +float GrobbulusMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return 1.0f; + + if (dynamic_cast(action)) + return botAI->IsMainTank(bot) ? 0.0f : 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +//float HeiganDanceMultiplier::GetValue(Action* action) +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!boss) +// { +// return 1.0f; +// } +// bool platform_phase = boss->IsWithinDist2d(2794.26f, -3706.67f, 10.0f); +// bool eruption_casting = false; +// if (boss->HasUnitState(UNIT_STATE_CASTING)) +// { +// Spell* spell = boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); +// if (!spell) +// { +// spell = boss->GetCurrentSpell(CURRENT_CHANNELED_SPELL); +// } +// if (spell) +// { +// SpellInfo const* info = spell->GetSpellInfo(); +// bool isEruption = NaxxSpellIds::MatchesAnySpellId(info, {NaxxSpellIds::Eruption10}); +// if (!isEruption && info && info->SpellName[LOCALE_enUS]) +// { +// // Fallback to name for custom spell data. +// isEruption = botAI->EqualLowercaseName(info->SpellName[LOCALE_enUS], "eruption"); +// } +// if (isEruption) +// { +// eruption_casting = true; +// } +// } +// } +// if (dynamic_cast(action) || +// dynamic_cast(action) || +// dynamic_cast(action) ) +// { +// return 0.0f; +// } +// if (!platform_phase && !eruption_casting) +// { +// return 1.0f; +// } +// if (dynamic_cast(action) || dynamic_cast(action)) +// { +// return 1.0f; +// } +// if (dynamic_cast(action) && !dynamic_cast(action)) +// { +// CastSpellAction* spellAction = dynamic_cast(action); +// uint32 spellId = AI_VALUE2(uint32, "spell id", spellAction->getSpell()); +// SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); +// if (!spellInfo) +// { +// return 0.0f; +// } +// uint32 castTime = spellInfo->CalcCastTime(); +// if (castTime == 0 && !spellInfo->IsChanneled()) +// { +// return 1.0f; +// } +// } +// return 0.0f; +//} + +float LoathebGenericMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "loatheb"); + if (!boss) + return 1.0f; + + context->GetValue("neglect threat")->Set(true); + if (botAI->GetState() == BOT_STATE_COMBAT && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + if (!dynamic_cast(action)) + return 1.0f; + + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::NecroticAura10}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("necrotic aura", bot); + } + if (!aura || aura->GetDuration() <= 1500) + return 1.0f; + + return 0.0f; +} + +float ThaddiusGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + // pet phase + if (helper.IsPhasePet() && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + { + return 0.0f; + } + // die at the same time + Unit* target = AI_VALUE(Unit*, "current target"); + Unit* feugen = AI_VALUE2(Unit*, "find target", "feugen"); + Unit* stalagg = AI_VALUE2(Unit*, "find target", "stalagg"); + if (helper.IsPhasePet() && target && feugen && stalagg && target->GetHealthPct() <= 40 && + (feugen->GetHealthPct() >= target->GetHealthPct() + 3 || stalagg->GetHealthPct() >= target->GetHealthPct() + 3)) + { + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + } + // magnetic pull + // uint32 curr_timer = eventMap->GetTimer(); + // // if (curr_phase == 2 && bot->GetPositionZ() > 312.5f && dynamic_cast(action)) + // { + // if (curr_phase == 2 && (curr_timer % 20000 >= 18000 || curr_timer % 20000 <= 2000) && + // dynamic_cast(action)) + // { + // // MotionMaster *mm = bot->GetMotionMaster(); + // // mm->Clear(); + // return 0.0f; + // } + // thaddius phase + // if (curr_phase == 8 && dynamic_cast(action)) + // { + // return 0.0f; + // } + return 1.0f; +} + +float SapphironGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float InstructorRazuviousGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + context->GetValue("neglect threat")->Set(true); + if (botAI->GetState() == BOT_STATE_COMBAT && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + { + return 0.0f; + } + return 1.0f; +} + +float KelthuzadGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if ((dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + { + return 0.0f; + } + if (helper.IsPhaseOne()) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + } + if (helper.IsPhaseTwo()) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + } + return 1.0f; +} + +float AnubrekhanGenericMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'rekhan"); + if (!boss) + return 1.0f; + + if (NaxxSpellIds::HasAnyAura( + botAI, boss, {NaxxSpellIds::LocustSwarm10, NaxxSpellIds::LocustSwarm10Alt, NaxxSpellIds::LocustSwarm25}) || + botAI->HasAura("locust swarm", boss)) + { + if (dynamic_cast(action)) + return 0.0f; + } + return 1.0f; +} + +float FourhorsemanGenericMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sir zeliek"); + if (!boss) + return 1.0f; + + context->GetValue("neglect threat")->Set(true); + if ((dynamic_cast(action) || dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +// float GothikGenericMultiplier::GetValue(Action* action) +// { +// Unit* boss = AI_VALUE2(Unit*, "find target", "gothik the harvester"); +// if (!boss) +// { +// return 1.0f; +// } +// BossAI* boss_ai = dynamic_cast(boss->GetAI()); +// EventMap* eventMap = boss_botAI->GetEvents(); +// uint32 curr_phase = eventMap->GetPhaseMask(); +// if (curr_phase == 1 && (dynamic_cast(action))) +// { +// return 0.0f; +// } +// if (curr_phase == 1 && (dynamic_cast(action))) +// { +// Unit* target = action->GetTarget(); +// if (target == boss) +// { +// return 0.0f; +// } +// } +// return 1.0f; +// } + +float GluthGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if ((dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + + if (botAI->IsMainTank(bot)) + { + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::MortalWound10, NaxxSpellIds::MortalWound25}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("mortal wound", bot, false, true); + } + if (aura && aura->GetStackAmount() >= 5) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + { + return 0.0f; + } + } + } + if (dynamic_cast(action)) + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (helper.IsZombieChow(target)) + return 0.0f; + } + return 1.0f; +} diff --git a/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h new file mode 100644 index 00000000..af702f60 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Multiplier/RaidNaxxMultipliers.h @@ -0,0 +1,115 @@ + +#ifndef _PLAYERRBOT_RAIDNAXXMULTIPLIERS_H +#define _PLAYERRBOT_RAIDNAXXMULTIPLIERS_H + +#include "Multiplier.h" +#include "RaidNaxxBossHelper.h" + +class GrobbulusMultiplier : public Multiplier +{ +public: + GrobbulusMultiplier(PlayerbotAI* ai) : Multiplier(ai, "grobbulus") {} + +public: + virtual float GetValue(Action* action); +}; + +//class HeiganDanceMultiplier : public Multiplier +//{ +//public: +// HeiganDanceMultiplier(PlayerbotAI* ai) : Multiplier(ai, "helgan dance") {} +// +//public: +// virtual float GetValue(Action* action); +//}; + +class LoathebGenericMultiplier : public Multiplier +{ +public: + LoathebGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "loatheb generic") {} + +public: + virtual float GetValue(Action* action); +}; + +class ThaddiusGenericMultiplier : public Multiplier +{ +public: + ThaddiusGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "thaddius generic"), helper(ai) {} + +public: + virtual float GetValue(Action* action); + +private: + ThaddiusBossHelper helper; +}; + +class SapphironGenericMultiplier : public Multiplier +{ +public: + SapphironGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "sapphiron generic"), helper(ai) {} + + virtual float GetValue(Action* action); + +private: + SapphironBossHelper helper; +}; + +class InstructorRazuviousGenericMultiplier : public Multiplier +{ +public: + InstructorRazuviousGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "instructor razuvious generic"), helper(ai) {} + virtual float GetValue(Action* action); + +private: + RazuviousBossHelper helper; +}; + +class KelthuzadGenericMultiplier : public Multiplier +{ +public: + KelthuzadGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "kelthuzad generic"), helper(ai) {} + virtual float GetValue(Action* action); + +private: + KelthuzadBossHelper helper; +}; + +class AnubrekhanGenericMultiplier : public Multiplier +{ +public: + AnubrekhanGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "anubrekhan generic") {} + +public: + virtual float GetValue(Action* action); +}; + +class FourhorsemanGenericMultiplier : public Multiplier +{ +public: + FourhorsemanGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "fourhorseman generic") {} + +public: + virtual float GetValue(Action* action); +}; + +// class GothikGenericMultiplier : public Multiplier +// { +// public: +// GothikGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "gothik generic") {} + +// public: +// virtual float GetValue(Action* action); +// }; + +class GluthGenericMultiplier : public Multiplier +{ +public: + GluthGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "gluth generic"), helper(ai) {} + float GetValue(Action* action) override; + +private: + GluthBossHelper helper; +}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h b/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h new file mode 100644 index 00000000..45941d67 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/RaidNaxxActionContext.h @@ -0,0 +1,95 @@ +// /* +// * 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_RAIDNAXXACTIONCONTEXT_H +#define _PLAYERBOT_RAIDNAXXACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "RaidNaxxActions.h" + +class RaidNaxxActionContext : public NamedObjectContext +{ +public: + RaidNaxxActionContext() + { + creators["grobbulus go behind the boss"] = &RaidNaxxActionContext::go_behind_the_boss; + creators["rotate grobbulus"] = &RaidNaxxActionContext::rotate_grobbulus; + creators["grobbulus move center"] = &RaidNaxxActionContext::grobbulus_move_center; + creators["grobbulus move away"] = &RaidNaxxActionContext::grobbulus_move_away; + + //creators["heigan dance melee"] = &RaidNaxxActionContext::heigan_dance_melee; + //creators["heigan dance ranged"] = &RaidNaxxActionContext::heigan_dance_ranged; + creators["thaddius attack nearest pet"] = &RaidNaxxActionContext::thaddius_attack_nearest_pet; + // creators["thaddius melee to place"] = &RaidNaxxActionContext::thaddius_tank_to_place; + // creators["thaddius ranged to place"] = &RaidNaxxActionContext::thaddius_ranged_to_place; + creators["thaddius move to platform"] = &RaidNaxxActionContext::thaddius_move_to_platform; + creators["thaddius move polarity"] = &RaidNaxxActionContext::thaddius_move_polarity; + + creators["razuvious use obedience crystal"] = &RaidNaxxActionContext::razuvious_use_obedience_crystal; + creators["razuvious target"] = &RaidNaxxActionContext::razuvious_target; + + creators["horseman attract alternatively"] = &RaidNaxxActionContext::horseman_attract_alternatively; + creators["horseman attack in order"] = &RaidNaxxActionContext::horseman_attack_in_order; + + creators["sapphiron ground position"] = &RaidNaxxActionContext::sapphiron_ground_position; + creators["sapphiron flight position"] = &RaidNaxxActionContext::sapphiron_flight_position; + + creators["kel'thuzad choose target"] = &RaidNaxxActionContext::kelthuzad_choose_target; + creators["kel'thuzad position"] = &RaidNaxxActionContext::kelthuzad_position; + + creators["anub'rekhan choose target"] = &RaidNaxxActionContext::anubrekhan_choose_target; + creators["anub'rekhan position"] = &RaidNaxxActionContext::anubrekhan_position; + + creators["gluth choose target"] = &RaidNaxxActionContext::gluth_choose_target; + creators["gluth position"] = &RaidNaxxActionContext::gluth_position; + creators["gluth slowdown"] = &RaidNaxxActionContext::gluth_slowdown; + + //creators["patchwerk ranged position"] = &RaidNaxxActionContext::patchwerk_ranged_position; + + creators["loatheb position"] = &RaidNaxxActionContext::loatheb_position; + creators["loatheb choose target"] = &RaidNaxxActionContext::loatheb_choose_target; + } + +private: + static Action* go_behind_the_boss(PlayerbotAI* ai) { return new GrobbulusGoBehindAction(ai); } + static Action* rotate_grobbulus(PlayerbotAI* ai) { return new GrobbulusRotateAction(ai); } + static Action* grobbulus_move_center(PlayerbotAI* ai) { return new GrobblulusMoveCenterAction(ai); } + static Action* grobbulus_move_away(PlayerbotAI* ai) { return new GrobbulusMoveAwayAction(ai); } + //static Action* heigan_dance_melee(PlayerbotAI* ai) { return new HeiganDanceMeleeAction(ai); } + //static Action* heigan_dance_ranged(PlayerbotAI* ai) { return new HeiganDanceRangedAction(ai); } + static Action* thaddius_attack_nearest_pet(PlayerbotAI* ai) { return new ThaddiusAttackNearestPetAction(ai); } + // static Action* thaddius_tank_to_place(PlayerbotAI* ai) { return new ThaddiusMeleeToPlaceAction(ai); } + // static Action* thaddius_ranged_to_place(PlayerbotAI* ai) { return new ThaddiusRangedToPlaceAction(ai); } + static Action* thaddius_move_to_platform(PlayerbotAI* ai) { return new ThaddiusMoveToPlatformAction(ai); } + static Action* thaddius_move_polarity(PlayerbotAI* ai) { return new ThaddiusMovePolarityAction(ai); } + static Action* razuvious_target(PlayerbotAI* ai) { return new RazuviousTargetAction(ai); } + static Action* razuvious_use_obedience_crystal(PlayerbotAI* ai) + { + return new RazuviousUseObedienceCrystalAction(ai); + } + static Action* horseman_attract_alternatively(PlayerbotAI* ai) + { + return new HorsemanAttractAlternativelyAction(ai); + } + static Action* horseman_attack_in_order(PlayerbotAI* ai) { return new HorsemanAttactInOrderAction(ai); } + // static Action* sapphiron_ground_main_tank_position(PlayerbotAI* ai) { return new + // SapphironGroundMainTankPositionAction(ai); } + static Action* sapphiron_ground_position(PlayerbotAI* ai) { return new SapphironGroundPositionAction(ai); } + static Action* sapphiron_flight_position(PlayerbotAI* ai) { return new SapphironFlightPositionAction(ai); } + // static Action* sapphiron_avoid_chill(PlayerbotAI* ai) { return new SapphironAvoidChillAction(ai); } + static Action* kelthuzad_choose_target(PlayerbotAI* ai) { return new KelthuzadChooseTargetAction(ai); } + static Action* kelthuzad_position(PlayerbotAI* ai) { return new KelthuzadPositionAction(ai); } + static Action* anubrekhan_choose_target(PlayerbotAI* ai) { return new AnubrekhanChooseTargetAction(ai); } + static Action* anubrekhan_position(PlayerbotAI* ai) { return new AnubrekhanPositionAction(ai); } + static Action* gluth_choose_target(PlayerbotAI* ai) { return new GluthChooseTargetAction(ai); } + static Action* gluth_position(PlayerbotAI* ai) { return new GluthPositionAction(ai); } + static Action* gluth_slowdown(PlayerbotAI* ai) { return new GluthSlowdownAction(ai); } + //static Action* patchwerk_ranged_position(PlayerbotAI* ai) { return new PatchwerkRangedPositionAction(ai); } + static Action* loatheb_position(PlayerbotAI* ai) { return new LoathebPositionAction(ai); } + static Action* loatheb_choose_target(PlayerbotAI* ai) { return new LoathebChooseTargetAction(ai); } +}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h b/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h new file mode 100644 index 00000000..d980b476 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h @@ -0,0 +1,86 @@ +// /* +// * 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_RAIDNAXXTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H + +#include "AiObjectContext.h" +#include "NamedObjectContext.h" +#include "RaidNaxxTriggers.h" + +class RaidNaxxTriggerContext : public NamedObjectContext +{ +public: + RaidNaxxTriggerContext() + { + creators["mutating injection melee"] = &RaidNaxxTriggerContext::mutating_injection_melee; + creators["mutating injection ranged"] = &RaidNaxxTriggerContext::mutating_injection_ranged; + creators["mutating injection removed"] = &RaidNaxxTriggerContext::mutating_injection_removed; + creators["grobbulus cloud"] = &RaidNaxxTriggerContext::grobbulus_cloud; + //creators["heigan melee"] = &RaidNaxxTriggerContext::heigan_melee; + //creators["heigan ranged"] = &RaidNaxxTriggerContext::heigan_ranged; + + creators["thaddius phase pet"] = &RaidNaxxTriggerContext::thaddius_phase_pet; + creators["thaddius phase pet lose aggro"] = &RaidNaxxTriggerContext::thaddius_phase_pet_lose_aggro; + creators["thaddius phase transition"] = &RaidNaxxTriggerContext::thaddius_phase_transition; + creators["thaddius phase thaddius"] = &RaidNaxxTriggerContext::thaddius_phase_thaddius; + + creators["razuvious tank"] = &RaidNaxxTriggerContext::razuvious_tank; + creators["razuvious nontank"] = &RaidNaxxTriggerContext::razuvious_nontank; + + creators["horseman attractors"] = &RaidNaxxTriggerContext::horseman_attractors; + creators["horseman except attractors"] = &RaidNaxxTriggerContext::horseman_except_attractors; + + creators["sapphiron ground"] = &RaidNaxxTriggerContext::sapphiron_ground; + creators["sapphiron flight"] = &RaidNaxxTriggerContext::sapphiron_flight; + + creators["kel'thuzad"] = &RaidNaxxTriggerContext::kelthuzad; + + creators["anub'rekhan"] = &RaidNaxxTriggerContext::anubrekhan; + creators["faerlina"] = &RaidNaxxTriggerContext::faerlina; + creators["maexxna"] = &RaidNaxxTriggerContext::maexxna; + //creators["patchwerk tank"] = &RaidNaxxTriggerContext::patchwerk_tank; + //creators["patchwerk non-tank"] = &RaidNaxxTriggerContext::patchwerk_non_tank; + //creators["patchwerk ranged"] = &RaidNaxxTriggerContext::patchwerk_ranged; + + creators["gluth"] = &RaidNaxxTriggerContext::gluth; + creators["gluth main tank mortal wound"] = &RaidNaxxTriggerContext::gluth_main_tank_mortal_wound; + + creators["loatheb"] = &RaidNaxxTriggerContext::loatheb; + } + +private: + static Trigger* mutating_injection_melee(PlayerbotAI* ai) { return new MutatingInjectionMeleeTrigger(ai); } + static Trigger* mutating_injection_ranged(PlayerbotAI* ai) { return new MutatingInjectionRangedTrigger(ai); } + static Trigger* mutating_injection_removed(PlayerbotAI* ai) { return new MutatingInjectionRemovedTrigger(ai); } + static Trigger* grobbulus_cloud(PlayerbotAI* ai) { return new GrobbulusCloudTrigger(ai); } + //static Trigger* heigan_melee(PlayerbotAI* ai) { return new HeiganMeleeTrigger(ai); } + //static Trigger* heigan_ranged(PlayerbotAI* ai) { return new HeiganRangedTrigger(ai); } + + static Trigger* thaddius_phase_pet(PlayerbotAI* ai) { return new ThaddiusPhasePetTrigger(ai); } + static Trigger* thaddius_phase_pet_lose_aggro(PlayerbotAI* ai) { return new ThaddiusPhasePetLoseAggroTrigger(ai); } + static Trigger* thaddius_phase_transition(PlayerbotAI* ai) { return new ThaddiusPhaseTransitionTrigger(ai); } + static Trigger* thaddius_phase_thaddius(PlayerbotAI* ai) { return new ThaddiusPhaseThaddiusTrigger(ai); } + static Trigger* razuvious_tank(PlayerbotAI* ai) { return new RazuviousTankTrigger(ai); } + static Trigger* razuvious_nontank(PlayerbotAI* ai) { return new RazuviousNontankTrigger(ai); } + + static Trigger* horseman_attractors(PlayerbotAI* ai) { return new HorsemanAttractorsTrigger(ai); } + static Trigger* horseman_except_attractors(PlayerbotAI* ai) { return new HorsemanExceptAttractorsTrigger(ai); } + + static Trigger* sapphiron_ground(PlayerbotAI* ai) { return new SapphironGroundTrigger(ai); } + static Trigger* sapphiron_flight(PlayerbotAI* ai) { return new SapphironFlightTrigger(ai); } + static Trigger* kelthuzad(PlayerbotAI* ai) { return new KelthuzadTrigger(ai); } + static Trigger* anubrekhan(PlayerbotAI* ai) { return new AnubrekhanTrigger(ai); } + static Trigger* faerlina(PlayerbotAI* ai) { return new FaerlinaTrigger(ai); } + static Trigger* maexxna(PlayerbotAI* ai) { return new MaexxnaTrigger(ai); } + //static Trigger* patchwerk_tank(PlayerbotAI* ai) { return new PatchwerkTankTrigger(ai); } + //static Trigger* patchwerk_non_tank(PlayerbotAI* ai) { return new PatchwerkNonTankTrigger(ai); } + //static Trigger* patchwerk_ranged(PlayerbotAI* ai) { return new PatchwerkRangedTrigger(ai); } + static Trigger* gluth(PlayerbotAI* ai) { return new GluthTrigger(ai); } + static Trigger* gluth_main_tank_mortal_wound(PlayerbotAI* ai) { return new GluthMainTankMortalWoundTrigger(ai); } + static Trigger* loatheb(PlayerbotAI* ai) { return new LoathebTrigger(ai); } +}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp new file mode 100644 index 00000000..e0c3421d --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.cpp @@ -0,0 +1,156 @@ +#include "RaidNaxxStrategy.h" + +#include "RaidNaxxMultipliers.h" + +void RaidNaxxStrategy::InitTriggers(std::vector& triggers) +{ + // Grobbulus + triggers.push_back(new TriggerNode("mutating injection melee", + { NextAction("grobbulus move away", ACTION_RAID + 2) } + )); + + triggers.push_back(new TriggerNode("mutating injection ranged", + { NextAction("grobbulus go behind the boss", ACTION_RAID + 2) } + )); + + triggers.push_back(new TriggerNode("mutating injection removed", + { NextAction("grobbulus move center", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("grobbulus cloud", + { NextAction("rotate grobbulus", ACTION_RAID + 1) } + )); + + // Heigan the Unclean + //triggers.push_back(new TriggerNode("heigan melee", + // { NextAction("heigan dance melee", ACTION_RAID + 1) } + //)); + + //triggers.push_back(new TriggerNode("heigan ranged", + // { NextAction("heigan dance ranged", ACTION_RAID + 1) } + //)); + + // Kel'Thuzad + triggers.push_back( + new TriggerNode("kel'thuzad", + { + NextAction("kel'thuzad position", ACTION_RAID + 2), + NextAction("kel'thuzad choose target", ACTION_RAID + 1) + }) + ); + + // Anub'Rekhan + triggers.push_back(new TriggerNode("anub'rekhan", + { NextAction("anub'rekhan position", ACTION_RAID + 1) } + )); + + // Grand Widow Faerlina + triggers.push_back(new TriggerNode("faerlina", + { NextAction("avoid aoe", ACTION_RAID + 1) } + )); + + // Maexxna + triggers.push_back( + new TriggerNode("maexxna", + { + NextAction("rear flank", ACTION_RAID + 1), + NextAction("avoid aoe", ACTION_RAID + 1) + }) + ); + + // Patchwerk + //triggers.push_back(new TriggerNode("patchwerk tank", + // { NextAction("tank face", ACTION_RAID + 2) } + //)); + + //triggers.push_back(new TriggerNode("patchwerk ranged", + // { NextAction("patchwerk ranged position", ACTION_RAID + 2) } + //)); + + //triggers.push_back(new TriggerNode("patchwerk non-tank", + // { NextAction("rear flank", ACTION_RAID + 1) } + //)); + + // Thaddius + triggers.push_back(new TriggerNode("thaddius phase pet", + { NextAction("thaddius attack nearest pet", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("thaddius phase pet lose aggro", + { NextAction("taunt spell", ACTION_RAID + 2) } + )); + + triggers.push_back(new TriggerNode("thaddius phase transition", + { NextAction("thaddius move to platform", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("thaddius phase thaddius", + { NextAction("thaddius move polarity", ACTION_RAID + 1) } + )); + + // Instructor Razuvious + triggers.push_back(new TriggerNode("razuvious tank", + { NextAction("razuvious use obedience crystal", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("razuvious nontank", + { NextAction("razuvious target", ACTION_RAID + 1) } + )); + + // four horseman + triggers.push_back(new TriggerNode("horseman attractors", + { NextAction("horseman attract alternatively", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("horseman except attractors", + { NextAction("horseman attack in order", ACTION_RAID + 1) } + )); + + // sapphiron + triggers.push_back(new TriggerNode("sapphiron ground", + { NextAction("sapphiron ground position", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("sapphiron flight", + { NextAction("sapphiron flight position", ACTION_RAID + 1) } + )); + + // Gluth + triggers.push_back( + new TriggerNode("gluth", + { + NextAction("gluth choose target", ACTION_RAID + 1), + NextAction("gluth position", ACTION_RAID + 1), + NextAction("gluth slowdown", ACTION_RAID) + }) + ); + + triggers.push_back(new TriggerNode("gluth main tank mortal wound", + { NextAction("taunt spell", ACTION_RAID + 1) } + )); + + // Loatheb + triggers.push_back( + new TriggerNode("loatheb", + { + NextAction("loatheb position", ACTION_RAID + 1), + NextAction("loatheb choose target", ACTION_RAID + 1) + }) + ); + +} + +void RaidNaxxStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new GrobbulusMultiplier(botAI)); + //multipliers.push_back(new HeiganDanceMultiplier(botAI)); + multipliers.push_back(new LoathebGenericMultiplier(botAI)); + multipliers.push_back(new ThaddiusGenericMultiplier(botAI)); + multipliers.push_back(new SapphironGenericMultiplier(botAI)); + multipliers.push_back(new InstructorRazuviousGenericMultiplier(botAI)); + multipliers.push_back(new KelthuzadGenericMultiplier(botAI)); + multipliers.push_back(new AnubrekhanGenericMultiplier(botAI)); + multipliers.push_back(new FourhorsemanGenericMultiplier(botAI)); + // multipliers.push_back(new GothikGenericMultiplier(botAI)); + multipliers.push_back(new GluthGenericMultiplier(botAI)); +} diff --git a/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h new file mode 100644 index 00000000..4b8a9a7c --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h @@ -0,0 +1,18 @@ + +#ifndef _PLAYERBOT_RAIDNAXXSTRATEGY_H +#define _PLAYERBOT_RAIDNAXXSTRATEGY_H + +#include "AiObjectContext.h" +#include "Multiplier.h" +#include "Strategy.h" + +class RaidNaxxStrategy : public Strategy +{ +public: + RaidNaxxStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "naxx"; } + virtual void InitTriggers(std::vector& triggers) override; + virtual void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp new file mode 100644 index 00000000..f4fb38e1 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.cpp @@ -0,0 +1,257 @@ +#include "RaidNaxxTriggers.h" + +#include "Playerbots.h" +#include "RaidNaxxSpellIds.h" +#include "Timer.h" +#include "Trigger.h" + +bool MutatingInjectionMeleeTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + return MutatingInjectionTrigger::IsActive() && !botAI->IsRanged(bot); +} + +bool MutatingInjectionRangedTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + return MutatingInjectionTrigger::IsActive() && botAI->IsRanged(bot); +} + +bool AuraRemovedTrigger::IsActive() +{ + bool check = botAI->HasAura(name, bot, false, false, -1, true); + bool ret = false; + if (prev_check && !check) + ret = true; + + prev_check = check; + return ret; +} + +bool MutatingInjectionRemovedTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + return HasNoAuraTrigger::IsActive() && botAI->GetState() == BOT_STATE_COMBAT && botAI->IsRanged(bot); +} + +bool GrobbulusCloudTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + if (!botAI->IsMainTank(bot)) + return false; + + // bot->Yell("has aggro on " + boss->GetName() + " : " + to_string(AI_VALUE2(bool, "has aggro", "boss target")), + // LANG_UNIVERSAL); + if (!AI_VALUE2(bool, "has aggro", "boss target")) + return false; + + uint32 now = getMSTime(); + bool poison_cloud_casting = false; + if (boss->HasUnitState(UNIT_STATE_CASTING)) + { + Spell* spell = boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell) + spell = boss->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + + if (spell) + poison_cloud_casting = NaxxSpellIds::MatchesAnySpellId(spell->GetSpellInfo(), {NaxxSpellIds::PoisonCloud}); + + } + if (!poison_cloud_casting && last_cloud_ms != 0 && now - last_cloud_ms < CloudRotationDelayMs) + return false; + + last_cloud_ms = now; + return true; +} + +//bool HeiganMeleeTrigger::IsActive() +//{ +// Unit* heigan = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!heigan) +// { +// return false; +// } +// return !botAI->IsRanged(bot); +//} +// +//bool HeiganRangedTrigger::IsActive() +//{ +// Unit* heigan = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!heigan) +// { +// return false; +// } +// return botAI->IsRanged(bot); +//} + +bool RazuviousTankTrigger::IsActive() +{ + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_10MAN_NORMAL) + return helper.UpdateBossAI() && botAI->IsTank(bot); + + return helper.UpdateBossAI() && bot->getClass() == CLASS_PRIEST; +} + +bool RazuviousNontankTrigger::IsActive() +{ + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_10MAN_NORMAL) + return helper.UpdateBossAI() && !(botAI->IsTank(bot)); + + return helper.UpdateBossAI() && !(bot->getClass() == CLASS_PRIEST); +} + +bool HorsemanAttractorsTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsAttracter(bot); +} + +bool HorsemanExceptAttractorsTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return !helper.IsAttracter(bot); +} + +bool SapphironGroundTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseGround(); +} + +bool SapphironFlightTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseFlight(); +} + +bool GluthTrigger::IsActive() { return helper.UpdateBossAI(); } + +bool GluthMainTankMortalWoundTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* mt = AI_VALUE(Unit*, "main tank"); + if (!mt) + return false; + + Aura* aura = NaxxSpellIds::GetAnyAura(mt, {NaxxSpellIds::MortalWound10, NaxxSpellIds::MortalWound25}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("mortal wound", mt, false, true); + } + if (!aura || aura->GetStackAmount() < 5) + return false; + + return true; +} + +bool KelthuzadTrigger::IsActive() { return helper.UpdateBossAI(); } + +bool AnubrekhanTrigger::IsActive() { + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'rekhan"); + if (!boss) + return false; + + return true; +} + +bool FaerlinaTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grand widow faerlina"); + if (!boss) + return false; + + return true; +} + +bool MaexxnaTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "maexxna"); + if (!boss) + return false; + + return !botAI->IsTank(bot); +} + +//bool PatchwerkTankTrigger::IsActive() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// { +// return false; +// } +// return !botAI->IsTank(bot) && !botAI->IsRanged(bot); +//} +// +//bool PatchwerkRangedTrigger::IsActive() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// { +// return false; +// } +// return !botAI->IsTank(bot) && botAI->IsRanged(bot); +//} +// +//bool PatchwerkNonTankTrigger::IsActive() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// { +// return false; +// } +// return !botAI->IsTank(bot); +//} + +bool LoathebTrigger::IsActive() { return helper.UpdateBossAI(); } + +bool ThaddiusPhasePetTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhasePet(); +} + +bool ThaddiusPhaseTransitionTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseTransition(); +} + +bool ThaddiusPhaseThaddiusTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseThaddius(); +} diff --git a/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h new file mode 100644 index 00000000..6f7727cf --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Trigger/RaidNaxxTriggers.h @@ -0,0 +1,259 @@ + +#ifndef _PLAYERBOT_RAIDNAXXTRIGGERS_H +#define _PLAYERBOT_RAIDNAXXTRIGGERS_H + +#include "EventMap.h" +#include "GenericTriggers.h" +#include "PlayerbotAIConfig.h" +#include "RaidNaxxBossHelper.h" +#include "Trigger.h" + +class MutatingInjectionTrigger : public HasAuraTrigger +{ +public: + MutatingInjectionTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "mutating injection", 1) {} +}; + +class MutatingInjectionMeleeTrigger : public MutatingInjectionTrigger +{ +public: + MutatingInjectionMeleeTrigger(PlayerbotAI* ai) : MutatingInjectionTrigger(ai) {} + bool IsActive() override; +}; + +class MutatingInjectionRangedTrigger : public MutatingInjectionTrigger +{ +public: + MutatingInjectionRangedTrigger(PlayerbotAI* ai) : MutatingInjectionTrigger(ai) {} + bool IsActive() override; +}; + +class AuraRemovedTrigger : public Trigger +{ +public: + AuraRemovedTrigger(PlayerbotAI* botAI, std::string name) : Trigger(botAI, name, 1) + { + this->prev_check = false; + } + virtual bool IsActive() override; + +protected: + bool prev_check; +}; + +class MutatingInjectionRemovedTrigger : public HasNoAuraTrigger +{ +public: + MutatingInjectionRemovedTrigger(PlayerbotAI* ai) : HasNoAuraTrigger(ai, "mutating injection") {} + virtual bool IsActive(); +}; + +class GrobbulusCloudTrigger : public Trigger +{ +public: + GrobbulusCloudTrigger(PlayerbotAI* ai) : Trigger(ai, "grobbulus cloud event"), last_cloud_ms(0) {} + bool IsActive() override; + +private: + uint32 last_cloud_ms; + static constexpr uint32 CloudRotationDelayMs = 15000; +}; + +//class HeiganMeleeTrigger : public Trigger +//{ +//public: +// HeiganMeleeTrigger(PlayerbotAI* ai) : Trigger(ai, "heigan melee") {} +// virtual bool IsActive(); +//}; +// +//class HeiganRangedTrigger : public Trigger +//{ +//public: +// HeiganRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "heigan ranged") {} +// bool IsActive() override; +//}; + +class RazuviousTankTrigger : public Trigger +{ +public: + RazuviousTankTrigger(PlayerbotAI* ai) : Trigger(ai, "instructor razuvious tank"), helper(ai) {} + bool IsActive() override; + +private: + RazuviousBossHelper helper; +}; + +class RazuviousNontankTrigger : public Trigger +{ +public: + RazuviousNontankTrigger(PlayerbotAI* ai) : Trigger(ai, "instructor razuvious non-tank"), helper(ai) {} + bool IsActive() override; + +private: + RazuviousBossHelper helper; +}; + +class KelthuzadTrigger : public Trigger +{ +public: + KelthuzadTrigger(PlayerbotAI* ai) : Trigger(ai, "kel'thuzad trigger"), helper(ai) {} + bool IsActive() override; + +private: + KelthuzadBossHelper helper; +}; + +class AnubrekhanTrigger : public Trigger +{ +public: + AnubrekhanTrigger(PlayerbotAI* ai) : Trigger(ai, "anub'rekhan") {} + bool IsActive() override; +}; + + class FaerlinaTrigger : public Trigger + { + public: + FaerlinaTrigger(PlayerbotAI* ai) : Trigger(ai, "faerlina") {} + bool IsActive() override; + }; + +class MaexxnaTrigger : public Trigger +{ +public: + MaexxnaTrigger(PlayerbotAI* ai) : Trigger(ai, "maexxna") {} + bool IsActive() override; +}; + +//class PatchwerkTankTrigger : public Trigger +//{ +//public: +// PatchwerkTankTrigger(PlayerbotAI* ai) : Trigger(ai, "patchwerk tank") {} +// bool IsActive() override; +//}; +// +//class PatchwerkNonTankTrigger : public Trigger +//{ +//public: +// PatchwerkNonTankTrigger(PlayerbotAI* ai) : Trigger(ai, "patchwerk non-tank") {} +// bool IsActive() override; +//}; +// +//class PatchwerkRangedTrigger : public Trigger +//{ +//public: +// PatchwerkRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "patchwerk ranged") {} +// bool IsActive() override; +//}; + +class ThaddiusPhasePetTrigger : public Trigger +{ +public: + ThaddiusPhasePetTrigger(PlayerbotAI* ai) : Trigger(ai, "thaddius phase pet"), helper(ai) {} + bool IsActive() override; + +private: + ThaddiusBossHelper helper; +}; + +class ThaddiusPhasePetLoseAggroTrigger : public ThaddiusPhasePetTrigger +{ +public: + ThaddiusPhasePetLoseAggroTrigger(PlayerbotAI* ai) : ThaddiusPhasePetTrigger(ai) {} + virtual bool IsActive() + { + Unit* target = AI_VALUE(Unit*, "current target"); + return ThaddiusPhasePetTrigger::IsActive() && botAI->IsTank(bot) && target && target->GetVictim() != bot; + } +}; + +class ThaddiusPhaseTransitionTrigger : public Trigger +{ +public: + ThaddiusPhaseTransitionTrigger(PlayerbotAI* ai) : Trigger(ai, "thaddius phase transition"), helper(ai) {} + bool IsActive() override; + +private: + ThaddiusBossHelper helper; +}; + +class ThaddiusPhaseThaddiusTrigger : public Trigger +{ +public: + ThaddiusPhaseThaddiusTrigger(PlayerbotAI* ai) : Trigger(ai, "thaddius phase thaddius"), helper(ai) {} + bool IsActive() override; + +private: + ThaddiusBossHelper helper; +}; + +class HorsemanAttractorsTrigger : public Trigger +{ +public: + HorsemanAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "fourhorsemen attractors"), helper(ai) {} + bool IsActive() override; + +private: + FourhorsemanBossHelper helper; +}; + +class HorsemanExceptAttractorsTrigger : public Trigger +{ +public: + HorsemanExceptAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "fourhorsemen except attractors"), helper(ai) {} + bool IsActive() override; + +private: + FourhorsemanBossHelper helper; +}; + +class SapphironGroundTrigger : public Trigger +{ +public: + SapphironGroundTrigger(PlayerbotAI* ai) : Trigger(ai, "sapphiron ground"), helper(ai) {} + bool IsActive() override; + +private: + SapphironBossHelper helper; +}; + +class SapphironFlightTrigger : public Trigger +{ +public: + SapphironFlightTrigger(PlayerbotAI* ai) : Trigger(ai, "sapphiron flight"), helper(ai) {} + bool IsActive() override; + +private: + SapphironBossHelper helper; +}; + +class GluthTrigger : public Trigger +{ +public: + GluthTrigger(PlayerbotAI* ai) : Trigger(ai, "gluth trigger"), helper(ai) {} + bool IsActive() override; + +private: + GluthBossHelper helper; +}; + +class GluthMainTankMortalWoundTrigger : public Trigger +{ +public: + GluthMainTankMortalWoundTrigger(PlayerbotAI* ai) : Trigger(ai, "gluth main tank mortal wound trigger"), helper(ai) {} + bool IsActive() override; + +private: + GluthBossHelper helper; +}; + +class LoathebTrigger : public Trigger +{ +public: + LoathebTrigger(PlayerbotAI* ai) : Trigger(ai, "loatheb"), helper(ai) {} + bool IsActive() override; + +private: + LoathebBossHelper helper; +}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h b/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h new file mode 100644 index 00000000..9b87bb58 --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Util/RaidNaxxBossHelper.h @@ -0,0 +1,533 @@ +#ifndef _PLAYERBOT_RAIDNAXXBOSSHELPER_H +#define _PLAYERBOT_RAIDNAXXBOSSHELPER_H + +#include + +#include "AiObject.h" +#include "AiObjectContext.h" +#include "EventMap.h" +#include "Log.h" +#include "NamedObjectContext.h" +#include "ObjectGuid.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "ScriptedCreature.h" +#include "SharedDefines.h" +#include "Spell.h" +#include "Timer.h" +#include "RaidNaxxSpellIds.h" + +const uint32 NAXX_MAP_ID = 533; + +template +class GenericBossHelper : public AiObject +{ +public: + GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} + virtual bool UpdateBossAI() + { + if (!bot->IsInCombat()) + _unit = nullptr; + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + _unit = nullptr; + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", _name); + if (!_unit) + return false; + + _target = _unit->ToCreature(); + if (!_target) + return false; + + _ai = dynamic_cast(_target->GetAI()); + if (!_ai) + return false; + + _event_map = &_ai->events; + if (!_event_map) + return false; + } + if (!_event_map) + return false; + + _timer = getMSTime(); + return true; + } + virtual void Reset() + { + _unit = nullptr; + _target = nullptr; + _ai = nullptr; + _event_map = nullptr; + _timer = 0; + } + +protected: + std::string _name; + Unit* _unit = nullptr; + Creature* _target = nullptr; + BossAiType* _ai = nullptr; + EventMap* _event_map = nullptr; + uint32 _timer = 0; +}; + +class KelthuzadBossHelper : public AiObject +{ +public: + KelthuzadBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + const std::pair center = {3716.19f, -5106.58f}; + const std::pair tank_pos = {3709.19f, -5104.86f}; + const std::pair assist_tank_pos = {3746.05f, -5112.74f}; + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + _unit = AI_VALUE2(Unit*, "find target", "kel'thuzad"); + + return _unit != nullptr; + } + bool IsPhaseOne() { return _unit && _unit->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE); } + bool IsPhaseTwo() { return _unit && !_unit->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE); } + Unit* GetAnyShadowFissure() + { + Unit* shadow_fissure = nullptr; + GuidVector units = *context->GetValue("nearest triggers"); + for (auto i = units.begin(); i != units.end(); i++) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + if (botAI->EqualLowercaseName(unit->GetName(), "shadow fissure")) + shadow_fissure = unit; + } + return shadow_fissure; + } + +private: + void Reset() { _unit = nullptr; } + + Unit* _unit = nullptr; +}; + +class RazuviousBossHelper : public AiObject +{ +public: + RazuviousBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + _unit = AI_VALUE2(Unit*, "find target", "instructor razuvious"); + + return _unit != nullptr; + } + +private: + void Reset() { _unit = nullptr; } + + Unit* _unit = nullptr; +}; + +class SapphironBossHelper : public AiObject +{ +public: + const std::pair mainTankPos = {3512.07f, -5274.06f}; + const std::pair center = {3517.31f, -5253.74f}; + const float GENERIC_HEIGHT = 137.29f; + SapphironBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", "sapphiron"); + if (!_unit) + return false; + } + bool now_flying = _unit->IsFlying(); + if (_was_flying && !now_flying) + _last_land_ms = getMSTime(); + + _was_flying = now_flying; + return true; + } + bool IsPhaseGround() { return _unit && !_unit->IsFlying(); } + bool IsPhaseFlight() { return _unit && _unit->IsFlying(); } + bool JustLanded() + { + if (!_last_land_ms) + return false; + + return getMSTime() - _last_land_ms <= POSITION_TIME_AFTER_LANDED; + } + bool WaitForExplosion() + { + if (!IsPhaseFlight()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && + (NaxxSpellIds::HasAnyAura(botAI, member, {NaxxSpellIds::Icebolt10, NaxxSpellIds::Icebolt25}) || + botAI->HasAura("icebolt", member, false, false, -1, true))) + { + return true; + } + } + return false; + } + bool FindPosToAvoidChill(std::vector& dest) + { + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::Chill25}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("chill", bot); + } + if (!aura) + return false; + + DynamicObject* dyn_obj = aura->GetDynobjOwner(); + if (!dyn_obj) + return false; + + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + float angle = 0; + uint32 index = botAI->GetGroupSlotIndex(bot); + if (currentTarget) + { + if (botAI->IsRanged(bot)) + { + if (bot->GetExactDist2d(currentTarget) <= 45.0f) + angle = bot->GetAngle(dyn_obj) - M_PI + (rand_norm() - 0.5) * M_PI / 2; + else + { + if (index % 2 == 0) + angle = bot->GetAngle(currentTarget) + M_PI / 2; + else + angle = bot->GetAngle(currentTarget) - M_PI / 2; + } + } + else + { + if (index % 3 == 0) + angle = bot->GetAngle(currentTarget); + else if (index % 3 == 1) + angle = bot->GetAngle(currentTarget) + M_PI / 2; + else + angle = bot->GetAngle(currentTarget) - M_PI / 2; + } + } + else + angle = bot->GetAngle(dyn_obj) - M_PI + (rand_norm() - 0.5) * M_PI / 2; + + dest = {bot->GetPositionX() + cos(angle) * 5.0f, bot->GetPositionY() + sin(angle) * 5.0f, bot->GetPositionZ()}; + return true; + } + +private: + void Reset() + { + _unit = nullptr; + _was_flying = false; + _last_land_ms = 0; + } + + const uint32 POSITION_TIME_AFTER_LANDED = 5000; + Unit* _unit = nullptr; + bool _was_flying = false; + uint32 _last_land_ms = 0; +}; + +class GluthBossHelper : public AiObject +{ +public: + const std::pair mainTankPos25 = {3331.48f, -3109.06f}; + const std::pair mainTankPos10 = {3278.29f, -3162.06f}; + const std::pair beforeDecimatePos = {3267.34f, -3175.68f}; + const std::pair leftSlowDownPos = {3290.68f, -3141.65f}; + const std::pair rightSlowDownPos = {3300.78f, -3151.98f}; + const std::pair rangedPos = {3301.45f, -3139.29f}; + const std::pair healPos = {3303.09f, -3135.24f}; + + const float decimatedZombiePct = 10.0f; + GluthBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", "gluth"); + if (!_unit) + return false; + } + if (_unit->IsInCombat()) + { + if (_combat_start_ms == 0) + _combat_start_ms = getMSTime(); + } + else + _combat_start_ms = 0; + + return true; + } + bool BeforeDecimate() + { + if (!_unit || !_unit->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* spell = _unit->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell) + spell = _unit->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + + if (!spell) + return false; + + SpellInfo const* info = spell->GetSpellInfo(); + if (!info) + return false; + + if (NaxxSpellIds::MatchesAnySpellId( + info, {NaxxSpellIds::Decimate10, NaxxSpellIds::Decimate25, NaxxSpellIds::Decimate25Alt})) + return true; + + // Fallback to name for custom spell data. + return info->SpellName[LOCALE_enUS] && botAI->EqualLowercaseName(info->SpellName[LOCALE_enUS], "decimate"); + } + bool JustStartCombat() const { return _combat_start_ms != 0 && getMSTime() - _combat_start_ms < 10000; } + bool IsZombieChow(Unit* unit) const { return unit && botAI->EqualLowercaseName(unit->GetName(), "zombie chow"); } + +private: + void Reset() + { + _unit = nullptr; + _combat_start_ms = 0; + } + + Unit* _unit = nullptr; + uint32 _combat_start_ms = 0; +}; + +class LoathebBossHelper : public AiObject +{ +public: + const std::pair mainTankPos = {2877.57f, -3967.00f}; + const std::pair rangePos = {2896.96f, -3980.61f}; + LoathebBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + _unit = AI_VALUE2(Unit*, "find target", "loatheb"); + + return _unit != nullptr; + } + +private: + void Reset() { _unit = nullptr; } + + Unit* _unit = nullptr; +}; + +class FourhorsemanBossHelper : public AiObject +{ +public: + const float posZ = 241.27f; + const std::pair attractPos[2] = {{2502.03f, -2910.90f}, + {2484.61f, -2947.07f}}; // left (sir zeliek), right (lady blaumeux) + FourhorsemanBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + else if (_combat_start_ms == 0) + _combat_start_ms = getMSTime(); + + if (_sir && (!_sir->IsInWorld() || !_sir->IsAlive())) + Reset(); + + if (!_sir) + { + _sir = AI_VALUE2(Unit*, "find target", "sir zeliek"); + if (!_sir) + return false; + } + _lady = AI_VALUE2(Unit*, "find target", "lady blaumeux"); + return true; + } + void Reset() + { + _sir = nullptr; + _lady = nullptr; + _combat_start_ms = 0; + posToGo = 0; + } + bool IsAttracter(Player* bot) + { + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_25MAN_NORMAL) + { + return botAI->IsAssistRangedDpsOfIndex(bot, 0) || botAI->IsAssistHealOfIndex(bot, 0) || + botAI->IsAssistHealOfIndex(bot, 1) || botAI->IsAssistHealOfIndex(bot, 2); + } + return botAI->IsAssistRangedDpsOfIndex(bot, 0) || botAI->IsAssistHealOfIndex(bot, 0); + } + void CalculatePosToGo(Player* bot) + { + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + Unit* lady = _lady; + if (!lady) + posToGo = 0; + else + { + uint32 elapsed_ms = _combat_start_ms ? getMSTime() - _combat_start_ms : 0; + // Interval: 24s - 15s - 15s - ... + posToGo = !(elapsed_ms <= 9000 || ((elapsed_ms - 9000) / 67500) % 2 == 0); + if (botAI->IsAssistRangedDpsOfIndex(bot, 0) || (raid25 && botAI->IsAssistHealOfIndex(bot, 1))) + posToGo = 1 - posToGo; + } + } + std::pair CurrentAttractPos() + { + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + float posX = attractPos[posToGo].first, posY = attractPos[posToGo].second; + if (posToGo == 1) + { + float offset_x = 0.0f; + float offset_y = 0.0f; + float bias = 4.5f; + if (raid25) + { + offset_x = -bias; + offset_y = bias; + } + posX += offset_x; + posY += offset_y; + } + return {posX, posY}; + } + Unit* CurrentAttackTarget() + { + if (posToGo == 0) + return _sir; + + return _lady; + } + +protected: + Unit* _sir = nullptr; + Unit* _lady = nullptr; + uint32 _combat_start_ms = 0; + int posToGo = 0; +}; +class ThaddiusBossHelper : public AiObject +{ +public: + const std::pair tankPosFeugen = {3522.94f, -3002.60f}; + const std::pair tankPosStalagg = {3436.14f, -2919.98f}; + const std::pair rangedPosFeugen = {3500.45f, -2997.92f}; + const std::pair rangedPosStalagg = {3441.01f, -2942.04f}; + const float tankPosZ = 312.61f; + ThaddiusBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", "thaddius"); + if (!_unit) + return false; + } + feugen = AI_VALUE2(Unit*, "find target", "feugen"); + stalagg = AI_VALUE2(Unit*, "find target", "stalagg"); + return true; + } + bool IsPhasePet() { return (feugen && feugen->IsAlive()) || (stalagg && stalagg->IsAlive()); } + bool IsPhaseTransition() + { + if (IsPhasePet()) + return false; + + return _unit && _unit->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE); + } + bool IsPhaseThaddius() { return !IsPhasePet() && !IsPhaseTransition(); } + Unit* GetNearestPet() + { + Unit* unit = nullptr; + if (feugen && feugen->IsAlive()) + unit = feugen; + + if (stalagg && stalagg->IsAlive() && (!feugen || bot->GetDistance(stalagg) < bot->GetDistance(feugen))) + unit = stalagg; + + return unit; + } + std::pair PetPhaseGetPosForTank() + { + if (GetNearestPet() == feugen) + return tankPosFeugen; + + return tankPosStalagg; + } + std::pair PetPhaseGetPosForRanged() + { + if (GetNearestPet() == feugen) + return rangedPosFeugen; + + return rangedPosStalagg; + } + +protected: + void Reset() + { + _unit = nullptr; + feugen = nullptr; + stalagg = nullptr; + } + + Unit* _unit = nullptr; + Unit* feugen = nullptr; + Unit* stalagg = nullptr; +}; + +#endif diff --git a/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h b/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h new file mode 100644 index 00000000..53299e7e --- /dev/null +++ b/src/Ai/Raid/Naxxramas/Util/RaidNaxxSpellIds.h @@ -0,0 +1,165 @@ +#ifndef _PLAYERBOT_RAIDNAXXSPELLIDS_H +#define _PLAYERBOT_RAIDNAXXSPELLIDS_H + +#include + +#include "PlayerbotAI.h" + +// use src/server/scripts/Northrend/Naxxramas/naxxramas.h for CreatureId, NaxxramasSay, NaxxramasEvent, NaxxramasMisc +namespace NaxxSpellIds +{ + // Heigan + static constexpr uint32 Eruption10 = 29371; +/* + SPELL_SPELL_DISRUPTION = 29310, + SPELL_DECREPIT_FEVER = 29998, + SPELL_PLAGUE_CLOUD = 29350, + SPELL_TELEPORT_SELF = 30211 +*/ + + // Grobbulus + static constexpr uint32 PoisonCloud = 28240; + + // Thaddius polarity + static constexpr uint32 PositiveCharge10 = 28059; + static constexpr uint32 PositiveCharge25 = 28062; + static constexpr uint32 PositiveChargeStack = 29659; + static constexpr uint32 NegativeCharge10 = 28084; + static constexpr uint32 NegativeCharge25 = 28085; + static constexpr uint32 NegativeChargeStack = 29660; +/* + SPELL_MAGNETIC_PULL = 28337, + SPELL_TESLA_SHOCK = 28099, + SPELL_SHOCK_VISUAL = 28159, + + // Stalagg + SPELL_POWER_SURGE = 54529, + SPELL_STALAGG_CHAIN = 28096, + + // Feugen + SPELL_STATIC_FIELD = 28135, + SPELL_FEUGEN_CHAIN = 28111, + + // Thaddius + SPELL_POLARITY_SHIFT = 28089, + SPELL_BALL_LIGHTNING = 28299, + SPELL_CHAIN_LIGHTNING = 28167, + SPELL_BERSERK = 27680, + SPELL_THADDIUS_VISUAL_LIGHTNING = 28136, + SPELL_THADDIUS_SPAWN_STUN = 28160, + + SPELL_POSITIVE_CHARGE = 28062, + SPELL_POSITIVE_CHARGE_STACK = 29659, + SPELL_NEGATIVE_CHARGE = 28085, + SPELL_NEGATIVE_CHARGE_STACK = 29660, + SPELL_POSITIVE_POLARITY = 28059, + SPELL_NEGATIVE_POLARITY = 28084 +*/ + // Sapphiron + static constexpr uint32 Icebolt10 = 28522; + static constexpr uint32 Icebolt25 = 28526; + static constexpr uint32 Chill25 = 55699; +/* + // Fight + SPELL_FROST_AURA = 28531, + SPELL_CLEAVE = 19983, + SPELL_TAIL_SWEEP = 55697, + SPELL_SUMMON_BLIZZARD = 28560, + SPELL_LIFE_DRAIN = 28542, + SPELL_BERSERK = 26662, + + // Ice block + SPELL_ICEBOLT_CAST = 28526, + SPELL_ICEBOLT_TRIGGER = 28522, + SPELL_FROST_MISSILE = 30101, + SPELL_FROST_EXPLOSION = 28524, + + // Visuals + SPELL_SAPPHIRON_DIES = 29357 +*/ + // Gluth + static constexpr uint32 Decimate10 = 28374; + static constexpr uint32 Decimate25 = 54426; + static constexpr uint32 Decimate25Alt = 28375; + static constexpr uint32 MortalWound10 = 25646; + static constexpr uint32 MortalWound25 = 54378; +/* + SPELL_MORTAL_WOUND = 25646, + SPELL_ENRAGE = 28371, + SPELL_DECIMATE = 28374, + SPELL_DECIMATE_DAMAGE = 28375, + SPELL_BERSERK = 26662, + SPELL_INFECTED_WOUND = 29306, + SPELL_CHOW_SEARCHER = 28404 +*/ + // Anub'Rekhan + static constexpr uint32 LocustSwarm10 = 28785; + static constexpr uint32 LocustSwarm10Alt = 28786; + static constexpr uint32 LocustSwarm25 = 54021; // 25-man Locust Swarm +/* + SPELL_IMPALE = 28783, + SPELL_LOCUST_SWARM = 28785, + SPELL_SUMMON_CORPSE_SCARABS_5 = 29105, + SPELL_SUMMON_CORPSE_SCARABS_10 = 28864, + SPELL_BERSERK = 26662 + ACHIEV_TIMED_START_EVENT = 9891, + EVENT_SPAWN_CRYPT_GUARDS_1 = 0, + EVENT_BERSERK = 1, + //// + Position const cryptguardPositions[] = { + { 3299.732f, -3502.489f, 287.077f, 2.378f }, + { 3299.086f, -3450.929f, 287.077f, 3.999f }, + { 3331.217f, -3476.607f, 287.074f, 3.269f } +}; + +*/ + // Loatheb + static constexpr uint32 NecroticAura10 = 55593; +/* + SPELL_NECROTIC_AURA = 55593, + SPELL_SUMMON_SPORE = 29234, + SPELL_DEATHBLOOM = 29865, + SPELL_INEVITABLE_DOOM = 29204, + SPELL_BERSERK = 26662 +*/ + inline bool HasAnyAura(PlayerbotAI* botAI, Unit* unit, std::initializer_list spellIds) + { + if (!botAI || !unit) + return false; + + for (uint32 spellId : spellIds) + { + if (botAI->HasAura(spellId, unit)) + return true; + } + return false; + } + + inline Aura* GetAnyAura(Unit* unit, std::initializer_list spellIds) + { + if (!unit) + return nullptr; + + for (uint32 spellId : spellIds) + { + if (Aura* aura = unit->GetAura(spellId)) + return aura; + } + return nullptr; + } + + inline bool MatchesAnySpellId(SpellInfo const* info, std::initializer_list spellIds) + { + if (!info) + return false; + + for (uint32 spellId : spellIds) + { + if (info->Id == spellId) + return true; + } + return false; + } +} // namespace NaxxSpellIds + +#endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 4d87c5ac..d117c459 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -8,6 +8,7 @@ #include "RaidKarazhanStrategy.h" #include "RaidGruulsLairStrategy.h" #include "RaidMagtheridonStrategy.h" +#include "RaidNaxxStrategy.h" #include "RaidSSCStrategy.h" #include "RaidTempestKeepStrategy.h" #include "RaidOsStrategy.h" @@ -28,6 +29,7 @@ public: creators["karazhan"] = &RaidStrategyContext::karazhan; creators["gruulslair"] = &RaidStrategyContext::gruulslair; creators["magtheridon"] = &RaidStrategyContext::magtheridon; + creators["naxx"] = &RaidStrategyContext::naxx; creators["ssc"] = &RaidStrategyContext::ssc; creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; @@ -45,6 +47,7 @@ private: static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } + static Strategy* naxx(PlayerbotAI* botAI) { return new RaidNaxxStrategy(botAI); } static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index ff54b971..e6333708 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -41,6 +41,8 @@ #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" #include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" #include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" +#include "Ai/Raid/Naxxramas/RaidNaxxActionContext.h" +#include "Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" #include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" #include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" @@ -119,6 +121,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList Date: Fri, 6 Mar 2026 09:58:02 -0600 Subject: [PATCH 03/13] Some simple improvements to Karazhan strategies (#2173) # Pull Request I've made a few simple changes to the Karazhan strategies that should result in notable improvements in game. - **Attumen**: I was using a GetExactDist2d() check for phase 2 when bots stack behind him. That resulted in ranged bots being too close to attack. It's now switched to the correct GetDistance2d() check to account for the hitbox. - **Maiden of Virtue**: The tank continuously ran side-to-side when trying to tank her because it was trying to turn the boss with TankFaceAction but not being able to due to being required to be within a short distance of a set waypoint. I didn't understand the cause when I was originally working on Karazhan. To fix this, a new multiplier disables CombatFormationMoveAction (the "co+ disperse" strategy) and its inherited classes, except for SetBehindTargetAction. The only other class that inherits from CombatFormationMoveAction is TankFaceAction. I disabled the parent class also because the ranged bots have a coded positioning strategy and should not observe the co+ disperse strategy. - **The Curator**: Same deal as Maiden with a new multiplier. - **Nightbane**: Same deal as Maiden with a new multiplier. - **Malchezaar**: Infernal avoidance for non-enfeebled bots had movement priority set to MOVEMENT_FORCED. This was not good because it made bots refuse to cross Hellfire so if you got unlucky, they could be stuck on the other side of an Infernal from the boss and completely out of the fight. MOVEMENT_FORCED needs to be reserved for situations in which the bot absolutely cannot step in the AoE at all, and that's not the case for non-Enfeebled bots here. Priority is now changed to MOVEMENT_COMBAT. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? No additional complication in logic from these changes, and additional performance impact is exceedingly small (just a few more multipliers with inexpensive checks that would apply only in Karazhan). --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it Should be straightforward. Engage the above-mentioned bosses in Karazhan and observe the mechanics. I did test all of them. ## Complexity & Impact Does this change add new decision branches? - - [X] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [ ] No - - [X] Yes (**describe and justify impact**) Barely due to the additional multipliers. Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [X] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [X] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [X] No - - [ ] Yes (**explain below**) --- ## Final Checklist - - [X] Stability is not compromised - - [X] Performance impact is understood, tested, and acceptable - - [X] Added logic complexity is justified and explained - - [X] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision --- .../Karazhan/Action/RaidKarazhanActions.cpp | 8 ++-- .../Multiplier/RaidKarazhanMultipliers.cpp | 40 ++++++++++++++----- .../Multiplier/RaidKarazhanMultipliers.h | 16 ++++++++ .../Strategy/RaidKarazhanStrategy.cpp | 2 + .../Raid/Karazhan/Util/RaidKarazhanHelpers.h | 1 - 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp index 8f7f5f52..4e175f0f 100644 --- a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp +++ b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp @@ -116,9 +116,9 @@ bool AttumenTheHuntsmanStackBehindAction::Execute(Event /*event*/) float rearX = attumenMounted->GetPositionX() + std::cos(orientation) * distanceBehind; float rearY = attumenMounted->GetPositionY() + std::sin(orientation) * distanceBehind; - if (bot->GetExactDist2d(rearX, rearY) > 1.0f) + if (bot->GetDistance2d(rearX, rearY) > 1.0f) { - return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, attumenMounted->GetPositionZ(), false, false, false, false, + return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } @@ -1178,7 +1178,7 @@ bool PrinceMalchezaarNonTankAvoidInfernalAction::Execute(Event /*event*/) bot->AttackStop(); bot->InterruptNonMeleeSpells(true); return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); + MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -1244,7 +1244,7 @@ bool PrinceMalchezaarMainTankMovementAction::Execute(Event /*event*/) { bot->AttackStop(); return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); + MovementPriority::MOVEMENT_COMBAT, true, true); } } diff --git a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp index 06a3335c..e68a3885 100644 --- a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp +++ b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp @@ -80,6 +80,19 @@ float AttumenTheHuntsmanWaitForDpsMultiplier::GetValue(Action* action) return 1.0f; } +// Disables co +disperse and co +tank face +float MaidenOfVirtueDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "maiden of virtue")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + // The assist tank should stay on the boss to be 2nd on aggro and tank Hateful Bolts float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) { @@ -93,6 +106,19 @@ float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) return 1.0f; } +// Disables co +disperse and co +tank face +float TheCuratorDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "the curator")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + // Save Bloodlust/Heroism for Evocation (100% increased damage) float TheCuratorDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) { @@ -350,17 +376,11 @@ float NightbaneDisableMovementMultiplier::GetValue(Action* action) if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - - // Disable CombatFormationMoveAction for all bots except: - // (1) main tank and (2) only during the ground phase, other melee - if (botAI->IsRanged(bot) || - (botAI->IsMelee(bot) && !botAI->IsMainTank(bot) && - nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z)) + dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) { - if (dynamic_cast(action)) - return 0.0f; + return 0.0f; } return 1.0f; diff --git a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h index 8ad5a1d4..23f5617d 100644 --- a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h +++ b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h @@ -27,6 +27,14 @@ public: virtual float GetValue(Action* action); }; +class MaidenOfVirtueDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + MaidenOfVirtueDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "maiden of virtue disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + class TheCuratorDisableTankAssistMultiplier : public Multiplier { public: @@ -35,6 +43,14 @@ public: virtual float GetValue(Action* action); }; +class TheCuratorDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + TheCuratorDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + class TheCuratorDelayBloodlustAndHeroismMultiplier : public Multiplier { public: diff --git a/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp b/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp index 04578f08..d9bbf816 100644 --- a/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp +++ b/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp @@ -146,7 +146,9 @@ void RaidKarazhanStrategy::InitMultipliers(std::vector& multipliers multipliers.push_back(new AttumenTheHuntsmanDisableTankAssistMultiplier(botAI)); multipliers.push_back(new AttumenTheHuntsmanStayStackedMultiplier(botAI)); multipliers.push_back(new AttumenTheHuntsmanWaitForDpsMultiplier(botAI)); + multipliers.push_back(new MaidenOfVirtueDisableCombatFormationMoveMultiplier(botAI)); multipliers.push_back(new TheCuratorDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new TheCuratorDisableCombatFormationMoveMultiplier(botAI)); multipliers.push_back(new TheCuratorDelayBloodlustAndHeroismMultiplier(botAI)); multipliers.push_back(new ShadeOfAranArcaneExplosionDisableChargeMultiplier(botAI)); multipliers.push_back(new ShadeOfAranFlameWreathDisableMovementMultiplier(botAI)); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h index ad7e8c3d..885af774 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h @@ -62,7 +62,6 @@ namespace KarazhanHelpers NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, // Terestian Illhoof - NPC_TERESTIAN_ILLHOOF = 15688, NPC_DEMON_CHAINS = 17248, NPC_KILREK = 17229, From ed81a43403f5442a09c5d0861e703d9d94d858e6 Mon Sep 17 00:00:00 2001 From: Rikus Louw Date: Fri, 6 Mar 2026 17:58:32 +0200 Subject: [PATCH 04/13] Added all TBC attunement quests (#2179) # Pull Request Added all TBC attunement quests to conf --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change Run maintenance on bots - Any required setup (e.g. multiple players, bots, specific configuration) This only applies to Individual Progression mod, since attunements aren't required in base AC - Expected behavior and how to verify it Bots should be able to enter: - The Eye (Tempest Keep) - Mount Hyjal - Black Temple ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) All attunements for TBC are now added on 'maintenance' command If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- conf/playerbots.conf.dist | 17 +++++++++++++++-- src/PlayerbotAIConfig.cpp | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index ede73583..0f957262 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -566,7 +566,10 @@ AiPlayerbot.AutoGearScoreLimit = 0 # Default: food, taxi, and raid are enabled AiPlayerbot.BotCheats = "food,taxi,raid" -# Attunement quests (comma-separated list of quest IDs) +# List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. +# While mod-playerbots does not restore removed attunement requirements, although other mods, such as mod-individual-progression, may do so. +# This is meant to exclude bots from such requirements. +# # Default: # Caverns of Time - Part 1 # - 10279, To The Master's Lair @@ -592,7 +595,17 @@ AiPlayerbot.BotCheats = "food,taxi,raid" # # Serpentshrine Cavern # - 10901, The Cudgel of Kar'desh -AiPlayerbot.AttunementQuests = 10279,10277,10282,10283,10284,10285,10296,10297,10298,11481,11482,11488,11490,11492,10901 +# +# The Eye +# - 10888, Trial of the Naaru: Magtheridon +# +# Mount Hyjal +# - 10445, The Vials of Eternity +# +# Black Temple +# - 10985, A Distraction for Akama +# +AiPlayerbot.AttunementQuests = 10279,10277,10282,10283,10284,10285,10296,10297,10298,11481,11482,11488,11490,11492,10901,10888,10445,10985 # # diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 356098d3..45551ab1 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -180,7 +180,7 @@ 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"), + 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 935252fcfe7c56c317d774030b55299b985a434e Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 6 Mar 2026 16:58:47 +0100 Subject: [PATCH 05/13] Action trigger fix (#2180) Maintenance PR unrelated with module itself Modified action trigger to cover branch change --- .github/workflows/check_pr_source.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check_pr_source.yml b/.github/workflows/check_pr_source.yml index 5b354f6f..82301983 100644 --- a/.github/workflows/check_pr_source.yml +++ b/.github/workflows/check_pr_source.yml @@ -2,6 +2,7 @@ name: Enforce test-staging → master on: pull_request: + types: [opened, synchronize, reopened, edited] branches: - master - test-staging From ca9f23a8e39fff4fbc80dec31577ac5d08916a0a Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 6 Mar 2026 08:59:36 -0700 Subject: [PATCH 06/13] Fix/Defensive: Prevent division by zero in MovementActions (#2185) Added a check to prevent division by zero for orphaned raid groups. # Pull Request If a bots somehow ends up alone in a raid group, this can divide by zero and freeze the server. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? This is the simplest and cheapest way to implement this fix. --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it The fix is a self-evident defensive measure. ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Core dump logs analysis to find this problem. If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- src/Ai/Base/Actions/MovementActions.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 6769a16c..e1b8f636 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -854,6 +854,11 @@ float MovementAction::GetFollowAngle() if (!group) return 0.0f; + // Prevent bots with orphaned raid groups from dividing by 0, which freezes the server. + uint32 memberCount = group->GetMembersCount(); + if (memberCount <= 1) + return 0.0f; + uint32 index = 1; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -861,7 +866,7 @@ float MovementAction::GetFollowAngle() continue; if (ref->GetSource() == bot) - return 2 * M_PI / (group->GetMembersCount() - 1) * index; + return 2 * M_PI / (memberCount - 1) * index; ++index; } From 788c7b025b75a49c89e196f5c67ed4978c453927 Mon Sep 17 00:00:00 2001 From: Hokken Date: Fri, 6 Mar 2026 17:41:40 +0000 Subject: [PATCH 07/13] Fix quest links triggering trade window (#2155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `ChatHelper::parseable()` matched any hyperlink containing `|H`, including quest links (`|Hquest:`), achievement links, spell links, etc. This caused bots to interpret quest links shared in party chat as item trade requests, opening the trade window instead of ignoring them. Narrowed the check from `"|H"` to `"|Hitem:"` so only actual item links trigger the parseable/trade logic. **One-line change** in `src/Bot/Cmd/ChatHelper.cpp:603` ## Root Cause The WoW client uses `|H:|h[Name]|h` hyperlinks for many object types: - `|Hitem:12345|h[Item Name]|h` — items - `|Hquest:678|h[Quest Name]|h` — quests - `|Hspell:890|h[Spell Name]|h` — spells - `|Hachievement:...|h` — achievements The old check `text.find("|H")` matched ALL of these, so sharing a quest link in party chat would cause the bot to enter the item parsing/trade flow. ## Test Scenarios | Scenario | Before | After | |----------|--------|-------| | Share `[Quest Name]` in party chat | Trade window opens | No reaction (correct) | | Share `[Item Name]` in party chat | Trade window opens | Trade window opens (unchanged) | | Say "questitem" in chat | Parsed correctly | Parsed correctly (unchanged) | | Share `[Spell Name]` in party chat | Trade window opens | No reaction (correct) | Tested on AzerothCore 3.3.5a with mod-playerbots, confirmed fix resolves the issue. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Hokken Co-authored-by: Claude Opus 4.6 --- src/Bot/Cmd/ChatHelper.cpp | 4 ++-- src/Bot/Cmd/ChatHelper.h | 2 +- src/Bot/Engine/ExternalEventHelper.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bot/Cmd/ChatHelper.cpp b/src/Bot/Cmd/ChatHelper.cpp index bf288747..fb6002a1 100644 --- a/src/Bot/Cmd/ChatHelper.cpp +++ b/src/Bot/Cmd/ChatHelper.cpp @@ -598,9 +598,9 @@ uint32 ChatHelper::parseSlot(std::string const text) return EQUIPMENT_SLOT_END; } -bool ChatHelper::parseable(std::string const text) +bool ChatHelper::parseableItem(std::string const text) { - return text.find("|H") != std::string::npos || text == "questitem" || text == "ammo" || + return text.find("|Hitem:") != std::string::npos || text == "questitem" || text == "ammo" || substrContainsInMap(text, consumableSubClasses) || substrContainsInMap(text, tradeSubClasses) || substrContainsInMap(text, itemQualities) || substrContainsInMap(text, slots) || substrContainsInMap(text, chats) || diff --git a/src/Bot/Cmd/ChatHelper.h b/src/Bot/Cmd/ChatHelper.h index 562f2307..5069fcb5 100644 --- a/src/Bot/Cmd/ChatHelper.h +++ b/src/Bot/Cmd/ChatHelper.h @@ -66,7 +66,7 @@ public: static uint32 parseSlot(std::string const text); uint32 parseSkill(std::string const text); - static bool parseable(std::string const text); + static bool parseableItem(std::string const text); void eraseAllSubStr(std::string& mainStr, std::string const toErase); diff --git a/src/Bot/Engine/ExternalEventHelper.cpp b/src/Bot/Engine/ExternalEventHelper.cpp index 2f42eee9..3a62fbda 100644 --- a/src/Bot/Engine/ExternalEventHelper.cpp +++ b/src/Bot/Engine/ExternalEventHelper.cpp @@ -30,7 +30,7 @@ bool ExternalEventHelper::ParseChatCommand(std::string const command, Player* ow return true; } - if (!ChatHelper::parseable(command)) + if (!ChatHelper::parseableItem(command)) return false; HandleCommand("c", command, owner); From 14c77b1e7b0232aec0bd7c28a89bf1641c51c8d1 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 6 Mar 2026 12:50:00 -0600 Subject: [PATCH 08/13] Remove instance strategies when leaving map (#2163) # Pull Request Currently, dungeon and raid strategies, which are automatically added when entering the applicable instance (unless disabled in config), will persist until manually removed or until a different instance strategy is applied. This is pretty bad because then bots will continue to check triggers for the instance when outside of it. This has been discussed for a long time, but after finally considering it today, I think the solution is pretty simple because the existing framework is already there. PlayerbotAI::ApplyInstanceStrategies() is the function for enabling strategies when entering an instance, and it's called whenever a bot changes maps. So all we need to do is to remove all instance strategies first when calling it. I tested these changes, and they worked for me, but obviously others should test too, and especially the code should be examined since that is not my area of expertise. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. I used Gemini to verify that my idea would work and had it put together the actual code for me. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/PlayerbotAI.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 7c4c6f15..c0e679f3 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1532,6 +1532,21 @@ std::vector PlayerbotAI::GetStrategies(BotState type) void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) { + static const std::vector allInstanceStrategies = + { + "aq20", "bwl", "karazhan", "gruulslair", "icc", "magtheridon", "moltencore", + "naxx", "onyxia", "ssc", "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" + }; + + for (const std::string& strat : allInstanceStrategies) + { + engines[BOT_STATE_COMBAT]->removeStrategy(strat); + engines[BOT_STATE_NON_COMBAT]->removeStrategy(strat); + } + std::string strategyName; switch (mapId) { @@ -1631,10 +1646,13 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) default: break; } + if (strategyName.empty()) return; + engines[BOT_STATE_COMBAT]->addStrategy(strategyName); engines[BOT_STATE_NON_COMBAT]->addStrategy(strategyName); + if (tellMaster && !strategyName.empty()) { std::ostringstream out; From 660a5c0543f111678136b194d0586cd31eeaa717 Mon Sep 17 00:00:00 2001 From: killerzwelch <40989785+killerzwelch@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:03:32 +0100 Subject: [PATCH 09/13] make playerbots compatible with 515aeca (#2181) # Pull Request needed changes for https://github.com/azerothcore/azerothcore-wotlk/commit/515aeca570e879a7fa12a1773436a2a0597979b2 --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/Factory/RandomPlayerbotFactory.cpp | 3 ++- src/Bot/RandomPlayerbotMgr.cpp | 9 +++++---- src/Mgr/Travel/TravelMgr.cpp | 3 ++- src/Mgr/Travel/TravelNode.cpp | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Bot/Factory/RandomPlayerbotFactory.cpp b/src/Bot/Factory/RandomPlayerbotFactory.cpp index 908bd389..617e4006 100644 --- a/src/Bot/Factory/RandomPlayerbotFactory.cpp +++ b/src/Bot/Factory/RandomPlayerbotFactory.cpp @@ -9,6 +9,7 @@ #include "ArenaTeamMgr.h" #include "DatabaseEnv.h" #include "PlayerbotAI.h" +#include "RaceMgr.h" #include "ScriptMgr.h" #include "SharedDefines.h" #include "SocialMgr.h" @@ -60,7 +61,7 @@ Player* RandomPlayerbotFactory::CreateRandomBot(WorldSession* session, uint8 cls const bool alliance = static_cast(urand(0, 1)); std::vector raceOptions; - for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race) { // skip disabled with config races if ((1 << (race - 1)) & sWorld->getIntConfig(CONFIG_CHARACTER_CREATING_DISABLED_RACEMASK)) diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index 62f3c7c6..bc1ffaad 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -37,6 +37,7 @@ #include "PlayerbotFactory.h" #include "Playerbots.h" #include "Position.h" +#include "RaceMgr.h" #include "Random.h" #include "RandomPlayerbotFactory.h" #include "ServerFacade.h" @@ -1995,7 +1996,7 @@ void RandomPlayerbotMgr::PrepareTeleportCache() } // add all initial position - for (uint32 i = 1; i < MAX_RACES; i++) + for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) { for (uint32 j = 1; j < MAX_CLASSES; j++) { @@ -2008,7 +2009,7 @@ void RandomPlayerbotMgr::PrepareTeleportCache() for (int32 l = 1; l <= 5; l++) { - if ((1 << (i - 1)) & RACEMASK_ALLIANCE) + if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) allianceStarterPerLevelCache[(uint8)l].push_back(pos); else hordeStarterPerLevelCache[(uint8)l].push_back(pos); @@ -3126,7 +3127,7 @@ void RandomPlayerbotMgr::PrintStats() std::map lvlPerRace; std::map lvlPerClass; - for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race) { perRace[race] = 0; lvlPerRace[race] = 0; @@ -3273,7 +3274,7 @@ void RandomPlayerbotMgr::PrintStats() } LOG_INFO("playerbots", "Bots race:"); - for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race) { if (perRace[race]) { diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index be17e28b..703cca0c 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -14,6 +14,7 @@ #include "MapMgr.h" #include "PathGenerator.h" #include "Playerbots.h" +#include "RaceMgr.h" #include "TransportMgr.h" #include "VMapFactory.h" #include "VMapMgr2.h" @@ -3335,7 +3336,7 @@ void TravelMgr::LoadQuestTravelTable() std::ostringstream out; - for (uint8 race = RACE_HUMAN; race < MAX_RACES; race++) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); race++) { for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls) { diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index bdf9cc95..100fdd15 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -11,6 +11,7 @@ #include "BudgetValues.h" #include "PathGenerator.h" #include "Playerbots.h" +#include "RaceMgr.h" #include "ServerFacade.h" #include "TransportMgr.h" @@ -1660,7 +1661,7 @@ void TravelNodeMap::generateStartNodes() startNames[RACE_GNOME] = "Dwarf and Gnome"; startNames[RACE_TROLL] = "Orc and Troll"; - for (uint32 i = 0; i < MAX_RACES; i++) + for (uint32 i = 0; i < sRaceMgr->GetMaxRaces(); i++) { for (uint32 j = 0; j < MAX_CLASSES; j++) { From 55708f397a7e63e13a06c101f4102d6cf8243f7c Mon Sep 17 00:00:00 2001 From: St0ny <42348051+Raz0r1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:03:44 +0100 Subject: [PATCH 10/13] add sql update (#2137) Correction of a spelling mistake in the German chatter-texts. # Pull Request There is an error in the German translation of the chatter text. This will be fixed with this PR. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [X] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [X] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [X] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [X] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [X] Stability is not compromised - - [X] Performance impact is understood, tested, and acceptable - - [X] Added logic complexity is justified and explained - - [X] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql diff --git a/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql b/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql new file mode 100644 index 00000000..90bf69a0 --- /dev/null +++ b/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql @@ -0,0 +1 @@ +UPDATE `ai_playerbot_texts` SET `text_loc3`='%s, du hörst den triefenden Sarkasmus in meinem text nicht' WHERE `id`=1353; From 1a3468368d37b3220369b6382bcfd452ed697637 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:04:37 -0700 Subject: [PATCH 11/13] PR template proposal, using the proposed template itself (#2170) ## Pull Request Description The addition of a PR template last month was a great idea. We had nothing before and just let people type whatever they thought was relevant. Some wrote a whole article with too many details, and some just wrote the title and didn't explain any of the important details. So the addition of the PR template makes sure contributors know what's most important to this project. However, several people thought the template was... a lot. A lot of the information it showed, while useful to the contributor, made it a bit confusing to reviewer to know what the contributor wrote, and what is part of the PR template, so a lot of these guidelines have now been put behind ``. Moreover, even what has been hidden, has been truncated. The main message of the guidelines is stability is our top priority. It is a critical message, but it was repeated several more times than it should've been. Less is more here, and if an important message is repeated in a verbose manner, people would gloss over it like scrolling down long terms and conditions. The questions were also made more concise and explicit. We don't want contributors to question the questions themselves and how do they even apply to their code. The process of PR submission itself should not feel like submitting a long bureaucratic form. Think of it like a scientific paper abstract: It gives a reasonably short summary explaining the work, as clearly as possible. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. These are the core questions that are important to know, but even then, not always relevant. So a note was added to the contributor that they can obviously skip these if their PR is something like a comment edit or whatever else that clearly doesn't add processing. ## How to Test the Changes You are already testing whether or not this template is effective by looking at it. 1. See the"[Preview](https://github.com/NoxMax/mod-playerbots/blob/PR-template-proposal/PULL_REQUEST_TEMPLATE.md)" of the file just so it's clear what the template is like without any of the comments I made here. 2. See the "[Code](https://github.com/NoxMax/mod-playerbots/blob/PR-template-proposal/PULL_REQUEST_TEMPLATE.md?plain=1)" section of the template to see how it would actually look to a contributor. The only difference is the alignment of the translation table; it looks weird in the .md file, but it would look properly aligned to the contributor when submitting. ## 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**) Broke down processing impact into minimal and moderate. It is not uncommon that we have changes that add some minimal processing, and yes, collectively they can become an issue, but we also need to distinguish them from the rare changes that have a moderate impact, and how critical those changes are. - 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**) This question merges two previous one, because it's really asking the same thing: Will your change be a headache to maintain down the line? ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | This is a new section, based on an idea that was discuss to not have everyone add their SQL translation files to their PR, and figure out file date name based on merge order, and coordinate who's using which message key. No. The hidden instruction instead tell the contributor to prerp the code to be translatable, by looking up GetBotTextOrDefault in the codebase for examples, and leave it that. When merged it would just use the default English fallback, then a monthly PR can be made containing translations for all the recently merged commits that have bot messages. The code would then automatically pickup the translated lines for that. This section of the template would remain if there's a consensus that this is how the translation workflow should be. ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Code comments, Conf comments, Commands in the Wiki). Final checklist remains the same, only clarifies to the contributors what sort of documentations that need updating. ## Notes for Reviewers This is a literal draft of of what the template should be, in that I look forward to your ideas to any ways that can further improve this. --- PULL_REQUEST_TEMPLATE.md | 167 +++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 94 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index d26b1b1c..d9ef2ced 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,124 +1,103 @@ -# Pull Request + -We prioritize **stability, performance, and predictability** over behavioral realism. -Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and -long-term robustness. +## Pull Request Description + -Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all -participants. Because every action and -decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and -negatively affect both players and -world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a -project goal. Increased behavioral -realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. -Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and -maintained continuously as the system evolves. -If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision -model**. More complex behavior should only be -available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. - -Principles: - -- **Stability before intelligence** - A stable system is always preferred over a smarter one. - -- **Performance is a shared resource** - Any increase in bot cost affects all players and all bots. - -- **Simple logic scales better than smart logic** - Predictable behavior under load is more valuable than perfect decisions. - -- **Complexity must justify itself** - If a feature cannot clearly explain its cost, it should not exist. - -- **Defaults must be cheap** - Expensive behavior must always be optional and clearly communicated. - -- **Bots should look reasonable, not perfect** - The goal is believable behavior, not human simulation. - -Before submitting, confirm that this change aligns with those principles. - ---- ## Feature Evaluation + -Please answer the following: + +- Describe the **minimum logic** required to achieve the intended behavior. +- Describe the **processing cost** when this logic executes across many bots. -- Describe the **minimum logic** required to achieve the intended behavior? -- Describe the **cheapest implementation** that produces an acceptable result? -- Describe the **runtime cost** when this logic executes across many bots? ---- ## How to Test the Changes + -- Step-by-step instructions to test the change -- Any required setup (e.g. multiple players, bots, specific configuration) -- Expected behavior and how to verify it -## Complexity & Impact -Does this change add new decision branches? -- - [ ] No -- - [ ] Yes (**explain below**) +## Impact Assessment + +- Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? + - [ ] No, not at all + - [ ] Minimal impact (**explain below**) + - [ ] Moderate impact (**explain below**) -Does this change increase per-bot or per-tick processing? -- - [ ] No -- - [ ] Yes (**describe and justify impact**) -Could this logic scale poorly under load? -- - [ ] No -- - [ ] Yes (**explain why**) ---- -## Defaults & Configuration +- Does this change modify default bot behavior? + - [ ] No + - [ ] Yes (**explain why**) -Does this change modify default bot behavior? -- - [ ] No -- - [ ] Yes (**explain why**) -If this introduces more advanced or AI-heavy logic: -- - [ ] Lightweight mode remains the default -- - [ ] More complex behavior is optional and thereby configurable ---- + +- Does this change add new decision branches or increase maintenance complexity? + - [ ] No + - [ ] Yes (**explain below**) + + + +## Messages to Translate + +Does this change add bot messages to translate? +- [ ] No +- [ ] Yes (**list messages in the table**) + +| Message key | Default message | +| --------------- | ------------------ | +| | | +| | | ## AI Assistance - -Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? -- - [ ] No -- - [ ] Yes (**explain below**) - -If yes, please specify: - -- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) -- Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) -- Which parts of the change were influenced or generated -- Whether the result was manually reviewed and adapted - + +Was AI assistance used while working on this change? +- [ ] No +- [ ] Yes (**explain below**) + + ---- ## Final Checklist -- - [ ] Stability is not compromised -- - [ ] Performance impact is understood, tested, and acceptable -- - [ ] Added logic complexity is justified and explained -- - [ ] Documentation updated if needed - ---- +- [ ] 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 - -Anything that significantly improves realism at the cost of stability or performance should be carefully discussed -before merging. + From a8dda550ad0c7405d5b7a3e4986b8f48c6cf1a83 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 8 Mar 2026 08:49:45 +0100 Subject: [PATCH 12/13] Requirement fix to use bigobj parameter to compile (#2176) # Pull Request Compilation fix which making possible compiling without bigobj parameter --- ## How to Test the Changes - compile using Visual Studio without bigobj parameter ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Automate file creation --- ## 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 --- .../2026_02_13_00_ai_playerbot_texts.sql | 2 +- src/Bot/Engine/AiObjectContext.cpp | 135 +----------------- src/Bot/Engine/BuildSharedActionContexts.cpp | 57 ++++++++ .../Engine/BuildSharedStrategyContexts.cpp | 14 ++ src/Bot/Engine/BuildSharedTriggerContexts.cpp | 57 ++++++++ src/Bot/Engine/BuildSharedValueContexts.cpp | 7 + 6 files changed, 137 insertions(+), 135 deletions(-) create mode 100644 src/Bot/Engine/BuildSharedActionContexts.cpp create mode 100644 src/Bot/Engine/BuildSharedStrategyContexts.cpp create mode 100644 src/Bot/Engine/BuildSharedTriggerContexts.cpp create mode 100644 src/Bot/Engine/BuildSharedValueContexts.cpp diff --git a/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql b/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql index 90bf69a0..9d254eb4 100644 --- a/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql +++ b/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql @@ -1 +1 @@ -UPDATE `ai_playerbot_texts` SET `text_loc3`='%s, du hörst den triefenden Sarkasmus in meinem text nicht' WHERE `id`=1353; +UPDATE `ai_playerbot_texts` SET `text_loc3`='%s, du hörst den triefenden Sarkasmus in meinem text nicht' WHERE `id`=1353; diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index e6333708..f8036975 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -4,61 +4,17 @@ */ #include "AiObjectContext.h" - -#include "ActionContext.h" -#include "ChatActionContext.h" -#include "ChatTriggerContext.h" +#include "Helpers.h" #include "DKAiObjectContext.h" #include "DruidAiObjectContext.h" #include "HunterAiObjectContext.h" #include "MageAiObjectContext.h" #include "PaladinAiObjectContext.h" -#include "Playerbots.h" #include "PriestAiObjectContext.h" #include "RogueAiObjectContext.h" #include "ShamanAiObjectContext.h" -#include "SharedValueContext.h" -#include "StrategyContext.h" -#include "TriggerContext.h" -#include "ValueContext.h" #include "WarlockAiObjectContext.h" #include "WarriorAiObjectContext.h" -#include "WorldPacketActionContext.h" -#include "WorldPacketTriggerContext.h" -#include "Ai/Dungeon/DungeonStrategyContext.h" -#include "Ai/Dungeon/WotlkDungeonActionContext.h" -#include "Ai/Dungeon/WotlkDungeonTriggerContext.h" -#include "Ai/Raid/RaidStrategyContext.h" -#include "Ai/Raid/Aq20/RaidAq20ActionContext.h" -#include "Ai/Raid/Aq20/RaidAq20TriggerContext.h" -#include "Ai/Raid/MoltenCore/RaidMcActionContext.h" -#include "Ai/Raid/MoltenCore/RaidMcTriggerContext.h" -#include "Ai/Raid/BlackwingLair/RaidBwlActionContext.h" -#include "Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h" -#include "Ai/Raid/Karazhan/RaidKarazhanActionContext.h" -#include "Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h" -#include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" -#include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" -#include "Ai/Raid/Naxxramas/RaidNaxxActionContext.h" -#include "Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h" -#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" -#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" -#include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" -#include "Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h" -#include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" -#include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" -#include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" -#include "Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h" -#include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" -#include "Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h" -#include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" -#include "Ai/Raid/Ulduar/RaidUlduarTriggerContext.h" -#include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" -#include "Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h" -#include "Ai/Raid/Icecrown/RaidIccActionContext.h" -#include "Ai/Raid/Icecrown/RaidIccTriggerContext.h" SharedNamedObjectContextList AiObjectContext::sharedStrategyContexts; SharedNamedObjectContextList AiObjectContext::sharedActionContexts; @@ -100,95 +56,6 @@ void AiObjectContext::BuildSharedContexts() BuildSharedValueContexts(sharedValueContexts); } -void AiObjectContext::BuildSharedStrategyContexts(SharedNamedObjectContextList& strategyContexts) -{ - strategyContexts.Add(new StrategyContext()); - strategyContexts.Add(new MovementStrategyContext()); - strategyContexts.Add(new AssistStrategyContext()); - strategyContexts.Add(new QuestStrategyContext()); - strategyContexts.Add(new DungeonStrategyContext()); - strategyContexts.Add(new RaidStrategyContext()); -} - -void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) -{ - actionContexts.Add(new ActionContext()); - actionContexts.Add(new ChatActionContext()); - actionContexts.Add(new WorldPacketActionContext()); - actionContexts.Add(new RaidAq20ActionContext()); - actionContexts.Add(new RaidMcActionContext()); - actionContexts.Add(new RaidBwlActionContext()); - actionContexts.Add(new RaidKarazhanActionContext()); - actionContexts.Add(new RaidGruulsLairActionContext()); - actionContexts.Add(new RaidMagtheridonActionContext()); - actionContexts.Add(new RaidNaxxActionContext()); - actionContexts.Add(new RaidSSCActionContext()); - actionContexts.Add(new RaidTempestKeepActionContext()); - actionContexts.Add(new RaidOsActionContext()); - actionContexts.Add(new RaidEoEActionContext()); - actionContexts.Add(new RaidVoAActionContext()); - actionContexts.Add(new RaidUlduarActionContext()); - actionContexts.Add(new RaidOnyxiaActionContext()); - actionContexts.Add(new RaidIccActionContext()); - actionContexts.Add(new WotlkDungeonUKActionContext()); - actionContexts.Add(new WotlkDungeonNexActionContext()); - actionContexts.Add(new WotlkDungeonANActionContext()); - actionContexts.Add(new WotlkDungeonOKActionContext()); - actionContexts.Add(new WotlkDungeonDTKActionContext()); - actionContexts.Add(new WotlkDungeonVHActionContext()); - actionContexts.Add(new WotlkDungeonGDActionContext()); - actionContexts.Add(new WotlkDungeonHoSActionContext()); - actionContexts.Add(new WotlkDungeonHoLActionContext()); - actionContexts.Add(new WotlkDungeonOccActionContext()); - actionContexts.Add(new WotlkDungeonUPActionContext()); - actionContexts.Add(new WotlkDungeonCoSActionContext()); - actionContexts.Add(new WotlkDungeonFoSActionContext()); - actionContexts.Add(new WotlkDungeonPoSActionContext()); - actionContexts.Add(new WotlkDungeonToCActionContext()); -} - -void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList& triggerContexts) -{ - triggerContexts.Add(new TriggerContext()); - triggerContexts.Add(new ChatTriggerContext()); - triggerContexts.Add(new WorldPacketTriggerContext()); - triggerContexts.Add(new RaidAq20TriggerContext()); - triggerContexts.Add(new RaidMcTriggerContext()); - triggerContexts.Add(new RaidBwlTriggerContext()); - triggerContexts.Add(new RaidKarazhanTriggerContext()); - triggerContexts.Add(new RaidGruulsLairTriggerContext()); - triggerContexts.Add(new RaidMagtheridonTriggerContext()); - triggerContexts.Add(new RaidNaxxTriggerContext()); - triggerContexts.Add(new RaidSSCTriggerContext()); - triggerContexts.Add(new RaidTempestKeepTriggerContext()); - triggerContexts.Add(new RaidOsTriggerContext()); - triggerContexts.Add(new RaidEoETriggerContext()); - triggerContexts.Add(new RaidVoATriggerContext()); - triggerContexts.Add(new RaidUlduarTriggerContext()); - triggerContexts.Add(new RaidOnyxiaTriggerContext()); - triggerContexts.Add(new RaidIccTriggerContext()); - triggerContexts.Add(new WotlkDungeonUKTriggerContext()); - triggerContexts.Add(new WotlkDungeonNexTriggerContext()); - triggerContexts.Add(new WotlkDungeonANTriggerContext()); - triggerContexts.Add(new WotlkDungeonOKTriggerContext()); - triggerContexts.Add(new WotlkDungeonDTKTriggerContext()); - triggerContexts.Add(new WotlkDungeonVHTriggerContext()); - triggerContexts.Add(new WotlkDungeonGDTriggerContext()); - triggerContexts.Add(new WotlkDungeonHoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonHoLTriggerContext()); - triggerContexts.Add(new WotlkDungeonOccTriggerContext()); - triggerContexts.Add(new WotlkDungeonUPTriggerContext()); - triggerContexts.Add(new WotlkDungeonCoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonFoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonPoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonToCTriggerContext()); -} - -void AiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) -{ - valueContexts.Add(new ValueContext()); -} - std::vector AiObjectContext::Save() { std::vector result; diff --git a/src/Bot/Engine/BuildSharedActionContexts.cpp b/src/Bot/Engine/BuildSharedActionContexts.cpp new file mode 100644 index 00000000..dc4ccdfe --- /dev/null +++ b/src/Bot/Engine/BuildSharedActionContexts.cpp @@ -0,0 +1,57 @@ +#include "AiObjectContext.h" +#include "ActionContext.h" +#include "ChatActionContext.h" +#include "WorldPacketActionContext.h" +#include "Ai/Raid/Aq20/RaidAq20ActionContext.h" +#include "Ai/Raid/MoltenCore/RaidMcActionContext.h" +#include "Ai/Raid/BlackwingLair/RaidBwlActionContext.h" +#include "Ai/Raid/Karazhan/RaidKarazhanActionContext.h" +#include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" +#include "Ai/Raid/Naxxramas/RaidNaxxActionContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" +#include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" +#include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" +#include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" +#include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" +#include "Ai/Raid/Icecrown/RaidIccActionContext.h" +#include "Ai/Dungeon/WotlkDungeonActionContext.h" + +void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) +{ + actionContexts.Add(new ActionContext()); + actionContexts.Add(new ChatActionContext()); + actionContexts.Add(new WorldPacketActionContext()); + actionContexts.Add(new RaidAq20ActionContext()); + actionContexts.Add(new RaidMcActionContext()); + actionContexts.Add(new RaidBwlActionContext()); + actionContexts.Add(new RaidKarazhanActionContext()); + actionContexts.Add(new RaidGruulsLairActionContext()); + actionContexts.Add(new RaidMagtheridonActionContext()); + actionContexts.Add(new RaidSSCActionContext()); + actionContexts.Add(new RaidTempestKeepActionContext()); + actionContexts.Add(new RaidNaxxActionContext()); + actionContexts.Add(new RaidOsActionContext()); + actionContexts.Add(new RaidEoEActionContext()); + actionContexts.Add(new RaidVoAActionContext()); + actionContexts.Add(new RaidUlduarActionContext()); + actionContexts.Add(new RaidOnyxiaActionContext()); + actionContexts.Add(new RaidIccActionContext()); + actionContexts.Add(new WotlkDungeonUKActionContext()); + actionContexts.Add(new WotlkDungeonNexActionContext()); + actionContexts.Add(new WotlkDungeonANActionContext()); + actionContexts.Add(new WotlkDungeonOKActionContext()); + actionContexts.Add(new WotlkDungeonDTKActionContext()); + actionContexts.Add(new WotlkDungeonVHActionContext()); + actionContexts.Add(new WotlkDungeonGDActionContext()); + actionContexts.Add(new WotlkDungeonHoSActionContext()); + actionContexts.Add(new WotlkDungeonHoLActionContext()); + actionContexts.Add(new WotlkDungeonOccActionContext()); + actionContexts.Add(new WotlkDungeonUPActionContext()); + actionContexts.Add(new WotlkDungeonCoSActionContext()); + actionContexts.Add(new WotlkDungeonFoSActionContext()); + actionContexts.Add(new WotlkDungeonPoSActionContext()); + actionContexts.Add(new WotlkDungeonToCActionContext()); +} diff --git a/src/Bot/Engine/BuildSharedStrategyContexts.cpp b/src/Bot/Engine/BuildSharedStrategyContexts.cpp new file mode 100644 index 00000000..171276d2 --- /dev/null +++ b/src/Bot/Engine/BuildSharedStrategyContexts.cpp @@ -0,0 +1,14 @@ +#include "AiObjectContext.h" +#include "StrategyContext.h" +#include "Ai/Dungeon/DungeonStrategyContext.h" +#include "Ai/Raid/RaidStrategyContext.h" + +void AiObjectContext::BuildSharedStrategyContexts(SharedNamedObjectContextList& strategyContexts) +{ + strategyContexts.Add(new StrategyContext()); + strategyContexts.Add(new MovementStrategyContext()); + strategyContexts.Add(new AssistStrategyContext()); + strategyContexts.Add(new QuestStrategyContext()); + strategyContexts.Add(new DungeonStrategyContext()); + strategyContexts.Add(new RaidStrategyContext()); +} diff --git a/src/Bot/Engine/BuildSharedTriggerContexts.cpp b/src/Bot/Engine/BuildSharedTriggerContexts.cpp new file mode 100644 index 00000000..241e850b --- /dev/null +++ b/src/Bot/Engine/BuildSharedTriggerContexts.cpp @@ -0,0 +1,57 @@ +#include "AiObjectContext.h" +#include "TriggerContext.h" +#include "ChatTriggerContext.h" +#include "WorldPacketTriggerContext.h" +#include "Ai/Raid/Aq20/RaidAq20TriggerContext.h" +#include "Ai/Raid/MoltenCore/RaidMcTriggerContext.h" +#include "Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h" +#include "Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h" +#include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" +#include "Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h" +#include "Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h" +#include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" +#include "Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarTriggerContext.h" +#include "Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h" +#include "Ai/Raid/Icecrown/RaidIccTriggerContext.h" +#include "Ai/Dungeon/WotlkDungeonTriggerContext.h" + +void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList& triggerContexts) +{ + triggerContexts.Add(new TriggerContext()); + triggerContexts.Add(new ChatTriggerContext()); + triggerContexts.Add(new WorldPacketTriggerContext()); + triggerContexts.Add(new RaidAq20TriggerContext()); + triggerContexts.Add(new RaidMcTriggerContext()); + triggerContexts.Add(new RaidBwlTriggerContext()); + triggerContexts.Add(new RaidKarazhanTriggerContext()); + triggerContexts.Add(new RaidGruulsLairTriggerContext()); + triggerContexts.Add(new RaidMagtheridonTriggerContext()); + triggerContexts.Add(new RaidNaxxTriggerContext()); + triggerContexts.Add(new RaidSSCTriggerContext()); + triggerContexts.Add(new RaidTempestKeepTriggerContext()); + triggerContexts.Add(new RaidOsTriggerContext()); + triggerContexts.Add(new RaidEoETriggerContext()); + triggerContexts.Add(new RaidVoATriggerContext()); + triggerContexts.Add(new RaidUlduarTriggerContext()); + triggerContexts.Add(new RaidOnyxiaTriggerContext()); + triggerContexts.Add(new RaidIccTriggerContext()); + triggerContexts.Add(new WotlkDungeonUKTriggerContext()); + triggerContexts.Add(new WotlkDungeonNexTriggerContext()); + triggerContexts.Add(new WotlkDungeonANTriggerContext()); + triggerContexts.Add(new WotlkDungeonOKTriggerContext()); + triggerContexts.Add(new WotlkDungeonDTKTriggerContext()); + triggerContexts.Add(new WotlkDungeonVHTriggerContext()); + triggerContexts.Add(new WotlkDungeonGDTriggerContext()); + triggerContexts.Add(new WotlkDungeonHoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonHoLTriggerContext()); + triggerContexts.Add(new WotlkDungeonOccTriggerContext()); + triggerContexts.Add(new WotlkDungeonUPTriggerContext()); + triggerContexts.Add(new WotlkDungeonCoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonFoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonPoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonToCTriggerContext()); +} diff --git a/src/Bot/Engine/BuildSharedValueContexts.cpp b/src/Bot/Engine/BuildSharedValueContexts.cpp new file mode 100644 index 00000000..31f12da5 --- /dev/null +++ b/src/Bot/Engine/BuildSharedValueContexts.cpp @@ -0,0 +1,7 @@ +#include "AiObjectContext.h" +#include "ValueContext.h" + +void AiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) +{ + valueContexts.Add(new ValueContext()); +} From 2925f248a6832c8ce054eabd4a7fb40a798f8378 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 8 Mar 2026 08:50:01 +0100 Subject: [PATCH 13/13] Workaround for crash related with uninitalized script (#2196) # Pull Request Workaround for crashes related with uninitialized new script introduced in https://github.com/azerothcore/azerothcore-wotlk/commit/e74adf550e778bbbf182e1f4ef95ba2acb67fa19 --- ## Feature Evaluation --- ## How to Test the Changes - run server with default bots amount without changes and should crashed - run server with default bots amount with changes and should run normally ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [x] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Used Copilot Cli to find crash reason. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- --- src/Script/Playerbots.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp index ab6d64f9..af28027c 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -17,6 +17,7 @@ #include "Playerbots.h" +#include "BattlefieldScript.h" #include "Channel.h" #include "Config.h" #include "DatabaseEnv.h" @@ -518,12 +519,20 @@ public: void OnBattlegroundEnd(Battleground* bg, TeamId /*winnerTeam*/) override { bgStrategies.erase(bg->GetInstanceID()); } }; +// Workaround for missing InitEnabledHooksIfNeeded for new BattlefieldScript in ScriptMgr +class PlayerbotsBattlefieldScript : public BattlefieldScript +{ +public: + PlayerbotsBattlefieldScript() : BattlefieldScript("PlayerbotsBattlefieldScript") { } +}; + void AddPlayerbotsSecureLoginScripts(); void AddSC_TempestKeepBotScripts(); void AddPlayerbotsScripts() { + new PlayerbotsBattlefieldScript(); new PlayerbotsDatabaseScript(); new PlayerbotsPlayerScript(); new PlayerbotsMiscScript();