From 4a79a46da5650ea2e6908d45296983ae6b661ef6 Mon Sep 17 00:00:00 2001
From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com>
Date: Sat, 2 May 2026 21:18:54 +0200
Subject: [PATCH 01/19] Add argument "all" to "rep" command and new "emblems"
command (#2035)
## Summary
- restrict `reputation all` to a curated list of WotLK/BC/Classic
faction IDs (filtered by team)
- reuse a shared formatter for reputation lines
- add an `emblems` chat command to report emblem counts
### Multibot will need a update
---------
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/TellEmblemsAction.cpp | 39 ++++++++
src/Ai/Base/Actions/TellEmblemsAction.h | 21 +++++
src/Ai/Base/Actions/TellReputationAction.cpp | 89 ++++++++++++++-----
src/Ai/Base/Actions/TellReputationAction.h | 6 ++
src/Ai/Base/ChatActionContext.h | 3 +
src/Ai/Base/ChatTriggerContext.h | 2 +
.../Strategy/ChatCommandHandlerStrategy.cpp | 6 +-
7 files changed, 143 insertions(+), 23 deletions(-)
create mode 100644 src/Ai/Base/Actions/TellEmblemsAction.cpp
create mode 100644 src/Ai/Base/Actions/TellEmblemsAction.h
diff --git a/src/Ai/Base/Actions/TellEmblemsAction.cpp b/src/Ai/Base/Actions/TellEmblemsAction.cpp
new file mode 100644
index 000000000..4d69baa11
--- /dev/null
+++ b/src/Ai/Base/Actions/TellEmblemsAction.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "TellEmblemsAction.h"
+
+#include
+
+#include "Event.h"
+#include "Playerbots.h"
+
+bool TellEmblemsAction::Execute(Event /*event*/)
+{
+ static std::array const emblemIds = {
+ 29434, // Badge of Justice
+ 40752, // Emblem of Heroism
+ 40753, // Emblem of Valor
+ 45624, // Emblem of Conquest
+ 47241, // Emblem of Triumph
+ 49426 // Emblem of Frost
+ };
+
+ botAI->TellMaster("=== Emblems ===");
+
+ for (uint32 itemId : emblemIds)
+ {
+ ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
+ if (!proto)
+ continue;
+
+ uint32 count = bot->GetItemCount(itemId, false);
+ std::ostringstream out;
+ out << chat->FormatItem(proto, count);
+ botAI->TellMaster(out);
+ }
+
+ return true;
+}
diff --git a/src/Ai/Base/Actions/TellEmblemsAction.h b/src/Ai/Base/Actions/TellEmblemsAction.h
new file mode 100644
index 000000000..570fb2d04
--- /dev/null
+++ b/src/Ai/Base/Actions/TellEmblemsAction.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#ifndef _PLAYERBOT_TELLEMBLEMSACTION_H
+#define _PLAYERBOT_TELLEMBLEMSACTION_H
+
+#include "InventoryAction.h"
+
+class PlayerbotAI;
+
+class TellEmblemsAction : public InventoryAction
+{
+public:
+ TellEmblemsAction(PlayerbotAI* botAI) : InventoryAction(botAI, "emblems") {}
+
+ bool Execute(Event event) override;
+};
+
+#endif
diff --git a/src/Ai/Base/Actions/TellReputationAction.cpp b/src/Ai/Base/Actions/TellReputationAction.cpp
index 0ccff606a..22c11e165 100644
--- a/src/Ai/Base/Actions/TellReputationAction.cpp
+++ b/src/Ai/Base/Actions/TellReputationAction.cpp
@@ -5,34 +5,23 @@
#include "TellReputationAction.h"
+#include
+
#include "Event.h"
#include "PlayerbotAI.h"
#include "ReputationMgr.h"
-bool TellReputationAction::Execute(Event /*event*/)
+#include "SharedDefines.h"
+
+std::string TellReputationAction::BuildReputationLine(FactionEntry const* entry)
{
- Player* master = GetMaster();
- if (!master)
- return false;
-
- ObjectGuid selection = master->GetTarget();
- if (selection.IsEmpty())
- return false;
-
- Unit* unit = ObjectAccessor::GetUnit(*master, selection);
- if (!unit)
- return false;
-
- FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry();
- uint32 faction = factionTemplate->faction;
- FactionEntry const* entry = sFactionStore.LookupEntry(faction);
- int32 reputation = bot->GetReputationMgr().GetReputation(faction);
+ ReputationMgr& repMgr = bot->GetReputationMgr();
+ ReputationRank rank = repMgr.GetRank(entry);
+ int32 reputation = repMgr.GetReputation(entry->ID);
std::ostringstream out;
- out << entry->name[0] << ": ";
- out << "|cff";
+ out << entry->name[0] << ": |cff";
- ReputationRank rank = bot->GetReputationMgr().GetRank(entry);
switch (rank)
{
case REP_HATED:
@@ -71,7 +60,65 @@ bool TellReputationAction::Execute(Event /*event*/)
base -= ReputationMgr::PointsInRank[i];
out << " (" << (reputation - base) << "/" << ReputationMgr::PointsInRank[rank] << ")";
- botAI->TellMaster(out);
+ return out.str();
+}
+
+bool TellReputationAction::Execute(Event event)
+{
+ std::string const param = event.getParam();
+ if (param == "all")
+ {
+ ReputationMgr& repMgr = bot->GetReputationMgr();
+ std::vector lines;
+
+ FactionStateList const& stateList = repMgr.GetStateList();
+ lines.reserve(stateList.size());
+
+ for (auto const& itr : stateList)
+ {
+ FactionState const& faction = itr.second;
+ if (!(faction.Flags & FACTION_FLAG_VISIBLE))
+ continue;
+
+ if (faction.Flags & (FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED) &&
+ !(faction.Flags & FACTION_FLAG_SPECIAL))
+ continue;
+
+ FactionEntry const* entry = sFactionStore.LookupEntry(faction.ID);
+ if (!entry)
+ continue;
+
+ lines.push_back(BuildReputationLine(entry));
+ }
+
+ std::sort(lines.begin(), lines.end());
+
+ botAI->TellMaster("=== Reputations ===");
+ for (auto const& line : lines)
+ botAI->TellMaster(line);
+
+ return true;
+ }
+
+ Player* master = GetMaster();
+ if (!master)
+ return false;
+
+ ObjectGuid selection = master->GetTarget();
+ if (selection.IsEmpty())
+ return false;
+
+ Unit* unit = ObjectAccessor::GetUnit(*master, selection);
+ if (!unit)
+ return false;
+
+ FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry();
+
+ FactionEntry const* entry = sFactionStore.LookupEntry(factionTemplate->faction);
+ if (!entry)
+ return false;
+
+ botAI->TellMaster(BuildReputationLine(entry));
return true;
}
diff --git a/src/Ai/Base/Actions/TellReputationAction.h b/src/Ai/Base/Actions/TellReputationAction.h
index 3adaa66d5..d97d0d177 100644
--- a/src/Ai/Base/Actions/TellReputationAction.h
+++ b/src/Ai/Base/Actions/TellReputationAction.h
@@ -6,8 +6,11 @@
#ifndef _PLAYERBOT_TELLREPUTATIONACTION_H
#define _PLAYERBOT_TELLREPUTATIONACTION_H
+#include
+
#include "Action.h"
+struct FactionEntry;
class PlayerbotAI;
class TellReputationAction : public Action
@@ -16,6 +19,9 @@ public:
TellReputationAction(PlayerbotAI* botAI) : Action(botAI, "reputation") {}
bool Execute(Event event) override;
+
+private:
+ std::string BuildReputationLine(FactionEntry const* entry);
};
#endif
diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h
index af51c23ae..497ae2e9c 100644
--- a/src/Ai/Base/ChatActionContext.h
+++ b/src/Ai/Base/ChatActionContext.h
@@ -66,6 +66,7 @@
#include "TaxiAction.h"
#include "TeleportAction.h"
#include "TellCastFailedAction.h"
+#include "TellEmblemsAction.h"
#include "TellItemCountAction.h"
#include "TellLosAction.h"
#include "TellReputationAction.h"
@@ -120,6 +121,7 @@ public:
creators["teleport"] = &ChatActionContext::teleport;
creators["taxi"] = &ChatActionContext::taxi;
creators["repair"] = &ChatActionContext::repair;
+ creators["emblems"] = &ChatActionContext::emblems;
creators["use"] = &ChatActionContext::use;
creators["item count"] = &ChatActionContext::item_count;
creators["equip"] = &ChatActionContext::equip;
@@ -276,6 +278,7 @@ private:
static Action* item_count(PlayerbotAI* botAI) { return new TellItemCountAction(botAI); }
static Action* use(PlayerbotAI* botAI) { return new UseItemAction(botAI); }
static Action* repair(PlayerbotAI* botAI) { return new RepairAllAction(botAI); }
+ static Action* emblems(PlayerbotAI* botAI) { return new TellEmblemsAction(botAI); }
static Action* taxi(PlayerbotAI* botAI) { return new TaxiAction(botAI); }
static Action* teleport(PlayerbotAI* botAI) { return new TeleportAction(botAI); }
static Action* release(PlayerbotAI* botAI) { return new ReleaseSpiritAction(botAI); }
diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h
index 7742a9305..ef8827c29 100644
--- a/src/Ai/Base/ChatTriggerContext.h
+++ b/src/Ai/Base/ChatTriggerContext.h
@@ -41,6 +41,7 @@ public:
creators["teleport"] = &ChatTriggerContext::teleport;
creators["taxi"] = &ChatTriggerContext::taxi;
creators["repair"] = &ChatTriggerContext::repair;
+ creators["emblems"] = &ChatTriggerContext::emblems;
creators["u"] = &ChatTriggerContext::use;
creators["use"] = &ChatTriggerContext::use;
creators["c"] = &ChatTriggerContext::item_count;
@@ -235,6 +236,7 @@ private:
static Trigger* item_count(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "c"); }
static Trigger* use(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "use"); }
static Trigger* repair(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "repair"); }
+ static Trigger* emblems(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "emblems"); }
static Trigger* taxi(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "taxi"); }
static Trigger* teleport(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "teleport"); }
static Trigger* q(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "q"); }
diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp
index 8d5449ef3..2e4503c18 100644
--- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp
+++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp
@@ -114,6 +114,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger
triggers.push_back(new TriggerNode("pet attack", { NextAction("pet attack", relevance) }));
triggers.push_back(new TriggerNode("roll", { NextAction("roll", relevance) }));
triggers.push_back(new TriggerNode("focus heal", { NextAction("focus heal targets", relevance) }));
+ triggers.push_back(new TriggerNode("emblems", { NextAction("emblems", relevance) }));
}
ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI)
@@ -138,6 +139,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("teleport");
supported.push_back("taxi");
supported.push_back("repair");
+ supported.push_back("emblems");
supported.push_back("talents");
supported.push_back("spells");
supported.push_back("co");
@@ -202,8 +204,8 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("unlock items");
supported.push_back("unlock traded item");
supported.push_back("tame");
- supported.push_back("glyphs"); // Added for custom Glyphs
- supported.push_back("glyph equip"); // Added for custom Glyphs
+ supported.push_back("glyphs");
+ supported.push_back("glyph equip");
supported.push_back("pet");
supported.push_back("pet attack");
supported.push_back("wait for attack time");
From 410ce134fe1e09ac4b76a4bfd4699957db20a2ea Mon Sep 17 00:00:00 2001
From: HennyWilly <5954598+HennyWilly@users.noreply.github.com>
Date: Sat, 2 May 2026 21:19:11 +0200
Subject: [PATCH 02/19] Fix Deep Breath issues during Onyxia encounter (#2318)
## Pull Request Description
The current strategy for Onyxia causes bots to get hit by her breath
attack relatively consistently during phase 2.
The problem was that the safe zone coordinates always use the bot's
z-coordinate. If the bots are standing at the lower altitude part of the
arena, `SearchForBestPath` inside `MoveTo` causes `INVALID_HEIGHT`,
resulting in the bot not moving at all and getting hit by the breath
attack.
This PR fixes this behavior by using the actual terrain z-coordinates
for the predefined safe zones instead of always using the bot's
z-coordinate. These values are chosen to ensure valid pathfinding
regardless of the bot's current elevation.
Additionally, bots now interrupt their spells if they are not inside a
safe zone during the breath. This causes the bots to immediately start
running instead of finishing their casts first.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
Replaced the use of `bot->GetPositionZ()` in `GetSafeZonesForBreath`
with predefined safe zone z-coordinates to ensure valid pathfinding.
Added `AttackStop` and `InterruptNonMeleeSpells` to guarantee immediate
movement when outside safe zones.
No additional condition checks or branching logic were introduced.
- Describe the **processing cost** when this logic executes across many
bots.
Minimal. The logic only runs within the Onyxia encounter script and
calling `AttackStop` and `InterruptNonMeleeSpells` should be negligible.
## How to Test the Changes
Enter Onyxia's Lair (10, 25 or 40 (mod-individual-progression)) and
engage Onyxia with the appropriate number of bots.
During phase 2 (Onyxia takes off), check if the bots move to the safe
zones during the breath attack.
Tip: Mark Onyxia as moon (RTI), so that phase 2 doesn't end too quickly.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [ ] No, not at all
- - [x] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
The calls of `AttackStop` and `InterruptNonMeleeSpells` cause minimal
overhead compared to the original strategy. This should be negligible.
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
Yes (encounter-specific). Bots will now interrupt casts earlier during
Onyxia phase 2 to prioritize movement to safe zones.
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
The strategy for Onyxia might need additional work:
For example, the Onyxian Lair Guards are completely ignored while whelps
are alive and their Blast Nova doesn't get handled at all.
This PR focuses on fixing the Deep Breath behavior. Handling of Onyxian
Lair Guards is not included and should be implemented in a separate PR.
---
.../Raid/Onyxia/Action/RaidOnyxiaActions.cpp | 6 +++-
src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h | 30 ++++++++++---------
2 files changed, 21 insertions(+), 15 deletions(-)
diff --git a/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp b/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp
index e2fe3cbe6..5b470ee5c 100644
--- a/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp
+++ b/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp
@@ -99,8 +99,12 @@ bool RaidOnyxiaMoveToSafeZoneAction::Execute(Event /*event*/)
if (bot->IsWithinDist2d(bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->radius))
return false; // Already safe
+ // Stop current spell first
+ bot->AttackStop();
+ bot->InterruptNonMeleeSpells(false);
+
// bot->Yell("Moving to Safe Zone!", LANG_UNIVERSAL);
- return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bot->GetPositionZ(),
+ return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->pos.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_COMBAT);
}
diff --git a/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h b/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h
index 3943aaf60..d5b8eafd9 100644
--- a/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h
+++ b/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h
@@ -2,7 +2,6 @@
#ifndef _PLAYERBOT_RAIDONYXIAACTIONS_H_
#define _PLAYERBOT_RAIDONYXIAACTIONS_H_
-#include "Action.h"
#include "AttackAction.h"
#include "GenericSpellActions.h"
#include "MovementActions.h"
@@ -45,42 +44,45 @@ public:
bool Execute(Event event) override;
private:
- std::vector GetSafeZonesForBreath(uint32 spellId)
+ static std::vector GetSafeZonesForBreath(uint32 spellId)
{
- // Define your safe zone coordinates based on the map
- // Example assumes Onyxia's lair map coordinates
- float z = bot->GetPositionZ(); // Stay at current height
+ // Safe zone coordinates based on the map
+ // Assumes Onyxia's lair map coordinates
switch (spellId)
{
case 17086: // N to S
case 18351: // S to N
- return {SafeZone{Position(-10.0f, -180.0f, z), 5.0f},
- SafeZone{Position(-20.0f, -250.0f, z), 5.0f}}; // Bottom Safe Zone
+ return {
+ SafeZone{Position(-10.0f, -180.0f, -87.0f), 5.0f},
+ SafeZone{Position(-20.0f, -250.0f, -88.0f), 5.0f}
+ }; // Bottom Safe Zone
case 18576: // E to W
case 18609: // W to E
return {
- SafeZone{Position(20.0f, -210.0f, z), 5.0f},
- SafeZone{Position(-75.0f, -210.0f, z), 5.0f},
+ SafeZone{Position(20.0f, -210.0f, -85.5f), 5.0f},
+ SafeZone{Position(-75.0f, -210.0f, -83.4f), 5.0f},
}; // Left Safe Zone
case 18564: // SE to NW
case 18584: // NW to SE
return {
- SafeZone{Position(-60.0f, -195.0f, z), 5.0f},
- SafeZone{Position(10.0f, -240.0f, z), 5.0f},
+ SafeZone{Position(-60.0f, -195.0f, -85.0f), 5.0f},
+ SafeZone{Position(10.0f, -240.0f, -85.9f), 5.0f},
}; // NW Safe Zone
case 18596: // SW to NE
case 18617: // NE to SW
return {
- SafeZone{Position(7.0f, -185.0f, z), 5.0f},
- SafeZone{Position(-60.0f, -240.0f, z), 5.0f},
+ SafeZone{Position(7.0f, -185.0f, -86.2f), 5.0f},
+ SafeZone{Position(-60.0f, -240.0f, -85.2f), 5.0f},
}; // NE Safe Zone
default:
- return {SafeZone{Position(0.0f, 0.0f, z), 5.0f}}; // Fallback center - shouldn't ever happen
+ return {
+ SafeZone{Position(-40.0f, -214.0f, -86.6f), 5.0f}
+ }; // Fallback center - shouldn't ever happen
}
}
};
From c819516325a628913725ed4b9021ba37818ee4c7 Mon Sep 17 00:00:00 2001
From: Keleborn <22352763+Celandriel@users.noreply.github.com>
Date: Sat, 2 May 2026 12:19:23 -0700
Subject: [PATCH 03/19] Fix rpg travel flying (#2324)
## Pull Request Description
Clean up values that were incorrectly translated from the sql search
into the dbc search.
Refactors structure for cities in TravelMgr to try to resolve some
duplication issues.
Change to position based search, so that bots dont get stuck if they
fail to resolve the flightmaster game object when it hasnt spawned.
TravelFlight state now stores flight master entry + world position
instead of ObjectGuid, so the bot can move back into range and
re-resolve the NPC locally via FindNearestCreature
Bundles reliability cleanup in NewRpgTravelFlightAction: uses
info.ChangeToIdle() consistently and adds the missing return true after
a failed taxi path
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
No expected shanges.
## How to Test the Changes
Run the server and check if zones are getting populated well.
## 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**)
Run with 4k bots, no issues.
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
It should correctly send bots to the areas appropriate for their level
in an equally weighted manner.
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - x ] Yes (**explain below**)
Refactoring the data structure based on my instruction.
All parts reviewed.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/World/Rpg/Action/NewRpgAction.cpp | 12 +-
src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 22 +-
src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 2 +-
src/Ai/World/Rpg/NewRpgInfo.cpp | 7 +-
src/Ai/World/Rpg/NewRpgInfo.h | 5 +-
src/Mgr/Travel/TravelMgr.cpp | 421 +++++++++++--------
src/Mgr/Travel/TravelMgr.h | 25 +-
src/Mgr/Travel/TravelNode.cpp | 2 +-
8 files changed, 290 insertions(+), 206 deletions(-)
diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp
index ca0ca2433..290be0c0a 100644
--- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp
+++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp
@@ -3,6 +3,7 @@
#include
#include
+#include "AreaDefines.h"
#include "BroadcastHelper.h"
#include "ChatHelper.h"
#include "G3D/Vector2.h"
@@ -468,10 +469,14 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
data.inFlight = true;
return false;
}
- Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster);
+
+ if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE)
+ return MoveFarTo(data.flightMasterPos);
+
+ Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3);
if (!flightMaster || !flightMaster->IsAlive())
{
- botAI->rpgInfo.ChangeToIdle();
+ info.ChangeToIdle();
return true;
}
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
@@ -487,7 +492,8 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
{
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
- botAI->rpgInfo.ChangeToIdle();
+ info.ChangeToIdle();
+ return true;
}
return true;
}
diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp
index 092b11538..336c7599d 100644
--- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp
+++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp
@@ -1027,19 +1027,21 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
return dest;
}
-bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path)
+bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path)
{
- flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
- if (!flightMaster)
+ TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot);
+ if (!info)
return false;
std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
if (availablePaths.empty())
return false;
+ flightMasterEntry = info->templateEntry;
+ flightMasterPos = info->pos;
path = availablePaths[urand(0, availablePaths.size() - 1)];
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)",
- bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size());
+ bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size());
return true;
}
@@ -1139,11 +1141,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta
}
case RPG_TRAVEL_FLIGHT:
{
- ObjectGuid flightMaster;
+ uint32 flightMasterEntry = 0;
+ WorldPosition flightMasterPos;
std::vector path;
- if (SelectRandomFlightTaxiNode(flightMaster, path))
+ if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path))
{
- botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
+ botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path);
return true;
}
return false;
@@ -1220,9 +1223,10 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
}
case RPG_TRAVEL_FLIGHT:
{
- ObjectGuid flightMaster;
+ uint32 flightMasterEntry = 0;
+ WorldPosition flightMasterPos;
std::vector path;
- return SelectRandomFlightTaxiNode(flightMaster, path);
+ return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path);
}
case RPG_OUTDOOR_PVP:
{
diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h
index eaba72446..e73a23219 100644
--- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h
+++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h
@@ -54,7 +54,7 @@ protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);
static WorldPosition SelectRandomCampPos(Player* bot);
- bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path);
+ bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path);
bool RandomChangeStatus(std::vector candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);
diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp
index 4935503fc..780430f6d 100644
--- a/src/Ai/World/Rpg/NewRpgInfo.cpp
+++ b/src/Ai/World/Rpg/NewRpgInfo.cpp
@@ -37,11 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
data = do_quest;
}
-void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path)
+void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path)
{
startT = getMSTime();
TravelFlight flight;
- flight.fromFlightMaster = fromFlightMaster;
+ flight.flightMasterEntry = flightMasterEntry;
+ flight.flightMasterPos = flightMasterPos;
flight.path = std::move(path);
flight.inFlight = false;
data = flight;
@@ -157,7 +158,7 @@ std::string NewRpgInfo::ToString()
else if constexpr (std::is_same_v)
{
out << "TRAVEL_FLIGHT";
- out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry();
+ out << "\nflightMasterEntry: " << arg.flightMasterEntry;
out << "\nfromNode: " << arg.path[0];
out << "\ntoNode: " << arg.path[arg.path.size() - 1];
out << "\ninFlight: " << arg.inFlight;
diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h
index 9e6abdda4..5896915a4 100644
--- a/src/Ai/World/Rpg/NewRpgInfo.h
+++ b/src/Ai/World/Rpg/NewRpgInfo.h
@@ -49,7 +49,8 @@ struct NewRpgInfo
// RPG_TRAVEL_FLIGHT
struct TravelFlight
{
- ObjectGuid fromFlightMaster{};
+ uint32 flightMasterEntry{0};
+ WorldPosition flightMasterPos{};
std::vector path;
bool inFlight{false};
};
@@ -96,7 +97,7 @@ struct NewRpgInfo
void ChangeToWanderNpc();
void ChangeToWanderRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest);
- void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path);
+ void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path);
void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0);
void ChangeToRest();
void ChangeToIdle();
diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp
index 1868bc2e3..adc1e4a3e 100644
--- a/src/Mgr/Travel/TravelMgr.cpp
+++ b/src/Mgr/Travel/TravelMgr.cpp
@@ -8,6 +8,7 @@
#include
#include
+#include "AreaDefines.h"
#include "Creature.h"
#include "Log.h"
#include "ObjectAccessor.h"
@@ -28,67 +29,60 @@
// Navigation data
-enum class CityId : uint8
+struct Capital
{
- STORMWIND,
- IRONFORGE,
- DARNASSUS,
- EXODAR,
- ORGRIMMAR,
- UNDERCITY,
- THUNDER_BLUFF,
- SILVERMOON_CITY,
- SHATTRATH_CITY,
- DALARAN
+ uint32 zoneId;
+ TeamId team;
+ char const* name;
+ std::vector bankers;
};
-static const std::unordered_map> bankerToCity = {
- {2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}},
- {2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}},
- {4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}},
- {17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}},
- {3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}},
- {4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}},
- {2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}},
- {17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}},
- {16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}},
- {19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}},
- {19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}},
- {30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}},
- {28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}}
+static const std::vector capitals = {
+ { AREA_STORMWIND_CITY, TEAM_ALLIANCE, "Stormwind", {2455, 2456, 2457} },
+ { AREA_IRONFORGE, TEAM_ALLIANCE, "Ironforge", {2460, 2461, 5099} },
+ { AREA_DARNASSUS, TEAM_ALLIANCE, "Darnassus", {4155, 4208, 4209} },
+ { AREA_THE_EXODAR, TEAM_ALLIANCE, "Exodar", {17773, 18350, 16710} },
+ { AREA_ORGRIMMAR, TEAM_HORDE, "Orgrimmar", {3320, 3309, 3318} },
+ { AREA_UNDERCITY, TEAM_HORDE, "Undercity", {4549, 2459, 2458, 4550} },
+ { AREA_THUNDER_BLUFF, TEAM_HORDE, "Thunder Bluff", {2996, 8356, 8357} },
+ { AREA_SILVERMOON_CITY, TEAM_HORDE, "Silvermoon", {17631, 17632, 17633, 16615, 16616, 16617} },
+ { AREA_SHATTRATH_CITY, TEAM_NEUTRAL, "Shattrath", {19246, 19338, 19034, 19318} },
+ { AREA_DALARAN, TEAM_NEUTRAL, "Dalaran", {30604, 30605, 30607, 28675, 28676, 28677, 29530} }
};
-static const std::unordered_map> cityToBankers = {
- {CityId::STORMWIND, {2455, 2456, 2457}},
- {CityId::IRONFORGE, {2460, 2461, 5099}},
- {CityId::DARNASSUS, {4155, 4208, 4209}},
- {CityId::EXODAR, {17773, 18350, 16710}},
- {CityId::ORGRIMMAR, {3320, 3309, 3318}},
- {CityId::UNDERCITY, {4549, 2459, 2458, 4550}},
- {CityId::THUNDER_BLUFF, {2996, 8356, 8357}},
- {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}},
- {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}},
- {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}}
-};
-
-static int GetCityWeight(CityId city)
+static Capital const* FindCapitalByZone(uint32 zoneId)
{
- int weight = 0;
- switch (city)
+ for (Capital const& capital : capitals)
+ if (capital.zoneId == zoneId)
+ return &capital;
+ return nullptr;
+}
+
+static Capital const* FindCapitalByBanker(uint16 bankerEntry)
+{
+ for (Capital const& capital : capitals)
+ for (uint16 bankerId : capital.bankers)
+ if (bankerId == bankerEntry)
+ return &capital;
+ return nullptr;
+}
+
+static int GetCityWeight(uint32 zoneId)
+{
+ switch (zoneId)
{
- case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break;
- case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break;
- case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break;
- case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break;
- case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break;
- case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break;
- case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break;
- case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break;
- case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break;
- case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break;
- default: weight = 0; break;
+ case AREA_STORMWIND_CITY: return sPlayerbotAIConfig.weightTeleToStormwind;
+ case AREA_IRONFORGE: return sPlayerbotAIConfig.weightTeleToIronforge;
+ case AREA_DARNASSUS: return sPlayerbotAIConfig.weightTeleToDarnassus;
+ case AREA_THE_EXODAR: return sPlayerbotAIConfig.weightTeleToExodar;
+ case AREA_ORGRIMMAR: return sPlayerbotAIConfig.weightTeleToOrgrimmar;
+ case AREA_UNDERCITY: return sPlayerbotAIConfig.weightTeleToUndercity;
+ case AREA_THUNDER_BLUFF: return sPlayerbotAIConfig.weightTeleToThunderBluff;
+ case AREA_SILVERMOON_CITY: return sPlayerbotAIConfig.weightTeleToSilvermoonCity;
+ case AREA_SHATTRATH_CITY: return sPlayerbotAIConfig.weightTeleToShattrathCity;
+ case AREA_DALARAN: return sPlayerbotAIConfig.weightTeleToDalaran;
}
- return weight;
+ return 0;
}
WorldPosition::WorldPosition(std::string const str)
@@ -4369,76 +4363,117 @@ void TravelMgr::Init()
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
}
-Creature* TravelMgr::GetNearestFlightMaster(Player* bot)
+TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
{
- std::map& flightMasterCache =
+ auto const& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
- Creature* nearestFlightMaster = nullptr;
+ FlightMasterInfo const* nearest = nullptr;
float nearestDistance = std::numeric_limits::max();
- for (auto const& [entry, pos] : flightMasterCache)
+ for (auto const& [dbGuid, info] : flightMasterCache)
{
- if (pos.GetMapId() != bot->GetMapId())
+ if (info.pos.GetMapId() != bot->GetMapId())
continue;
- float distance = bot->GetExactDist2dSq(pos);
- if (distance > nearestDistance)
- continue;
-
- Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
- if (flightMaster)
+ float distance = bot->GetExactDist2dSq(info.pos);
+ if (distance < nearestDistance)
{
nearestDistance = distance;
- nearestFlightMaster = flightMaster;
+ nearest = &info;
}
}
- return nearestFlightMaster;
+ return nearest;
}
-ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot)
+std::vector TravelMgr::GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode) const
{
- Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
- if (!nearestFlightMaster)
- return ObjectGuid::Empty;
-
- return nearestFlightMaster->GetGUID();
+ auto const& cache = (team == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
+ std::unordered_set seen;
+ std::vector result;
+ for (auto const& [entry, info] : cache)
+ {
+ if (info.zoneId != zoneId || info.taxiNodeId == 0 || info.taxiNodeId == excludeNode)
+ continue;
+ if (seen.insert(info.taxiNodeId).second)
+ result.push_back(info.taxiNodeId);
+ }
+ return result;
}
std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot)
{
std::vector> validDestinations;
- Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
- if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f)
+ FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
+ if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f)
return validDestinations;
- uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(),
- nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(),
- bot->GetTeamId());
+ uint32 fromNode = nearestFlightMaster->taxiNodeId;
if (!fromNode)
return validDestinations;
- std::vector candidateLocations;
- if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
- candidateLocations = GetCityLocations(bot);
+ TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
+ if (!startNode)
+ return validDestinations;
- std::vector hubLocations = GetTravelHubs(bot);
- candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end());
+ uint32 botLevel = bot->GetLevel();
- for (auto const& loc : candidateLocations)
+ // Bots already in a capital shouldn't have another capital picked as a
+ // flight destination — that just shuffles them between cities.
+ bool botInCapital = false;
+ if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
+ botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
+
+ //Simplify destination delection. Its either target cities (Based on config value) or target world.
+ std::vector candidateZones;
+ if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
{
- uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(),
- loc.GetPositionZ(), loc.GetMapId(),
- bot->GetTeamId());
- if (!candidateNode)
- continue;
-
- std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
- if (!path.empty())
- validDestinations.push_back(path);
+ TeamId botTeam = bot->GetTeamId();
+ for (Capital const& capital : capitals)
+ {
+ if (capital.team != TEAM_NEUTRAL && capital.team != botTeam)
+ continue;
+ candidateZones.push_back(capital.zoneId);
+ }
}
+ if (candidateZones.empty())
+ {
+ for (auto const& [zoneId, bracket] : zone2LevelBracket)
+ {
+ if (botLevel < bracket.low || botLevel > bracket.high)
+ continue;
+ if (GetFlightNodesInZone(zoneId, bot->GetTeamId(), fromNode).empty())
+ continue;
+ candidateZones.push_back(zoneId);
+ }
+ }
+
+ if (candidateZones.empty())
+ return validDestinations;
+
+ while (!candidateZones.empty())
+ {
+ uint32 zoneIndex = urand(0, candidateZones.size() - 1);
+ uint32 pickedZone = candidateZones[zoneIndex];
+
+ std::vector usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode);
+
+ if (!usableNodes.empty())
+ {
+ uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)];
+ std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode);
+ if (!path.empty())
+ {
+ validDestinations.push_back(std::move(path));
+ return validDestinations;
+ }
+ }
+
+ candidateZones.erase(candidateZones.begin() + zoneIndex);
+ }
+
return validDestinations;
}
@@ -4472,34 +4507,34 @@ std::vector TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations;
TeamId botTeamId = bot->GetTeamId();
- std::unordered_set validBankerCities;
+ std::unordered_set validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
{
- auto cityIt = bankerToCity.find(loc.entry);
- if (cityIt == bankerToCity.end())
+ Capital const* capital = FindCapitalByBanker(loc.entry);
+ if (!capital)
continue;
- TeamId cityTeamId = cityIt->second.second;
+ TeamId cityTeamId = capital->team;
if (cityTeamId == botTeamId ||
(cityTeamId == TEAM_NEUTRAL)
)
- validBankerCities.insert(cityIt->second.first);
+ validBankerCities.insert(capital->zoneId);
}
// Fallback if no valid cities
if (validBankerCities.empty())
return fallbackLocations;
// Apply weights to valid cities
- std::vector weightedCities;
- for (CityId city : validBankerCities)
+ std::vector weightedCities;
+ for (uint32 zoneId : validBankerCities)
{
- int weight = GetCityWeight(city);
+ int weight = GetCityWeight(zoneId);
if (weight <= 0)
continue;
for (int i = 0; i < weight; ++i)
- weightedCities.push_back(city);
+ weightedCities.push_back(zoneId);
}
// Fallback if no valid cities
@@ -4507,9 +4542,11 @@ std::vector TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations;
// Pick a weighted city randomly, then a random banker in that city
- CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
-
- auto const& bankers = cityToBankers.at(selectedCity);
+ uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
+ Capital const* selectedCapital = FindCapitalByZone(selectedCity);
+ if (!selectedCapital)
+ return fallbackLocations;
+ auto const& bankers = selectedCapital->bankers;
uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
if (locIt != bankerEntryToLocation.end())
@@ -4520,78 +4557,78 @@ std::vector TravelMgr::GetCityLocations(Player* bot)
void TravelMgr::PrepareZone2LevelBracket()
{
- // Classic WoW - Low - level zones
- zone2LevelBracket[1] = {5, 12}; // Dun Morogh
- zone2LevelBracket[12] = {5, 12}; // Elwynn Forest
- zone2LevelBracket[14] = {5, 12}; // Durotar
- zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades
- zone2LevelBracket[141] = {5, 12}; // Teldrassil
- zone2LevelBracket[215] = {5, 12}; // Mulgore
- zone2LevelBracket[3430] = {5, 12}; // Eversong Woods
- zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle
+ // Classic WoW - starter zones
+ zone2LevelBracket[AREA_DUN_MOROGH] = {5, 12};
+ zone2LevelBracket[AREA_ELWYNN_FOREST] = {5, 12};
+ zone2LevelBracket[AREA_DUROTAR] = {5, 12};
+ zone2LevelBracket[AREA_TIRISFAL_GLADES] = {5, 12};
+ zone2LevelBracket[AREA_TELDRASSIL] = {5, 12};
+ zone2LevelBracket[AREA_MULGORE] = {5, 12};
+ zone2LevelBracket[AREA_EVERSONG_WOODS] = {5, 12};
+ zone2LevelBracket[AREA_AZUREMYST_ISLE] = {5, 12};
- // Classic WoW - Mid - level zones
- zone2LevelBracket[17] = {10, 25}; // Barrens
- zone2LevelBracket[38] = {10, 20}; // Loch Modan
- zone2LevelBracket[40] = {10, 21}; // Westfall
- zone2LevelBracket[130] = {10, 23}; // Silverpine Forest
- zone2LevelBracket[148] = {10, 21}; // Darkshore
- zone2LevelBracket[3433] = {10, 22}; // Ghostlands
- zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle
+ // Classic WoW - low level zones
+ zone2LevelBracket[AREA_THE_BARRENS] = {10, 25};
+ zone2LevelBracket[AREA_LOCH_MODAN] = {10, 20};
+ zone2LevelBracket[AREA_WESTFALL] = {10, 21};
+ zone2LevelBracket[AREA_SILVERPINE_FOREST] = {10, 23};
+ zone2LevelBracket[AREA_DARKSHORE] = {10, 21};
+ zone2LevelBracket[AREA_GHOSTLANDS] = {10, 22};
+ zone2LevelBracket[AREA_BLOODMYST_ISLE] = {10, 21};
- // Classic WoW - High - level zones
- zone2LevelBracket[10] = {19, 33}; // Deadwind Pass
- zone2LevelBracket[11] = {21, 30}; // Wetlands
- zone2LevelBracket[44] = {16, 28}; // Redridge Mountains
- zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills
- zone2LevelBracket[331] = {18, 33}; // Ashenvale
- zone2LevelBracket[400] = {24, 36}; // Thousand Needles
- zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains
+ // Classic WoW - mid-level zones
+ zone2LevelBracket[AREA_DUSKWOOD] = {19, 33};
+ zone2LevelBracket[AREA_WETLANDS] = {21, 30};
+ zone2LevelBracket[AREA_REDRIDGE_MOUNTAINS] = {16, 28};
+ zone2LevelBracket[AREA_HILLSBRAD_FOOTHILLS] = {20, 34};
+ zone2LevelBracket[AREA_ASHENVALE] = {18, 33};
+ zone2LevelBracket[AREA_THOUSAND_NEEDLES] = {24, 36};
+ zone2LevelBracket[AREA_STONETALON_MOUNTAINS] = {16, 29};
- // Classic WoW - Higher - level zones
- zone2LevelBracket[3] = {36, 46}; // Badlands
- zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows
- zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh
- zone2LevelBracket[16] = {45, 52}; // Azshara
- zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale
- zone2LevelBracket[45] = {30, 42}; // Arathi Highlands
- zone2LevelBracket[47] = {42, 51}; // Hinterlands
- zone2LevelBracket[51] = {45, 51}; // Searing Gorge
- zone2LevelBracket[357] = {40, 52}; // Feralas
- zone2LevelBracket[405] = {30, 41}; // Desolace
- zone2LevelBracket[440] = {41, 52}; // Tanaris
+ // Classic WoW - 30-52 zones
+ zone2LevelBracket[AREA_BADLANDS] = {36, 46};
+ zone2LevelBracket[AREA_SWAMP_OF_SORROWS] = {36, 46};
+ zone2LevelBracket[AREA_DUSTWALLOW_MARSH] = {35, 46};
+ zone2LevelBracket[AREA_AZSHARA] = {45, 52};
+ zone2LevelBracket[AREA_STRANGLETHORN_VALE] = {32, 47};
+ zone2LevelBracket[AREA_ARATHI_HIGHLANDS] = {30, 42};
+ zone2LevelBracket[AREA_THE_HINTERLANDS] = {42, 51};
+ zone2LevelBracket[AREA_SEARING_GORGE] = {45, 51};
+ zone2LevelBracket[AREA_FERALAS] = {40, 52};
+ zone2LevelBracket[AREA_DESOLACE] = {30, 41};
+ zone2LevelBracket[AREA_TANARIS] = {41, 52};
- // Classic WoW - Top - level zones
- zone2LevelBracket[4] = {52, 57}; // Blasted Lands
- zone2LevelBracket[28] = {50, 60}; // Western Plaguelands
- zone2LevelBracket[46] = {51, 60}; // Burning Steppes
- zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands
- zone2LevelBracket[361] = {47, 57}; // Felwood
- zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater
- zone2LevelBracket[618] = {54, 61}; // Winterspring
- zone2LevelBracket[1377] = {54, 63}; // Silithus
+ // Classic WoW - top level zones
+ zone2LevelBracket[AREA_BLASTED_LANDS] = {52, 57};
+ zone2LevelBracket[AREA_WESTERN_PLAGUELANDS] = {50, 60};
+ zone2LevelBracket[AREA_BURNING_STEPPES] = {51, 60};
+ zone2LevelBracket[AREA_EASTERN_PLAGUELANDS] = {54, 62};
+ zone2LevelBracket[361] = {47, 57}; // Felwood (no AREA_ define)
+ zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater (no AREA_ define)
+ zone2LevelBracket[AREA_WINTERSPRING] = {54, 61};
+ zone2LevelBracket[AREA_SILITHUS] = {54, 63};
- // The Burning Crusade - Zones
- zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula
- zone2LevelBracket[3518] = {64, 70}; // Nagrand
- zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest
- zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley
- zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh
- zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains
- zone2LevelBracket[3523] = {67, 73}; // Netherstorm
- zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas
+ // The Burning Crusade zones
+ zone2LevelBracket[AREA_HELLFIRE_PENINSULA] = {58, 66};
+ zone2LevelBracket[AREA_NAGRAND] = {64, 70};
+ zone2LevelBracket[AREA_TEROKKAR_FOREST] = {62, 73};
+ zone2LevelBracket[AREA_SHADOWMOON_VALLEY] = {66, 73};
+ zone2LevelBracket[AREA_ZANGARMARSH] = {60, 67};
+ zone2LevelBracket[AREA_BLADES_EDGE_MOUNTAINS] = {64, 73};
+ zone2LevelBracket[AREA_NETHERSTORM] = {67, 73};
+ zone2LevelBracket[AREA_ISLE_OF_QUEL_DANAS] = {68, 73};
- // Wrath of the Lich King - Zones
- zone2LevelBracket[65] = {71, 77}; // Dragonblight
- zone2LevelBracket[66] = {74, 80}; // Zul'Drak
- zone2LevelBracket[67] = {77, 80}; // Storm Peaks
- zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier
- zone2LevelBracket[394] = {72, 78}; // Grizzly Hills
- zone2LevelBracket[495] = {68, 74}; // Howling Fjord
- zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest
- zone2LevelBracket[3537] = {68, 75}; // Borean Tundra
- zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin
- zone2LevelBracket[4197] = {79, 80}; // Wintergrasp
+ // Wrath of the Lich King zones
+ zone2LevelBracket[AREA_DRAGONBLIGHT] = {71, 77};
+ zone2LevelBracket[AREA_ZUL_DRAK] = {74, 80};
+ zone2LevelBracket[AREA_THE_STORM_PEAKS] = {77, 80};
+ zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier (no AREA_ define)
+ zone2LevelBracket[AREA_GRIZZLY_HILLS] = {72, 78};
+ zone2LevelBracket[AREA_HOWLING_FJORD] = {68, 74};
+ zone2LevelBracket[AREA_CRYSTALSONG_FOREST] = {77, 80};
+ zone2LevelBracket[AREA_BOREAN_TUNDRA] = {68, 75};
+ zone2LevelBracket[AREA_SHOLAZAR_BASIN] = {75, 80};
+ zone2LevelBracket[AREA_WINTERGRASP] = {79, 80};
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
@@ -4650,13 +4687,15 @@ void TravelMgr::PrepareDestinationCache()
(creatureTemplate->unit_flags & 4096) == 0 &&
creatureTemplate->rank == 0)
{
- uint32 roundX = (x / 50.0f) * 10.0f;
- uint32 roundY = (y / 50.0f) * 10.0f;
- uint32 roundZ = (z / 50.0f) * 10.0f;
+ uint32 roundX = static_cast(std::round(x / 50.0f));
+ uint32 roundY = static_cast(std::round(y / 50.0f));
+ uint32 roundZ = static_cast(std::round(z / 50.0f));
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
}
// FLIGHT MASTERS
+ // Entry 29480 is Grimwing (Storm Peaks)
+ // Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one.
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
@@ -4669,23 +4708,39 @@ void TravelMgr::PrepareDestinationCache()
{
WorldPosition pos(mapId, x, y, z, orient);
if (forHorde)
- hordeFlightMasterCache[guid] = pos;
+ {
+ FlightMasterInfo info;
+ info.pos = pos;
+ info.zoneId = areaId;
+ info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_HORDE);
+ info.templateEntry = templateEntry;
+ info.dbGuid = guid;
+ hordeFlightMasterCache[guid] = info;
+ }
if (forAlliance)
- allianceFlightMasterCache[guid] = pos;
+ {
+ FlightMasterInfo info;
+ info.pos = pos;
+ info.zoneId = areaId;
+ info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_ALLIANCE);
+ info.templateEntry = templateEntry;
+ info.dbGuid = guid;
+ allianceFlightMasterCache[guid] = info;
+ }
flightMastersCount++;
// Zones that have flight masters but no innkeepers — use flight master as hub
static const std::set zonesWithoutInnkeeper = {
- 4, // Blasted Lands (52-57)
- 16, // Azshara (45-52)
- 28, // Western Plaguelands (50-60)
- 46, // Burning Steppes (51-60)
- 51, // Searing Gorge (45-51)
+ AREA_BLASTED_LANDS,
+ AREA_AZSHARA,
+ AREA_WESTERN_PLAGUELANDS,
+ AREA_BURNING_STEPPES,
+ AREA_SEARING_GORGE,
361, // Felwood (47-57)
490, // Un'Goro Crater (49-56)
- 2817, // Crystalsong Forest (77-80)
- 4197 // Wintergrasp (79-80)
+ AREA_CRYSTALSONG_FOREST,
+ AREA_WINTERGRASP
};
if (zonesWithoutInnkeeper.count(areaId))
{
@@ -4756,7 +4811,7 @@ void TravelMgr::PrepareDestinationCache()
// Process temporary caches
for (auto const& [gridTuple, creatureDataList] : tempLocsCache)
{
- if (creatureDataList.size() > 2)
+ if (creatureDataList.size() >= 2)
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h
index f300ae636..99c8c8e4c 100644
--- a/src/Mgr/Travel/TravelMgr.h
+++ b/src/Mgr/Travel/TravelMgr.h
@@ -846,6 +846,21 @@ protected:
class TravelMgr
{
public:
+ struct NpcLocation
+ {
+ WorldLocation loc;
+ uint32 entry;
+ };
+
+ struct FlightMasterInfo
+ {
+ WorldPosition pos;
+ uint32 zoneId; // resolved once at cache load
+ uint32 taxiNodeId; // DBC taxi node nearest to this flight master
+ uint32 templateEntry; // creature template ID (for ObjectGuid construction)
+ uint32 dbGuid; // DB spawn GUID (for ObjectGuid construction)
+ };
+
static TravelMgr& instance()
{
static TravelMgr instance;
@@ -858,12 +873,14 @@ public:
// Navigation
void Init();
- Creature* GetNearestFlightMaster(Player* bot);
- ObjectGuid GetNearestFlightMasterGuid(Player* bot);
+
+ FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const;
std::vector> GetOptimalFlightDestinations(Player* bot);
const std::vector GetTeleportLocations(Player* bot);
const std::vector GetTravelHubs(Player* bot);
std::vector GetCityLocations(Player* bot);
+ std::vector GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const;
+ bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer);
const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
template
@@ -975,8 +992,8 @@ private:
};
// Navigation caches
- std::map allianceFlightMasterCache;
- std::map hordeFlightMasterCache;
+ std::map allianceFlightMasterCache;
+ std::map hordeFlightMasterCache;
std::map> allianceHubsPerLevelCache;
std::map> hordeHubsPerLevelCache;
std::map> bankerLocsPerLevelCache;
diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp
index 3b4996e97..9d25d4ea7 100644
--- a/src/Mgr/Travel/TravelNode.cpp
+++ b/src/Mgr/Travel/TravelNode.cpp
@@ -2467,7 +2467,7 @@ std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode)
TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode);
- if (!startNode || !endNode || startNode->map_id != endNode->map_id)
+ if (!startNode || !endNode)
return {};
auto cacheItr = taxiPathCache.find(fromNode);
From cc6f6c2c3ae7f0e286bde1767d2cef0db69c8a98 Mon Sep 17 00:00:00 2001
From: kadeshar
Date: Sat, 2 May 2026 21:19:37 +0200
Subject: [PATCH 04/19] Thorns reapply fix (#2338)
## Pull Request Description
Allowed druid cast Thorns on target which already have Thorns on him to
extend duration.
Related with: #2290
## How to Test the Changes
1. Invite 2 bot (one of them must be druid which can cast thorns)
2. Select second bot and use commad `cast thorns`
3. Wait until buff timer decrease
4. Use again same command
5. Druid should cast spell
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
To understand 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] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/Class/Druid/Action/DruidActions.cpp | 31 ++++++++++++++++++++++
src/Ai/Class/Druid/Action/DruidActions.h | 6 +++++
2 files changed, 37 insertions(+)
diff --git a/src/Ai/Class/Druid/Action/DruidActions.cpp b/src/Ai/Class/Druid/Action/DruidActions.cpp
index 2eb809480..c01cc4342 100644
--- a/src/Ai/Class/Druid/Action/DruidActions.cpp
+++ b/src/Ai/Class/Druid/Action/DruidActions.cpp
@@ -11,6 +11,22 @@
#include "AoeValues.h"
#include "TargetValue.h"
+namespace
+{
+ bool PrepareThornsTarget(PlayerbotAI* botAI, Unit* target)
+ {
+ if (!target)
+ return false;
+
+ Aura* existingThorns = botAI->GetAura("thorns", target, true);
+ if (!existingThorns)
+ return true;
+
+ target->RemoveOwnedAura(existingThorns, AURA_REMOVE_BY_CANCEL);
+ return true;
+ }
+}
+
std::vector CastAbolishPoisonAction::getAlternatives()
{
return NextAction::merge({ NextAction("cure poison") },
@@ -33,6 +49,21 @@ bool CastLifebloomOnMainTankAction::isUseful()
return !lifebloom || lifebloom->GetStackAmount() < 3 || lifebloom->GetDuration() < 2000;
}
+bool CastThornsAction::Execute(Event event)
+{
+ return PrepareThornsTarget(botAI, GetTarget()) && CastBuffSpellAction::Execute(event);
+}
+
+bool CastThornsOnPartyAction::Execute(Event event)
+{
+ return PrepareThornsTarget(botAI, GetTarget()) && BuffOnPartyAction::Execute(event);
+}
+
+bool CastThornsOnMainTankAction::Execute(Event event)
+{
+ return PrepareThornsTarget(botAI, GetTarget()) && BuffOnMainTankAction::Execute(event);
+}
+
Value* CastEntanglingRootsCcAction::GetTargetValue()
{
return context->GetValue("cc target", "entangling roots");
diff --git a/src/Ai/Class/Druid/Action/DruidActions.h b/src/Ai/Class/Druid/Action/DruidActions.h
index 016c3dfc4..7e02a985f 100644
--- a/src/Ai/Class/Druid/Action/DruidActions.h
+++ b/src/Ai/Class/Druid/Action/DruidActions.h
@@ -114,18 +114,24 @@ class CastThornsAction : public CastBuffSpellAction
{
public:
CastThornsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "thorns") {}
+
+ bool Execute(Event event) override;
};
class CastThornsOnPartyAction : public BuffOnPartyAction
{
public:
CastThornsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "thorns") {}
+
+ bool Execute(Event event) override;
};
class CastThornsOnMainTankAction : public BuffOnMainTankAction
{
public:
CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {}
+
+ bool Execute(Event event) override;
};
class CastLifebloomOnMainTankAction : public BuffOnMainTankAction
From 063eabc16e48f2288d1a1a3fa37dec22c0e033e3 Mon Sep 17 00:00:00 2001
From: kadeshar
Date: Sat, 2 May 2026 21:19:51 +0200
Subject: [PATCH 05/19] Spam guild fix (#2341)
## Pull Request Description
Removed messages in failed attempts of buying tabard.
Related with: #1885
## How to Test the Changes
Invite bot with guild strategy. Spam should not appear.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/Base/Actions/BuyAction.cpp | 8 +-------
src/Ai/Base/Actions/GuildCreateActions.cpp | 2 +-
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/src/Ai/Base/Actions/BuyAction.cpp b/src/Ai/Base/Actions/BuyAction.cpp
index f0729f251..e3d454dd9 100644
--- a/src/Ai/Base/Actions/BuyAction.cpp
+++ b/src/Ai/Base/Actions/BuyAction.cpp
@@ -213,13 +213,7 @@ bool BuyAction::Execute(Event event)
}
}
- if (!vendored)
- {
- botAI->TellError("There are no vendors nearby");
- return false;
- }
-
- return true;
+ return vendored;
}
bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto)
diff --git a/src/Ai/Base/Actions/GuildCreateActions.cpp b/src/Ai/Base/Actions/GuildCreateActions.cpp
index c536475f1..59696ecea 100644
--- a/src/Ai/Base/Actions/GuildCreateActions.cpp
+++ b/src/Ai/Base/Actions/GuildCreateActions.cpp
@@ -296,7 +296,7 @@ bool PetitionTurnInAction::isUseful()
bool BuyTabardAction::Execute(Event /*event*/)
{
- bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"));
+ bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"), true);
if (canBuy && AI_VALUE2(uint32, "item count", chat->FormatQItem(5976)))
return true;
From 94195c3b9b4b4c1fd76fd84a8df4ddacb0ea7443 Mon Sep 17 00:00:00 2001
From: Crow
Date: Sat, 2 May 2026 14:20:03 -0500
Subject: [PATCH 06/19] Bots Don't Autoequip Tools & Other Misc Weapons (#2346)
## Pull Request Description
Solve the rest of #2344
Now, bots won't autoequip any weapon from ITEM_SUBCLASS_WEAPON_MISC,
which includes all of the basic tools and some other crap that they have
no need to autoequip, either. Bots are still eligible to equip those
weapons (such as through the "e" command).
Note that MISC includes the Argent Tournament lances. I've not played
WotLK, but I assume those might be relevant for a strategy. It shouldn't
be a problem though because I've intentionally not made bots ineligible
for MISC weapons; they just won't consider them upgrades on their own.
I also cleaned up ItemUsageValue::QueryItemUsageForEquip to consolidate
checks and so on. None of that should be functional, or I screwed up.
The check for MISC is on lines 219 through 221.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
Activate selfbot. Unequip all weapons and have nothing in the inventory
except for a MISC weapon such as a skinning knife. Whisper self "equip
upgrade"--nothing should happen. Whisper self "e [LINK TO WEAPON]"--the
bot should equip the weapon.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [ ] No, not at all
- - [x] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
There's an extra check but totally meaningless with respect to
performance.
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
They won't auto-equip crap that will prevent them from using abilities.
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
I had GPT-5.4 evaluate different spots where I thought an exclusion
could be added before settling on this one.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/Base/Value/ItemUsageValue.cpp | 57 ++++++++--------------------
1 file changed, 15 insertions(+), 42 deletions(-)
diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp
index c3d976f0f..a1f9688d2 100644
--- a/src/Ai/Base/Value/ItemUsageValue.cpp
+++ b/src/Ai/Base/Value/ItemUsageValue.cpp
@@ -180,19 +180,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
delete pItem;
if (result != EQUIP_ERR_OK && result != EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
- {
return ITEM_USAGE_NONE;
- }
- // Check is unique items are equipped or not
- bool needToCheckUnique = false;
- if (result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
- {
- needToCheckUnique = true;
- }
- else if (itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
- {
- needToCheckUnique = true;
- }
+
+ // Check if unique items are equipped or not
+ bool needToCheckUnique = result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS ||
+ itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE);
if (needToCheckUnique)
{
@@ -206,28 +198,27 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped)
- {
return ITEM_USAGE_NONE; // Item is already equipped
- }
// If not equipped, continue processing
}
- if (itemProto->Class == ITEM_CLASS_QUIVER)
- if (bot->getClass() != CLASS_HUNTER)
- return ITEM_USAGE_NONE;
+ if (itemProto->Class == ITEM_CLASS_QUIVER && bot->getClass() != CLASS_HUNTER)
+ return ITEM_USAGE_NONE;
if (itemProto->Class == ITEM_CLASS_CONTAINER)
{
if (itemProto->SubClass != ITEM_SUBCLASS_CONTAINER)
return ITEM_USAGE_NONE; // Todo add logic for non-bag containers. We want to look at professions/class and
// only replace if non-bag is larger than bag.
-
if (GetSmallestBagSize() >= itemProto->ContainerSlots)
return ITEM_USAGE_NONE;
return ITEM_USAGE_EQUIP;
}
+ if (itemProto->Class == ITEM_CLASS_WEAPON && itemProto->SubClass == ITEM_SUBCLASS_WEAPON_MISC)
+ return ITEM_USAGE_NONE;
+
bool shouldEquip = false;
// uint32 statWeight = sRandomItemMgr.GetLiveStatWeight(bot, itemProto->ItemId);
StatsWeightCalculator calculator(bot);
@@ -254,19 +245,14 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
uint8 dstSlot = botAI->FindEquipSlot(itemProto, NULL_SLOT, true);
// Check if dest wasn't set correctly by CanEquipItem and use FindEquipSlot instead
// This occurs with unique items that are already in the bots bags when CanEquipItem is called
- if (dest == 0)
+ if (dest == 0 && dstSlot != NULL_SLOT)
{
- if (dstSlot != NULL_SLOT)
- {
- // Construct dest from dstSlot
- dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot;
- }
+ // Construct dest from dstSlot
+ dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot;
}
if (dstSlot == EQUIPMENT_SLOT_FINGER1 || dstSlot == EQUIPMENT_SLOT_TRINKET1)
- {
possibleSlots = 2;
- }
// Check weapon case separately to keep things a bit cleaner
bool have2HWeapon = false;
@@ -283,14 +269,9 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
itemProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD2);
// If the bot can Titan Grip, ignore any 2H weapon that isn't a 2H sword, mace, or axe.
- if (bot->CanTitanGrip())
- {
- // If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all.
- if (itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
- {
- return ITEM_USAGE_NONE;
- }
- }
+ // If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all.
+ if (bot->CanTitanGrip() && itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
+ return ITEM_USAGE_NONE;
// Now handle the logic for equipping and possible offhand slots
// If the bot can Dual Wield and:
@@ -317,9 +298,7 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
if (shouldEquipInSlot)
return ITEM_USAGE_EQUIP;
else
- {
return ITEM_USAGE_BAD_EQUIP;
- }
}
ItemTemplate const* oldItemProto = oldItem->GetTemplate();
@@ -328,22 +307,16 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
{
// uint32 oldStatWeight = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId);
if (itemScore || oldScore)
- {
shouldEquipInSlot = itemScore > oldScore * sPlayerbotAIConfig.equipUpgradeThreshold;
- }
}
// Bigger quiver
if (itemProto->Class == ITEM_CLASS_QUIVER)
{
if (!oldItem || oldItemProto->ContainerSlots < itemProto->ContainerSlots)
- {
return ITEM_USAGE_EQUIP;
- }
else
- {
return ITEM_USAGE_NONE;
- }
}
bool existingShouldEquip = true;
From 104a1b9ee1c33c1889ffee0460e998fb94a3f7ab Mon Sep 17 00:00:00 2001
From: Crow
Date: Sat, 2 May 2026 14:20:18 -0500
Subject: [PATCH 07/19] Clean up unnecessary includes in raid strategy and
trigger-context headers (#2347)
## Pull Request Description
This PR trims redundant includes from raid Strategy.h and
TriggerContext.h headers. I noticed a consistent pattern of including
Multiplier.h when it was not needed in Strategy.h and including
AiObjectContext.h in TriggerContext.h when only the narrower
NamedObjectContext.h is needed (both of which I was guilty of also).
Since we make new raid strategies based on existing raid strategies, I
figure let's go for the low-hanging fruit and just fix this so we stop
doing it wrong going forward.
While I was at it, I removed other unnecessary includes but in those two
files only (across dungeon and raid strategies).
Edit: Made a couple of other minor code cleanups I'd been intending to
do. Notably, we shouldn't be including a .cpp in PlayerbotAI.cpp.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
I had GPT-5.4 do the actual work because doing it myself file-by-file
would've been such a snoozefest.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
conf/playerbots.conf.dist | 2 +-
src/Ai/Raid/Aq20/RaidAq20TriggerContext.h | 1 -
src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h | 2 --
src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h | 1 -
src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h | 2 --
src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h | 1 -
src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h | 2 --
src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h | 2 +-
src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h | 1 -
src/Ai/Raid/Icecrown/RaidIccTriggerContext.h | 1 -
src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h | 3 ---
src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h | 2 +-
src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h | 1 -
src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h | 2 +-
src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h | 1 -
src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h | 1 -
src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h | 2 --
src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h | 1 -
src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h | 2 --
src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h | 1 -
src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h | 2 --
src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h | 1 -
src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h | 2 +-
src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h | 1 -
src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h | 2 +-
src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h | 1 -
src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h | 1 -
src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h | 1 -
src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h | 1 -
src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h | 4 ----
src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h | 2 +-
src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h | 1 -
src/Bot/PlayerbotAI.cpp | 4 ++--
33 files changed, 9 insertions(+), 45 deletions(-)
diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist
index 3d0866f44..3e72a5058 100644
--- a/conf/playerbots.conf.dist
+++ b/conf/playerbots.conf.dist
@@ -32,7 +32,7 @@
# LEVELS
# GEAR
# QUESTS
-# ACTIVITIES
+# ACTIVITY
# SPELLS
# STRATEGIES
# RPG STRATEGY
diff --git a/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h b/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h
index b49ae1c6b..b0307ca6a 100644
--- a/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h
+++ b/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidAq20Triggers.h"
diff --git a/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h b/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h
index 97ff7453a..86bcf8e47 100644
--- a/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h
+++ b/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h
@@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDAQ20STRATEGY_H
#define _PLAYERBOT_RAIDAQ20STRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
class RaidAq20Strategy : public Strategy
diff --git a/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h b/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h
index aa6b57c9f..de2ce0058 100644
--- a/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h
+++ b/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidBwlTriggers.h"
diff --git a/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h b/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h
index 4308871c8..e09ea2f3e 100644
--- a/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h
+++ b/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h
@@ -2,8 +2,6 @@
#ifndef _PLAYERBOT_RAIDBWLSTRATEGY_H
#define _PLAYERBOT_RAIDBWLSTRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
class RaidBwlStrategy : public Strategy
diff --git a/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h b/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h
index 0c58f6cbf..c545e10eb 100644
--- a/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h
+++ b/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidEoETriggers.h"
diff --git a/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h b/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h
index eb7a147bd..ba9802116 100644
--- a/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h
+++ b/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h
@@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDEOESTRATEGY_H
#define _PLAYERBOT_RAIDEOESTRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
class RaidEoEStrategy : public Strategy
diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h
index d12b0ce46..35a0f138e 100644
--- a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h
+++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h
@@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDGRUULSLAIRTRIGGERCONTEXT_H
#include "RaidGruulsLairTriggers.h"
-#include "AiObjectContext.h"
+#include "NamedObjectContext.h"
class RaidGruulsLairTriggerContext : public NamedObjectContext
{
diff --git a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h
index ba6f33f07..0d41d57a8 100644
--- a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h
+++ b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h
@@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDGRUULSLAIRSTRATEGY_H
#include "Strategy.h"
-#include "Multiplier.h"
class RaidGruulsLairStrategy : public Strategy
{
diff --git a/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h b/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h
index 64c320c72..83f300466 100644
--- a/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h
+++ b/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidIccTriggers.h"
diff --git a/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h b/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h
index 53967c334..fbd54cc64 100644
--- a/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h
+++ b/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h
@@ -1,10 +1,7 @@
#ifndef _PLAYERBOT_RAIDICCSTRATEGY_H
#define _PLAYERBOT_RAIDICCSTRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
-#include "RaidIccMultipliers.h"
class RaidIccStrategy : public Strategy
{
diff --git a/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h b/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h
index e3f606c94..a9c430734 100644
--- a/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h
+++ b/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h
@@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDKARAZHANTRIGGERCONTEXT_H
#include "RaidKarazhanTriggers.h"
-#include "AiObjectContext.h"
+#include "NamedObjectContext.h"
class RaidKarazhanTriggerContext : public NamedObjectContext
{
diff --git a/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h b/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h
index 7d6b16dee..4f95bf7b4 100644
--- a/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h
+++ b/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h
@@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDKARAZHANSTRATEGY_H_
#include "Strategy.h"
-#include "Multiplier.h"
class RaidKarazhanStrategy : public Strategy
{
diff --git a/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h b/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h
index 525fe496e..482152e0e 100644
--- a/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h
+++ b/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h
@@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H
#include "RaidMagtheridonTriggers.h"
-#include "AiObjectContext.h"
+#include "NamedObjectContext.h"
class RaidMagtheridonTriggerContext : public NamedObjectContext
{
diff --git a/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h b/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h
index 7b8ab8f9b..4d21464ae 100644
--- a/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h
+++ b/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h
@@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H
#include "Strategy.h"
-#include "Multiplier.h"
class RaidMagtheridonStrategy : public Strategy
{
diff --git a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h
index a62d851dc..1f694fe65 100644
--- a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h
+++ b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "BossAuraTriggers.h"
#include "NamedObjectContext.h"
#include "RaidMcTriggers.h"
diff --git a/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h b/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h
index 45b503e93..6e77910ec 100644
--- a/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h
+++ b/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h
@@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDMCSTRATEGY_H
#define _PLAYERBOT_RAIDMCSTRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
class RaidMcStrategy : public Strategy
diff --git a/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h b/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h
index 4d1557d56..83afc273d 100644
--- a/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h
+++ b/src/Ai/Raid/Naxxramas/RaidNaxxTriggerContext.h
@@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidNaxxTriggers.h"
diff --git a/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h
index 4b8a9a7c0..d2ce821a8 100644
--- a/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h
+++ b/src/Ai/Raid/Naxxramas/Strategy/RaidNaxxStrategy.h
@@ -2,8 +2,6 @@
#ifndef _PLAYERBOT_RAIDNAXXSTRATEGY_H
#define _PLAYERBOT_RAIDNAXXSTRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
class RaidNaxxStrategy : public Strategy
diff --git a/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h b/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h
index b8a1f4b31..3c1d40692 100644
--- a/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h
+++ b/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidOsTriggers.h"
diff --git a/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h b/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h
index 44983f1fa..0d9ae7871 100644
--- a/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h
+++ b/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h
@@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDOSSTRATEGY_H
#define _PLAYERBOT_RAIDOSSTRATEGY_H
-#include "AiObjectContext.h"
-#include "Multiplier.h"
#include "Strategy.h"
class RaidOsStrategy : public Strategy
diff --git a/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h b/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h
index dba18f564..daf624a0b 100644
--- a/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h
+++ b/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h
@@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidOnyxiaTriggers.h"
diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h
index 737fd3a38..5b0f8d5e3 100644
--- a/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h
+++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h
@@ -7,7 +7,7 @@
#define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H
#include "RaidSSCTriggers.h"
-#include "AiObjectContext.h"
+#include "NamedObjectContext.h"
class RaidSSCTriggerContext : public NamedObjectContext
{
diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h
index a994600ba..08d315d5a 100644
--- a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h
+++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h
@@ -7,7 +7,6 @@
#define _PLAYERBOT_RAIDSSCSTRATEGY_H_
#include "Strategy.h"
-#include "Multiplier.h"
class RaidSSCStrategy : public Strategy
{
diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h
index c6b4922d7..0bf1d0fdc 100644
--- a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h
+++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h
@@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H
#include "RaidTempestKeepTriggers.h"
-#include "AiObjectContext.h"
+#include "NamedObjectContext.h"
class RaidTempestKeepTriggerContext : public NamedObjectContext
{
diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h
index 77fd29c36..b19600bab 100644
--- a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h
+++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h
@@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_
#include "Strategy.h"
-#include "Multiplier.h"
class RaidTempestKeepStrategy : public Strategy
{
diff --git a/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h b/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h
index e4243fb10..e093f5797 100644
--- a/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h
+++ b/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h
@@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidUlduarTriggers.h"
#include "BossAuraTriggers.h"
diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h
index bb2feefe4..c391f6bdb 100644
--- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h
+++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h
@@ -2,7 +2,6 @@
#ifndef _PLAYERBOT_RAIDULDUARSTRATEGY_H
#define _PLAYERBOT_RAIDULDUARSTRATEGY_H
-#include "AiObjectContext.h"
#include "Strategy.h"
class RaidUlduarStrategy : public Strategy
diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h
index 6566793fd..6cb5e0f38 100644
--- a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h
+++ b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h
@@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H
-#include "AiObjectContext.h"
#include "BossAuraTriggers.h"
#include "NamedObjectContext.h"
#include "RaidVoATriggers.h"
diff --git a/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h b/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h
index 04ed2ac3a..c30261fe8 100644
--- a/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h
+++ b/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h
@@ -3,10 +3,6 @@
#define _PLAYERBOT_RAIDVOASTRATEGY_H
#include "Strategy.h"
-#include "PlayerbotAI.h"
-#include "string"
-#include "Trigger.h"
-#include "vector"
class RaidVoAStrategy : public Strategy
{
diff --git a/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h b/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h
index 5be8bad7f..cb8bac864 100644
--- a/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h
+++ b/src/Ai/Raid/ZulAman/RaidZulAmanTriggerContext.h
@@ -7,7 +7,7 @@
#define _PLAYERBOT_RAIDZULAMANTRIGGERCONTEXT_H
#include "RaidZulAmanTriggers.h"
-#include "AiObjectContext.h"
+#include "NamedObjectContext.h"
class RaidZulAmanTriggerContext : public NamedObjectContext
{
diff --git a/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h b/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h
index c49e08888..2cb5e8171 100644
--- a/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h
+++ b/src/Ai/Raid/ZulAman/Strategy/RaidZulAmanStrategy.h
@@ -7,7 +7,6 @@
#define _PLAYERBOT_RAIDZULAMANSTRATEGY_H_
#include "Strategy.h"
-#include "Multiplier.h"
class RaidZulAmanStrategy : public Strategy
{
diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp
index 357678928..8feb87cb5 100644
--- a/src/Bot/PlayerbotAI.cpp
+++ b/src/Bot/PlayerbotAI.cpp
@@ -54,9 +54,9 @@
#include "Unit.h"
#include "UpdateTime.h"
#include "Vehicle.h"
-#include "../../../../src/server/scripts/Spells/spell_dk.cpp"
-const int SPELL_TITAN_GRIP = 49152;
+constexpr uint32 SPELL_TITAN_GRIP = 49152;
+constexpr uint32 SPELL_DK_FROST_PRESENCE = 48263;
std::vector PlayerbotAI::dispel_whitelist = {
"mutating injection",
From ccce14238e6f5842e7ee90a3f63c3165fc2576d1 Mon Sep 17 00:00:00 2001
From: Keleborn <22352763+Celandriel@users.noreply.github.com>
Date: Sun, 3 May 2026 07:16:34 -0700
Subject: [PATCH 08/19] Core Update, change to DeserterCheck and signature
(#2354)
## Pull Request Description
Required change for
https://github.com/azerothcore/azerothcore-wotlk/pull/24641
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [ ] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [ ] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [ ] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [ ] Stability is not compromised.
- - [ ] Performance impact is understood, tested, and acceptable.
- - [ ] Added logic complexity is justified and explained.
- - [ ] Any new bot dialogue lines are translated.
- - [ ] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/Base/Actions/BattleGroundJoinAction.cpp | 2 +-
src/Ai/Base/Trigger/PvpTriggers.cpp | 2 +-
src/Bot/Factory/RandomPlayerbotFactory.cpp | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Ai/Base/Actions/BattleGroundJoinAction.cpp b/src/Ai/Base/Actions/BattleGroundJoinAction.cpp
index ab897a1b2..58ee42cfa 100644
--- a/src/Ai/Base/Actions/BattleGroundJoinAction.cpp
+++ b/src/Ai/Base/Actions/BattleGroundJoinAction.cpp
@@ -343,7 +343,7 @@ bool BGJoinAction::isUseful()
return false;
// check Deserter debuff
- if (!bot->CanJoinToBattleground())
+ if (bot->IsDeserter())
return false;
// check if has free queue slots (pointless as already making sure not in queue)
diff --git a/src/Ai/Base/Trigger/PvpTriggers.cpp b/src/Ai/Base/Trigger/PvpTriggers.cpp
index 31fcd0357..b20c5df97 100644
--- a/src/Ai/Base/Trigger/PvpTriggers.cpp
+++ b/src/Ai/Base/Trigger/PvpTriggers.cpp
@@ -297,7 +297,7 @@ bool PlayerWantsInBattlegroundTrigger::IsActive()
if (bot->GetBattleground() && bot->GetBattleground()->GetStatus() == STATUS_IN_PROGRESS)
return false;
- if (!bot->CanJoinToBattleground())
+ if (bot->IsDeserter())
return false;
return true;
diff --git a/src/Bot/Factory/RandomPlayerbotFactory.cpp b/src/Bot/Factory/RandomPlayerbotFactory.cpp
index 617e4006c..530715191 100644
--- a/src/Bot/Factory/RandomPlayerbotFactory.cpp
+++ b/src/Bot/Factory/RandomPlayerbotFactory.cpp
@@ -619,7 +619,7 @@ void RandomPlayerbotFactory::CreateRandomBots()
else
password = accountName;
- AccountMgr::CreateAccount(accountName, password);
+ sAccountMgr->CreateAccount(accountName, password);
LOG_DEBUG("playerbots", "Account {} created for random bots", accountName.c_str());
}
From 38caa1daa7399f272267cd6ffc9d2d4f154b7f3e Mon Sep 17 00:00:00 2001
From: Keleborn <22352763+Celandriel@users.noreply.github.com>
Date: Sun, 3 May 2026 12:41:45 -0700
Subject: [PATCH 09/19] Randombots respect realm PVP setting (#2342)
## Pull Request Description
Fix an issue where bots would eventually have pvp set by reset. THis
ensures bot pvp states are consistent with realm type.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
Corrects behavior to match server intent.
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
searching code, writing it.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Bot/RandomPlayerbotMgr.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp
index 5c0922fb9..3c0a9054c 100644
--- a/src/Bot/RandomPlayerbotMgr.cpp
+++ b/src/Bot/RandomPlayerbotMgr.cpp
@@ -2077,7 +2077,7 @@ void RandomPlayerbotMgr::Refresh(Player* bot)
bot->DurabilityRepairAll(false, 1.0f, false);
bot->SetFullHealth();
- bot->SetPvP(true);
+ bot->SetPvP(sWorld->IsPvPRealm());
PlayerbotFactory factory(bot, bot->GetLevel());
factory.Refresh();
@@ -2642,6 +2642,7 @@ void RandomPlayerbotMgr::OnPlayerLogin(Player* player)
{
// ObjectGuid::LowType guid = player->GetGUID().GetCounter(); //not used, conditional could be rewritten for
// simplicity. line marked for removal.
+ player->SetPvP(sWorld->IsPvPRealm());
}
else
{
From 5d9761c9e857dfbdda29ef166b361bd2e342c04c Mon Sep 17 00:00:00 2001
From: Crow
Date: Sat, 9 May 2026 00:39:32 -0500
Subject: [PATCH 10/19] Implement Battle for Mount Hyjal Strategies (#2258)
### Contingent on
https://github.com/mod-playerbots/mod-playerbots/pull/2295/
## Pull Request Description
This PR implements raid strategies for all bosses in everybody's
favorite TBC instance, the Battle for Mount Hyjal. As before, I have
designed these all to work with IP with 50% damage and healing. I also
did not merge the 1.88x buff to Vanilla & TBC healing items that IP
recently implemented.
The next post will outline all implemented strategies.
Note: Set to draft for now as I may tweak Archimonde some more, but I
generally consider Hyjal complete, subject to comments.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
As with previous strategies, I've worked within the existing context of
actions/multipliers/triggers. This strategy does implement new spell
hooks, but I don't think that is problematic for performance, and I'll
explain why they are necessary in the next post.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [ ] No, not at all
- - [x] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
There are many new triggers, multipliers, and actions, but they will be
evaluated only if the "hyjal" strategy is added. Additionally, I've
attempted to order and implement checks in a manner to limit performance
impact and have tested with .pmon active. In general, I consider
performance to be highly important so am always working on ways to limit
the impact (e.g., trying to use the most targeted grid searches
available when needed).
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
Only in the Hyjal instance, for obvious reasons.
- Does this change add new decision branches or increase maintenance
complexity?
- - [ ] No
- - [x] Yes (**explain below**)
New decision branches apply only in the new Hyjal instance, for obvious
reasons. Maintenance complexity should not be increased as code outside
of the new Hyjal files is not impacted, except to the extent needed to
register and implement the strategy in the same manner as all existing
strategies. Exception: As noted, I did add new spell hooks, but if they
become problematic, they can easily be removed.
## Messages to Translate
- Does this change add bot messages to translate?
- - [x] No
- - [ ] Yes (**list messages in the table**)
| Message key | Default message |
| --------------- | ------------------ |
| | |
| | |
## AI Assistance
- Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
Claude Sonnet 4.6 was used for more complex implementation and
occasionally GPT-5 mini was used for simple questions. I do not use AI
for brainstorming or developing strategies, only for implementation and
review of code. Most of this was written by me directly, but most
notably I needed AI support to implement the spell hooks and
triggers/actions that relied on them. Everything was reviewed and tested
many times.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
Caveat: The full impact of implementing the spell hooks on a broad scale
is beyond my knowledge to evaluate.
- - [x] Added logic complexity is justified and explained.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---------
Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash
Co-authored-by: Revision
Co-authored-by: kadeshar
---
.../Action/RaidHyjalSummitActions.cpp | 1200 +++++++++++++++++
.../Action/RaidHyjalSummitActions.h | 277 ++++
.../Multiplier/RaidHyjalSummitMultipliers.cpp | 295 ++++
.../Multiplier/RaidHyjalSummitMultipliers.h | 125 ++
.../RaidHyjalSummitActionContext.h | 218 +++
.../RaidHyjalSummitTriggerContext.h | 218 +++
.../Strategy/RaidHyjalSummitStrategy.cpp | 137 ++
.../Strategy/RaidHyjalSummitStrategy.h | 22 +
.../Trigger/RaidHyjalSummitTriggers.cpp | 357 +++++
.../Trigger/RaidHyjalSummitTriggers.h | 271 ++++
.../Util/RaidHyjalSummitHelpers.cpp | 268 ++++
.../HyjalSummit/Util/RaidHyjalSummitHelpers.h | 143 ++
.../Util/RaidHyjalSummitScripts.cpp | 211 +++
src/Ai/Raid/RaidStrategyContext.h | 3 +
src/Bot/Engine/BuildSharedActionContexts.cpp | 2 +
src/Bot/Engine/BuildSharedTriggerContexts.cpp | 2 +
src/Bot/PlayerbotAI.cpp | 14 +-
src/Script/Playerbots.cpp | 2 +
18 files changed, 3760 insertions(+), 5 deletions(-)
create mode 100644 src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.cpp
create mode 100644 src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.h
create mode 100644 src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.cpp
create mode 100644 src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.h
create mode 100644 src/Ai/Raid/HyjalSummit/RaidHyjalSummitActionContext.h
create mode 100644 src/Ai/Raid/HyjalSummit/RaidHyjalSummitTriggerContext.h
create mode 100644 src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.cpp
create mode 100644 src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.h
create mode 100644 src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.cpp
create mode 100644 src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.h
create mode 100644 src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.cpp
create mode 100644 src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.h
create mode 100644 src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitScripts.cpp
diff --git a/src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.cpp b/src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.cpp
new file mode 100644
index 000000000..4a5f36d02
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.cpp
@@ -0,0 +1,1200 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "RaidHyjalSummitActions.h"
+#include "RaidHyjalSummitHelpers.h"
+#include "Playerbots.h"
+#include "RaidBossHelpers.h"
+#include "Timer.h"
+
+using namespace HyjalSummitHelpers;
+
+// General
+
+bool HyjalSummitEraseTrackersAction::Execute(Event /*event*/)
+{
+ const ObjectGuid guid = bot->GetGUID();
+ const uint32 instanceId = bot->GetMap()->GetInstanceId();
+
+ bool erased = false;
+ if (botAI->IsTank(bot))
+ {
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ {
+ if (kazrogalTankStep.erase(guid) > 0)
+ erased = true;
+
+ if (isBelowManaThreshold.erase(guid) > 0)
+ erased = true;
+ }
+
+ if (!AI_VALUE2(Unit*, "find target", "azgalor"))
+ {
+ if (azgalorTankStep.erase(guid) > 0)
+ erased = true;
+
+ if (rainOfFirePosition.erase(instanceId) > 0)
+ erased = true;
+ }
+
+ return erased;
+ }
+ else
+ {
+ if (!AI_VALUE2(Unit*, "find target", "rage winterchill"))
+ {
+ if (hasReachedWinterchillPosition.erase(guid) > 0)
+ erased = true;
+
+ if (deathAndDecayPosition.erase(instanceId) > 0)
+ erased = true;
+ }
+
+ if (!AI_VALUE2(Unit*, "find target", "anetheron") &&
+ hasReachedAnetheronPosition.erase(guid) > 0)
+ erased = true;
+
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal") &&
+ isBelowManaThreshold.erase(guid) > 0)
+ erased = true;
+
+ if (!AI_VALUE2(Unit*, "find target", "archimonde") &&
+ doomfireTrails.erase(instanceId) > 0)
+ erased = true;
+
+ return erased;
+ }
+}
+
+// Rage Winterchill
+
+bool RageWinterchillMisdirectBossToMainTankAction::Execute(Event /*event*/)
+{
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ if (!winterchill)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return false;
+
+ if (botAI->CanCastSpell("misdirection", mainTank))
+ return botAI->CastSpell("misdirection", mainTank);
+
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) &&
+ botAI->CanCastSpell("steady shot", winterchill))
+ return botAI->CastSpell("steady shot", winterchill);
+
+ return false;
+}
+
+// Position is back towards the center of the base to give some more room to manuever
+bool RageWinterchillMainTankPositionBossAction::Execute(Event /*event*/)
+{
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ if (!winterchill)
+ return false;
+
+ if (bot->GetVictim() != winterchill)
+ return Attack(winterchill);
+
+ if (winterchill->GetVictim() == bot)
+ {
+ const Position& position = WINTERCHILL_TANK_POSITION;
+ const float distToPosition =
+ bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY());
+
+ if (distToPosition > 4.0f)
+ {
+ float moveX, moveY, moveZ;
+ constexpr float moveDist = 5.0f;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, true);
+ }
+ }
+ }
+
+ return false;
+}
+
+// Spread ranged DPS in a circle initially--after the initial spread, movement is free
+bool RageWinterchillSpreadRangedInCircleAction::Execute(Event /*event*/)
+{
+ RangedGroups groups = GetRangedGroups(botAI, bot);
+
+ if (groups.healers.empty() && groups.rangedDps.empty())
+ return false;
+
+ const ObjectGuid guid = bot->GetGUID();
+
+ if (!hasReachedWinterchillPosition[guid])
+ {
+ auto [botIndex, count] = GetBotCircleIndexAndCount(botAI, bot, groups);
+ const float radius = botAI->IsHeal(bot) ? 25.0f : 35.0f;
+ float angle = 0.0f;
+
+ constexpr float arcSpan = 2.0f * M_PI;
+ constexpr float arcCenter = 0.0f;
+ constexpr float arcStart = arcCenter - arcSpan / 2.0f;
+
+ angle = (count == 1) ? arcCenter :
+ (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1));
+
+ const Position& position = WINTERCHILL_TANK_POSITION;
+ float targetX = position.GetPositionX() + radius * std::cos(angle);
+ float targetY = position.GetPositionY() + radius * std::sin(angle);
+
+ if (bot->GetExactDist2d(targetX, targetY) > 2.0f)
+ {
+ float moveX, moveY, moveZ;
+ constexpr float moveDist = 10.0f;
+ if (GetGroundedStepPosition(bot, targetX, targetY, moveDist,
+ moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, false);
+ }
+ }
+ else
+ {
+ hasReachedWinterchillPosition[guid] = true;
+ }
+ }
+
+ return false;
+}
+
+bool RageWinterchillMeleeGetOutOfDeathAndDecayAction::Execute(Event /*event*/)
+{
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ if (!winterchill)
+ return false;
+
+ DeathAndDecayData* data =
+ GetActiveWinterchillDeathAndDecay(bot->GetMap()->GetInstanceId());
+ if (!data)
+ return false;
+
+ constexpr float moveDist = 10.0f;
+
+ const float centerX = data->position.GetPositionX();
+ const float centerY = data->position.GetPositionY();
+ const float currentDistance = bot->GetExactDist2d(centerX, centerY);
+ float escapeAngle =
+ std::atan2(bot->GetPositionY() - centerY, bot->GetPositionX() - centerX);
+
+ if (currentDistance <= 0.1f)
+ {
+ escapeAngle = std::atan2(centerY - winterchill->GetPositionY(),
+ centerX - winterchill->GetPositionX());
+ }
+
+ for (float delta = 0.0f; delta <= M_PI; delta += M_PI / 8.0f)
+ {
+ for (float angle : { escapeAngle + delta, escapeAngle - delta })
+ {
+ if (delta == 0.0f && angle != escapeAngle)
+ continue;
+
+ const float targetX = centerX + std::cos(angle) * DEATH_AND_DECAY_SAFE_RADIUS;
+ const float targetY = centerY + std::sin(angle) * DEATH_AND_DECAY_SAFE_RADIUS;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, targetX, targetY, moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, false);
+ }
+ }
+ }
+
+ return false;
+}
+
+// Anetheron
+
+bool AnetheronMisdirectBossAndInfernalsToTanksAction::Execute(Event /*event*/)
+{
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (!anetheron)
+ return false;
+
+ if (anetheron->GetHealthPct() > 95.0f)
+ {
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return false;
+
+ if (botAI->CanCastSpell("misdirection", mainTank))
+ return botAI->CastSpell("misdirection", mainTank);
+
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) &&
+ botAI->CanCastSpell("steady shot", anetheron))
+ return botAI->CastSpell("steady shot", anetheron);
+ }
+
+ if (Unit* infernal = AI_VALUE2(Unit*, "find target", "towering infernal");
+ infernal && infernal->GetHealthPct() > 50.0f)
+ {
+ Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0);
+ if (!firstAssistTank)
+ return false;
+
+ if (botAI->CanCastSpell("misdirection", firstAssistTank))
+ return botAI->CastSpell("misdirection", firstAssistTank);
+
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) &&
+ botAI->CanCastSpell("steady shot", infernal))
+ return botAI->CastSpell("steady shot", infernal);
+ }
+
+ return false;
+}
+
+// Position is back towards the center of the base, near the crossroads
+bool AnetheronMainTankPositionBossAction::Execute(Event /*event*/)
+{
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (!anetheron)
+ return false;
+
+ MarkTargetWithSquare(bot, anetheron);
+ SetRtiTarget(botAI, "square", anetheron);
+
+ if (bot->GetVictim() != anetheron)
+ return Attack(anetheron);
+
+ if (anetheron->GetVictim() == bot)
+ {
+ const Position& position = ANETHERON_TANK_POSITION;
+ const float distToPosition =
+ bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY());
+
+ if (distToPosition > 4.0f)
+ {
+ float moveX, moveY, moveZ;
+ constexpr float moveDist = 5.0f;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, true);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool AnetheronSpreadRangedInCircleAction::Execute(Event /*event*/)
+{
+ RangedGroups groups = GetRangedGroups(botAI, bot);
+
+ if (groups.healers.empty() && groups.rangedDps.empty())
+ return false;
+
+ const ObjectGuid guid = bot->GetGUID();
+
+ if (!hasReachedAnetheronPosition[guid])
+ {
+ auto [botIndex, count] = GetBotCircleIndexAndCount(botAI, bot, groups);
+ const float radius = botAI->IsHeal(bot) ? 27.0f : 34.0f;
+ float angle = 0.0f;
+
+ constexpr float arcSpan = M_PI * 2.0f;
+ constexpr float arcCenter = 0.0f;
+ constexpr float arcStart = arcCenter - arcSpan / 2.0f;
+
+ angle = (count == 1) ? arcCenter :
+ (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1));
+
+ const Position& position = ANETHERON_TANK_POSITION;
+
+ float targetX = position.GetPositionX() + radius * std::sin(angle);
+ float targetY = position.GetPositionY() + radius * std::cos(angle);
+
+ if (bot->GetExactDist2d(targetX, targetY) > 2.0f)
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, targetX, targetY, moveDist,
+ moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, false);
+ }
+ }
+ else
+ {
+ hasReachedAnetheronPosition[guid] = true;
+ }
+ }
+ else
+ {
+ constexpr float safeDistFromPlayer = 6.0f;
+ constexpr uint32 minInterval = 2000;
+ if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer))
+ return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval);
+ }
+
+ return false;
+}
+
+// Run to the nearest of two Infernal tanking spots, East and West of Anetheron
+bool AnetheronBringInfernalToInfernalTankAction::Execute(Event /*event*/)
+{
+ const Position& position = GetClosestInfernalTankPosition(bot);
+ if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f)
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_FORCED, true, false);
+ }
+ }
+
+ return false;
+}
+
+// Pick up the Infernal and bring it to the closest Infernal tanking position
+bool AnetheronFirstAssistTankPickUpInfernalsAction::Execute(Event /*event*/)
+{
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (!anetheron)
+ return false;
+
+ Player* infernoTarget = GetInfernoTarget(anetheron);
+ if (infernoTarget && infernoTarget != bot)
+ {
+ float distToInfernoTarget = bot->GetExactDist2d(infernoTarget);
+ if (distToInfernoTarget > 5.0f)
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, infernoTarget->GetPositionX(),
+ infernoTarget->GetPositionY(), moveDist,
+ moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_FORCED, true, false);
+ }
+ }
+ }
+
+ Unit* infernal = AI_VALUE2(Unit*, "find target", "towering infernal");
+ if (!infernal)
+ return false;
+
+ MarkTargetWithDiamond(bot, infernal);
+ SetRtiTarget(botAI, "diamond", infernal);
+
+ if (bot->GetVictim() != infernal)
+ return Attack(infernal);
+
+ if ((infernoTarget && infernoTarget == bot) ||
+ (infernal->GetVictim() == bot && bot->IsWithinMeleeRange(infernal)))
+ {
+ const Position& position = GetClosestInfernalTankPosition(bot);
+ const float distToPosition =
+ bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY());
+
+ if (distToPosition > 3.0f)
+ {
+ constexpr float moveDist = 5.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, true);
+ }
+ }
+ }
+
+ return false;
+}
+
+// Only nearbyish ranged DPS should attack Infernals
+bool AnetheronAssignDpsPriorityAction::Execute(Event /*event*/)
+{
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (!anetheron)
+ return false;
+
+ if (botAI->IsMelee(bot))
+ {
+ SetRtiTarget(botAI, "square", anetheron);
+
+ if (bot->GetVictim() != anetheron)
+ return Attack(anetheron);
+
+ return false;
+ }
+ if (Unit* infernal = AI_VALUE2(Unit*, "find target", "towering infernal"))
+ {
+ constexpr float safeDistFromInfernal = 10.0f;
+ constexpr uint32 minInterval = 0;
+ if (infernal->GetVictim() != bot &&
+ bot->GetDistance2d(infernal) < safeDistFromInfernal)
+ {
+ return FleePosition(infernal->GetPosition(), safeDistFromInfernal, minInterval);
+ }
+
+ if (anetheron->GetHealthPct() > 10.0f && botAI->IsRangedDps(bot) &&
+ bot->GetDistance2d(infernal) < 50.0f)
+ {
+ if (Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0);
+ !firstAssistTank || infernal->GetVictim() == firstAssistTank)
+ {
+ SetRtiTarget(botAI, "diamond", infernal);
+
+ if (bot->GetTarget() != infernal->GetGUID())
+ return Attack(infernal);
+ }
+ }
+ }
+ else if (botAI->IsRangedDps(bot))
+ {
+ SetRtiTarget(botAI, "square", anetheron);
+
+ if (bot->GetTarget() != anetheron->GetGUID())
+ return Attack(anetheron);
+ }
+
+ return false;
+}
+
+// Kaz'rogal
+
+bool KazrogalMisdirectBossToMainTankAction::Execute(Event /*event*/)
+{
+ Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal");
+ if (!kazrogal)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return false;
+
+ if (botAI->CanCastSpell("misdirection", mainTank))
+ return botAI->CastSpell("misdirection", mainTank);
+
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) &&
+ botAI->CanCastSpell("steady shot", kazrogal))
+ return botAI->CastSpell("steady shot", kazrogal);
+
+ return false;
+}
+
+// Position is near the gate so the raid can get start on DPS ASAP
+bool KazrogalMainTankPositionBossAction::Execute(Event /*event*/)
+{
+ Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal");
+ if (!kazrogal)
+ return false;
+
+ if (bot->GetVictim() != kazrogal)
+ return Attack(kazrogal);
+
+ if (kazrogal->GetVictim() == bot && bot->IsWithinMeleeRange(kazrogal))
+ {
+ const ObjectGuid guid = bot->GetGUID();
+ TankPositionState state = kazrogalTankStep.count(guid) ?
+ kazrogalTankStep[guid] : TankPositionState::MovingToTransition;
+
+ constexpr float maxDistance = 2.0f;
+ const Position& position = state == TankPositionState::MovingToTransition ?
+ KAZROGAL_TANK_TRANSITION_POSITION : KAZROGAL_TANK_FINAL_POSITION;
+ const float distToPosition = bot->GetExactDist2d(position);
+
+ if (distToPosition > maxDistance)
+ {
+ constexpr float moveDist = 5.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, true);
+ }
+ }
+
+ if (state == TankPositionState::MovingToTransition && distToPosition <= maxDistance)
+ {
+ kazrogalTankStep[guid] = TankPositionState::MovingToFinal;
+ }
+ else if (state != TankPositionState::MovingToTransition &&
+ distToPosition <= maxDistance)
+ {
+ const float orientation = atan2(kazrogal->GetPositionY() - bot->GetPositionY(),
+ kazrogal->GetPositionX() - bot->GetPositionX());
+ bot->SetFacingTo(orientation);
+ kazrogalTankStep[guid] = TankPositionState::Positioned;
+ }
+ }
+
+ return false;
+}
+
+// To spread cleave damage
+bool KazrogalAssistTanksMoveInFrontOfBossAction::Execute(Event /*event*/)
+{
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return false;
+
+ if (bot->GetExactDist2d(mainTank) > 4.0f)
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, mainTank->GetPositionX(), mainTank->GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, false);
+ }
+ }
+
+ return false;
+}
+
+bool KazrogalSpreadRangedInArcAction::Execute(Event /*event*/)
+{
+ Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal");
+ if (!kazrogal)
+ return false;
+
+ Group* group = bot->GetGroup();
+ if (!group)
+ return false;
+
+ std::vector rangedMembers;
+ for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
+ {
+ Player* member = ref->GetSource();
+ if (!member || !botAI->IsRanged(member))
+ continue;
+
+ rangedMembers.push_back(member);
+ }
+
+ if (rangedMembers.empty())
+ return false;
+
+ size_t count = rangedMembers.size();
+ auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot);
+ size_t botIndex = (findIt != rangedMembers.end()) ?
+ std::distance(rangedMembers.begin(), findIt) : 0;
+
+ constexpr float arcSpan = M_PI / 3.0f;
+ constexpr float arcCenter = 4.225f;
+ constexpr float arcStart = arcCenter - arcSpan / 2.0f;
+
+ constexpr float radius = 20.0f;
+ float angle = (count == 1) ? arcCenter :
+ (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1));
+
+ float targetX = kazrogal->GetPositionX() + radius * std::cos(angle);
+ float targetY = kazrogal->GetPositionY() + radius * std::sin(angle);
+
+ if (bot->GetExactDist2d(targetX, targetY) > 0.5f)
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, targetX, targetY, moveDist,
+ moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, false);
+ }
+ }
+
+ return false;
+}
+
+bool KazrogalLowManaBotTakeDefensiveMeasuresAction::Execute(Event /*event*/)
+{
+ switch (bot->getClass())
+ {
+ case CLASS_HUNTER:
+ if (!botAI->HasAura("aspect of the viper", bot) &&
+ botAI->CanCastSpell("aspect of the viper", bot))
+ {
+ return botAI->CastSpell("aspect of the viper", bot);
+ }
+ return false;
+
+ case CLASS_WARLOCK:
+ if (botAI->CanCastSpell("life tap", bot) &&
+ botAI->CastSpell("life tap", bot))
+ {
+ return true;
+ }
+ break;
+
+ case CLASS_MAGE:
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MARK_OF_KAZROGAL)) &&
+ bot->GetPower(POWER_MANA) <= 1200 && botAI->CanCastSpell("ice block", bot) &&
+ botAI->CastSpell("ice block", bot))
+ {
+ return true;
+ }
+ break;
+
+ case CLASS_PALADIN:
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MARK_OF_KAZROGAL)) &&
+ bot->GetPower(POWER_MANA) <= 1200 && botAI->CanCastSpell("divine shield", bot) &&
+ botAI->CastSpell("divine shield", bot))
+ {
+ return true;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (bot->GetPower(POWER_MANA) <= 3200)
+ isBelowManaThreshold.try_emplace(bot->GetGUID(), true);
+
+ constexpr float safeDistance = 16.0f;
+
+ Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance);
+ if (!nearestPlayer)
+ return false;
+
+ const float currentDistance = bot->GetDistance2d(nearestPlayer);
+ if (currentDistance < safeDistance)
+ {
+ Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal");
+ if (!kazrogal)
+ return false;
+
+ if (bot->GetExactDist2d(kazrogal) > 36.0f)
+ return MoveAway(nearestPlayer, safeDistance - currentDistance);
+ else
+ return MoveFromGroup(safeDistance);
+ }
+
+ return false;
+}
+
+// Warlocks: Use Shadow Ward if Mark is applied and mana is <= 3000
+// Paladins: Use Shadow Resistance Aura if Priest Shadow Protection is not up
+bool KazrogalCastShadowProtectionSpellAction::Execute(Event /*event*/)
+{
+ if (bot->getClass() == CLASS_WARLOCK && bot->GetPower(POWER_MANA) <= 3000 &&
+ botAI->CanCastSpell("shadow ward", bot))
+ return botAI->CastSpell("shadow ward", bot);
+
+ if (bot->getClass() == CLASS_PALADIN &&
+ botAI->CanCastSpell("shadow resistance aura", bot))
+ return botAI->CastSpell("shadow resistance aura", bot);
+
+ return false;
+}
+
+// Azgalor
+
+bool AzgalorMisdirectBossToMainTankAction::Execute(Event /*event*/)
+{
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return false;
+
+ if (botAI->CanCastSpell("misdirection", mainTank))
+ return botAI->CastSpell("misdirection", mainTank);
+
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) &&
+ botAI->CanCastSpell("steady shot", azgalor))
+ return botAI->CastSpell("steady shot", azgalor);
+
+ return false;
+}
+
+// Two-step move: back up toward the base, then move back toward the base entrance
+// to turn Azgalor away from the raid
+bool AzgalorMainTankPositionBossAction::Execute(Event /*event*/)
+{
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return false;
+
+ MarkTargetWithStar(bot, azgalor);
+ SetRtiTarget(botAI, "star", azgalor);
+
+ if (bot->GetVictim() != azgalor)
+ return Attack(azgalor);
+
+ if (azgalor->GetVictim() == bot && bot->IsWithinMeleeRange(azgalor))
+ {
+ const ObjectGuid guid = bot->GetGUID();
+ auto it = azgalorTankStep.try_emplace(
+ guid, TankPositionState::MovingToTransition).first;
+ TankPositionState state = it->second;
+
+ constexpr float maxDistance = 2.0f;
+ const Position& position = state == TankPositionState::MovingToTransition ?
+ AZGALOR_TANK_TRANSITION_POSITION : AZGALOR_TANK_FINAL_POSITION;
+ const float distToPosition = bot->GetExactDist2d(position);
+
+ if (distToPosition > maxDistance)
+ {
+ constexpr float moveDist = 5.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, true);
+ }
+ }
+
+ if (state == TankPositionState::MovingToTransition && distToPosition <= maxDistance)
+ {
+ azgalorTankStep[guid] = TankPositionState::MovingToFinal;
+ }
+ else if (state != TankPositionState::MovingToTransition &&
+ distToPosition <= maxDistance)
+ {
+ const float orientation = atan2(azgalor->GetPositionY() - bot->GetPositionY(),
+ azgalor->GetPositionX() - bot->GetPositionX());
+ bot->SetFacingTo(orientation);
+ azgalorTankStep[guid] = TankPositionState::Positioned;
+ }
+ }
+
+ return false;
+}
+
+bool AzgalorDisperseRangedAction::Execute(Event /*event*/)
+{
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return false;
+
+ TankPositionState tankState = GetAzgalorTankPositionState(botAI, bot);
+ const float safeDistFromBoss =
+ (tankState == TankPositionState::MovingToTransition ? 35.0f : 29.0f);
+ constexpr uint32 minInterval = 0;
+
+ if (bot->GetExactDist2d(azgalor) < safeDistFromBoss &&
+ FleePosition(azgalor->GetPosition(), safeDistFromBoss, minInterval))
+ return true;
+
+ Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard");
+ constexpr float safeDistFromDoomguard = 14.0f;
+ constexpr float safeDistFromPlayer = 5.0f;
+
+ if (doomguard && bot->GetExactDist2d(doomguard) < safeDistFromDoomguard)
+ {
+ return FleePosition(doomguard->GetPosition(), safeDistFromDoomguard);
+ }
+ else if (!doomguard || bot->GetTarget() != doomguard->GetGUID())
+ {
+ Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer);
+ if (nearestPlayer)
+ return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer);
+ }
+
+ return false;
+}
+
+bool AzgalorMeleeGetOutOfFireAndSwapTargetsAction::Execute(Event /*event*/)
+{
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return false;
+
+ constexpr float singleTickMoveAwayDist = 6.0f;
+ if (!IsInRainOfFire(bot, RAIN_OF_FIRE_RADIUS + singleTickMoveAwayDist))
+ {
+ SetRtiTarget(botAI, "star", azgalor);
+ return false;
+ }
+
+ Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard");
+ Unit* desiredTarget = doomguard;
+
+ if (!desiredTarget)
+ {
+ SetRtiTarget(botAI, "star", azgalor);
+ return MoveAway(azgalor, 5.0f);
+ }
+
+ SetRtiTarget(botAI, "circle", desiredTarget);
+
+ if (!bot->IsWithinMeleeRange(desiredTarget))
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, desiredTarget->GetPositionX(),
+ desiredTarget->GetPositionY(), moveDist,
+ moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, false);
+ }
+ }
+
+ if (bot->GetVictim() != desiredTarget || bot->GetTarget() != desiredTarget->GetGUID())
+ return Attack(desiredTarget);
+
+ return false;
+}
+
+// Wait for the tank to get to the transition position (i.e., move in to attack as
+// Azgalor turns away from the raid)
+bool AzgalorWaitAtSafePositionAction::Execute(Event /*event*/)
+{
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return false;
+
+ SetRtiTarget(botAI, "star", azgalor);
+
+ const Position& position = AZGALOR_DOOMGUARD_POSITION;
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ botAI->Reset();
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_FORCED, true, false);
+ }
+
+ return false;
+}
+
+// The spot is between the paths leading from Thrall's keep
+bool AzgalorMoveToDoomguardTankAction::Execute(Event /*event*/)
+{
+ const Position& position = AZGALOR_DOOMGUARD_POSITION;
+ if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 5.0f)
+ {
+ constexpr float moveDist = 10.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_FORCED, true, false);
+ }
+ }
+
+ return false;
+}
+
+bool AzgalorFirstAssistTankPositionDoomguardAction::Execute(Event /*event*/)
+{
+ const Position& position = AZGALOR_DOOMGUARD_POSITION;
+ float distToPosition =
+ bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY());
+
+ float moveDist = 0.0f;
+ bool shouldMove = false;
+ bool moveBackwards = false;
+
+ if (Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard"))
+ {
+ MarkTargetWithCircle(bot, doomguard);
+ SetRtiTarget(botAI, "circle", doomguard);
+
+ if (bot->GetVictim() != doomguard)
+ return Attack(doomguard);
+
+ if (doomguard->GetVictim() == bot && bot->IsWithinMeleeRange(doomguard) &&
+ distToPosition > 3.0f)
+ {
+ moveDist = std::min(5.0f, distToPosition);
+ shouldMove = true;
+ moveBackwards = true;
+ }
+ }
+ else if (distToPosition > 3.0f)
+ {
+ moveDist = std::min(10.0f, distToPosition);
+ shouldMove = true;
+ moveBackwards = false;
+ }
+ else
+ {
+ return true;
+ }
+
+ if (shouldMove)
+ {
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, moveBackwards);
+ }
+ }
+
+ return false;
+}
+
+// Only nearbyish ranged DPS should attack Doomguards; 65 yards should get to the
+// side of Azgalor but not bring in any ranged standing in front
+bool AzgalorRangedDpsPrioritizeDoomguardsAction::Execute(Event /*event*/)
+{
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return false;
+
+ if (azgalor->GetHealthPct() > 10.0f)
+ {
+ if (Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard");
+ doomguard && bot->GetDistance2d(doomguard) < 65.0f)
+ {
+ SetRtiTarget(botAI, "circle", doomguard);
+
+ if (bot->GetTarget() != doomguard->GetGUID())
+ return Attack(doomguard);
+ }
+ }
+ else
+ {
+ SetRtiTarget(botAI, "star", azgalor);
+
+ if (bot->GetTarget() != azgalor->GetGUID())
+ return Attack(azgalor);
+ }
+
+ return false;
+}
+
+// Archimonde
+
+bool ArchimondeMisdirectBossToMainTankAction::Execute(Event /*event*/)
+{
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (!archimonde)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return false;
+
+ if (botAI->CanCastSpell("misdirection", mainTank))
+ return botAI->CastSpell("misdirection", mainTank);
+
+ if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) &&
+ botAI->CanCastSpell("steady shot", archimonde))
+ return botAI->CastSpell("steady shot", archimonde);
+
+ return false;
+}
+
+// Initially move Archimonde up the hill a bit to get space from the World Tree
+bool ArchimondeMoveBossToInitialPositionAction::Execute(Event /*event*/)
+{
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (!archimonde)
+ return false;
+
+ if (bot->GetVictim() != archimonde)
+ return Attack(archimonde);
+
+ if (archimonde->GetVictim() == bot && bot->IsWithinMeleeRange(archimonde) &&
+ bot->GetHealthPct() > 50.0f)
+ {
+ const Position& position = ARCHIMONDE_INITIAL_POSITION;
+ const float distToPosition =
+ bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY());
+
+ if (distToPosition > 3.0f)
+ {
+ constexpr float moveDist = 5.0f;
+ float moveX, moveY, moveZ;
+ if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(),
+ moveDist, moveX, moveY, moveZ))
+ {
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false,
+ false, MovementPriority::MOVEMENT_COMBAT, true, true);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool ArchimondeCastFearImmunitySpellAction::Execute(Event /*event*/)
+{
+ if (bot->getClass() == CLASS_PRIEST)
+ return CastFearWardOnMainTank();
+ else
+ return UseTremorTotemStrategy();
+}
+
+bool ArchimondeCastFearImmunitySpellAction::CastFearWardOnMainTank()
+{
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (mainTank && botAI->CanCastSpell("fear ward", mainTank))
+ return botAI->CastSpell("fear ward", mainTank);
+
+ return false;
+}
+
+bool ArchimondeCastFearImmunitySpellAction::UseTremorTotemStrategy()
+{
+ if (!botAI->HasStrategy("tremor", BOT_STATE_COMBAT))
+ {
+ botAI->ChangeStrategy("+tremor", BOT_STATE_COMBAT);
+ return botAI->HasStrategy("tremor", BOT_STATE_COMBAT);
+ }
+
+ return false;
+}
+
+// (1) Try to run away from the Air Burst target
+// (2) At the beginning of the fight, spread ranged in anticipation of Air Burst
+bool ArchimondeSpreadToAvoidAirBurstAction::Execute(Event /*event*/)
+{
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (!archimonde)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (mainTank && bot != mainTank)
+ {
+ const float distanceToMainTank = bot->GetDistance2d(mainTank);
+ bool shouldMoveFromMainTank = false;
+ if (AirBurstData* data = GetRecentArchimondeAirBurst(bot->GetMap()->GetInstanceId()))
+ {
+ bool isRelevantAirBurstTarget =
+ data->targetGuid == mainTank->GetGUID() || data->targetGuid == bot->GetGUID();
+ shouldMoveFromMainTank =
+ isRelevantAirBurstTarget && distanceToMainTank < AIR_BURST_SAFE_DISTANCE;
+ }
+
+ if (!shouldMoveFromMainTank && archimonde->HasUnitState(UNIT_STATE_CASTING))
+ {
+ Spell* spell = archimonde->GetCurrentSpell(CURRENT_GENERIC_SPELL);
+ if (spell && spell->m_spellInfo->Id ==
+ static_cast(HyjalSummitSpells::SPELL_AIR_BURST))
+ {
+ Unit* spellTarget = spell->m_targets.GetUnitTarget();
+ if ((spellTarget == mainTank || spellTarget == bot) &&
+ distanceToMainTank < AIR_BURST_SAFE_DISTANCE)
+ {
+ shouldMoveFromMainTank = true;
+ }
+ }
+ }
+
+ if (shouldMoveFromMainTank)
+ return MoveAway(mainTank, AIR_BURST_SAFE_DISTANCE - distanceToMainTank);
+ }
+
+ if (archimonde->GetHealthPct() < 90.0f)
+ return false;
+
+ constexpr float safeDistFromPlayer = 10.0f;
+ constexpr uint32 minInterval = 1000;
+
+ if (botAI->IsRanged(bot))
+ {
+ Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer);
+ if (nearestPlayer &&
+ FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval))
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool ArchimondeAvoidDoomfireAction::Execute(Event /*event*/)
+{
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (!archimonde)
+ return false;
+
+ constexpr float dangerDist = 10.0f;
+ constexpr uint32 trailDuration = 18000;
+
+ const uint32 instanceId = bot->GetMap()->GetInstanceId();
+ const uint32 now = getMSTime();
+
+ auto it = doomfireTrails.find(instanceId);
+ if (it == doomfireTrails.end() || it->second.empty())
+ return false;
+
+ it->second.erase(std::remove_if(it->second.begin(), it->second.end(),
+ [now](const DoomfireTrailData& d)
+ {
+ return getMSTimeDiff(d.recordTime, now) > trailDuration;
+ }), it->second.end());
+
+ float totalDx = 0.0f, totalDy = 0.0f;
+ for (auto const& data : it->second)
+ {
+ const float d = bot->GetExactDist2d(data.position.GetPositionX(),
+ data.position.GetPositionY());
+
+ if (d < dangerDist && d > 0.0f)
+ {
+ const float weight = (dangerDist - d) / dangerDist;
+ totalDx += (bot->GetPositionX() - data.position.GetPositionX()) / d * weight;
+ totalDy += (bot->GetPositionY() - data.position.GetPositionY()) / d * weight;
+ }
+ }
+
+ if (totalDx != 0.0f || totalDy != 0.0f)
+ {
+ const float norm = std::sqrt(totalDx * totalDx + totalDy * totalDy);
+ const float moveDist = std::min(norm * dangerDist, dangerDist);
+ if (moveDist < 0.5f)
+ return false;
+
+ const float targetX = bot->GetPositionX() + (totalDx / norm) * moveDist;
+ const float targetY = bot->GetPositionY() + (totalDy / norm) * moveDist;
+
+ const MovementPriority priority = botAI->IsHeal(bot) ?
+ MovementPriority::MOVEMENT_COMBAT : MovementPriority::MOVEMENT_FORCED;
+
+ const bool backwards = archimonde->GetVictim() == bot;
+
+ return MoveTo(HYJAL_SUMMIT_MAP_ID, targetX, targetY, bot->GetPositionZ(),
+ false, false, false, false, priority, true, backwards);
+ }
+
+ return false;
+}
+
+bool ArchimondeRemoveDoomfireDotAction::Execute(Event /*event*/)
+{
+ switch (bot->getClass())
+ {
+ case CLASS_MAGE:
+ return botAI->CanCastSpell("ice block", bot) &&
+ botAI->CastSpell("ice block", bot);
+
+ case CLASS_PALADIN:
+ return botAI->CanCastSpell("divine shield", bot) &&
+ botAI->CastSpell("divine shield", bot);
+
+ case CLASS_ROGUE:
+ return botAI->CanCastSpell("cloak of shadows", bot) &&
+ botAI->CastSpell("cloak of shadows", bot);
+
+ default:
+ return false;
+ }
+}
diff --git a/src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.h b/src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.h
new file mode 100644
index 000000000..e741a7d26
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Action/RaidHyjalSummitActions.h
@@ -0,0 +1,277 @@
+/*
+ * 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_RAIDHYJALSUMMITACTIONS_H
+#define _PLAYERBOT_RAIDHYJALSUMMITACTIONS_H
+
+#include "Action.h"
+#include "AttackAction.h"
+#include "MovementActions.h"
+
+// General
+
+class HyjalSummitEraseTrackersAction : public Action
+{
+public:
+ HyjalSummitEraseTrackersAction(
+ PlayerbotAI* botAI) : Action(botAI, "hyjal summit erase trackers") {}
+ bool Execute(Event event) override;
+};
+
+// Rage Winterchill
+
+class RageWinterchillMisdirectBossToMainTankAction : public AttackAction
+{
+public:
+ RageWinterchillMisdirectBossToMainTankAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "rage winterchill misdirect boss to main tank") {}
+ bool Execute(Event event) override;
+};
+
+class RageWinterchillMainTankPositionBossAction : public AttackAction
+{
+public:
+ RageWinterchillMainTankPositionBossAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "rage winterchill main tank position boss") {}
+ bool Execute(Event event) override;
+};
+
+class RageWinterchillSpreadRangedInCircleAction : public MovementAction
+{
+public:
+ RageWinterchillSpreadRangedInCircleAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "rage winterchill spread ranged in circle") {}
+ bool Execute(Event event) override;
+};
+
+class RageWinterchillMeleeGetOutOfDeathAndDecayAction : public AttackAction
+{
+public:
+ RageWinterchillMeleeGetOutOfDeathAndDecayAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "rage winterchill melee get out of death and decay") {}
+ bool Execute(Event event) override;
+};
+
+// Anetheron
+
+class AnetheronMisdirectBossAndInfernalsToTanksAction : public AttackAction
+{
+public:
+ AnetheronMisdirectBossAndInfernalsToTanksAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "anetheron misdirect boss and infernals to tanks") {}
+ bool Execute(Event event) override;
+};
+
+class AnetheronMainTankPositionBossAction : public AttackAction
+{
+public:
+ AnetheronMainTankPositionBossAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "anetheron main tank position boss") {}
+ bool Execute(Event event) override;
+};
+
+class AnetheronSpreadRangedInCircleAction : public MovementAction
+{
+public:
+ AnetheronSpreadRangedInCircleAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "anetheron spread ranged in circle") {}
+ bool Execute(Event event) override;
+};
+
+class AnetheronBringInfernalToInfernalTankAction : public MovementAction
+{
+public:
+ AnetheronBringInfernalToInfernalTankAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "anetheron bring infernal to infernal tank") {}
+ bool Execute(Event event) override;
+};
+
+class AnetheronFirstAssistTankPickUpInfernalsAction : public AttackAction
+{
+public:
+ AnetheronFirstAssistTankPickUpInfernalsAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "anetheron first assist tank pick up infernals") {}
+ bool Execute(Event event) override;
+};
+
+class AnetheronAssignDpsPriorityAction : public AttackAction
+{
+public:
+ AnetheronAssignDpsPriorityAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "anetheron assign dps priority") {}
+ bool Execute(Event event) override;
+};
+
+// Kaz'rogal
+
+class KazrogalMisdirectBossToMainTankAction : public AttackAction
+{
+public:
+ KazrogalMisdirectBossToMainTankAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "kaz'rogal misdirect boss to main tank") {}
+ bool Execute(Event event) override;
+};
+
+class KazrogalMainTankPositionBossAction : public AttackAction
+{
+public:
+ KazrogalMainTankPositionBossAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "kaz'rogal main tank position boss") {}
+ bool Execute(Event event) override;
+};
+
+class KazrogalAssistTanksMoveInFrontOfBossAction : public AttackAction
+{
+public:
+ KazrogalAssistTanksMoveInFrontOfBossAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "kaz'rogal assist tanks move in front of boss") {}
+ bool Execute(Event event) override;
+};
+
+class KazrogalSpreadRangedInArcAction : public MovementAction
+{
+public:
+ KazrogalSpreadRangedInArcAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "kaz'rogal spread ranged in arc") {}
+ bool Execute(Event event) override;
+};
+
+class KazrogalLowManaBotTakeDefensiveMeasuresAction : public MovementAction
+{
+public:
+KazrogalLowManaBotTakeDefensiveMeasuresAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "kaz'rogal low mana bot take defensive measures") {}
+ bool Execute(Event event) override;
+};
+
+class KazrogalCastShadowProtectionSpellAction : public Action
+{
+public:
+ KazrogalCastShadowProtectionSpellAction(
+ PlayerbotAI* botAI) : Action(botAI, "kaz'rogal cast shadow protection spell") {}
+ bool Execute(Event event) override;
+};
+
+// Azgalor
+
+class AzgalorMisdirectBossToMainTankAction : public AttackAction
+{
+public:
+ AzgalorMisdirectBossToMainTankAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "azgalor misdirect boss to main tank") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorMainTankPositionBossAction : public AttackAction
+{
+public:
+ AzgalorMainTankPositionBossAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "azgalor main tank position boss") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorWaitAtSafePositionAction : public MovementAction
+{
+public:
+ AzgalorWaitAtSafePositionAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "azgalor wait at safe position") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorDisperseRangedAction : public MovementAction
+{
+public:
+ AzgalorDisperseRangedAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "azgalor disperse ranged") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorMeleeGetOutOfFireAndSwapTargetsAction : public AttackAction
+{
+public:
+ AzgalorMeleeGetOutOfFireAndSwapTargetsAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "azgalor melee get out of fire and swap targets") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorMoveToDoomguardTankAction : public MovementAction
+{
+public:
+ AzgalorMoveToDoomguardTankAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "azgalor move to doomguard tank") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorFirstAssistTankPositionDoomguardAction : public AttackAction
+{
+public:
+ AzgalorFirstAssistTankPositionDoomguardAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "azgalor first assist tank position doomguard") {}
+ bool Execute(Event event) override;
+};
+
+class AzgalorRangedDpsPrioritizeDoomguardsAction : public AttackAction
+{
+public:
+ AzgalorRangedDpsPrioritizeDoomguardsAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "azgalor ranged dps prioritize doomguards") {}
+ bool Execute(Event event) override;
+};
+
+// Archimonde
+
+class ArchimondeMisdirectBossToMainTankAction : public AttackAction
+{
+public:
+ ArchimondeMisdirectBossToMainTankAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "archimonde misdirect boss to main tank") {}
+ bool Execute(Event event) override;
+};
+
+class ArchimondeMoveBossToInitialPositionAction : public AttackAction
+{
+public:
+ ArchimondeMoveBossToInitialPositionAction(
+ PlayerbotAI* botAI) : AttackAction(botAI, "archimonde move boss to initial position") {}
+ bool Execute(Event event) override;
+};
+
+class ArchimondeCastFearImmunitySpellAction : public Action
+{
+public:
+ ArchimondeCastFearImmunitySpellAction(
+ PlayerbotAI* botAI) : Action(botAI, "archimonde cast fear immunity spell") {}
+ bool Execute(Event event) override;
+
+private:
+ bool CastFearWardOnMainTank();
+ bool UseTremorTotemStrategy();
+};
+
+class ArchimondeSpreadToAvoidAirBurstAction : public MovementAction
+{
+public:
+ ArchimondeSpreadToAvoidAirBurstAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "archimonde spread to avoid air burst") {}
+ bool Execute(Event event) override;
+};
+
+class ArchimondeAvoidDoomfireAction : public MovementAction
+{
+public:
+ ArchimondeAvoidDoomfireAction(
+ PlayerbotAI* botAI) : MovementAction(botAI, "archimonde avoid doomfire") {}
+ bool Execute(Event event) override;
+};
+
+class ArchimondeRemoveDoomfireDotAction : public Action
+{
+public:
+ ArchimondeRemoveDoomfireDotAction(
+ PlayerbotAI* botAI) : Action(botAI, "archimonde remove doomfire dot") {}
+ bool Execute(Event event) override;
+};
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.cpp b/src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.cpp
new file mode 100644
index 000000000..a9f666464
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.cpp
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "RaidHyjalSummitMultipliers.h"
+#include "RaidHyjalSummitActions.h"
+#include "RaidHyjalSummitHelpers.h"
+#include "AiFactory.h"
+#include "ChooseTargetActions.h"
+#include "DKActions.h"
+#include "DruidBearActions.h"
+#include "HunterActions.h"
+#include "PaladinActions.h"
+#include "RaidBossHelpers.h"
+#include "ReachTargetActions.h"
+#include "ShamanActions.h"
+#include "WarriorActions.h"
+
+using namespace HyjalSummitHelpers;
+
+// Without this multiplier, Bloodlust/Heroism will not be available for
+// bosses because it will be used on cooldown during trash waves
+float HyjalSummitTimeBloodlustAndHeroismMultiplier::GetValue(Action* action)
+{
+ if (bot->getClass() != CLASS_SHAMAN)
+ return 1.0f;
+
+ if (dynamic_cast(action) ||
+ dynamic_cast(action))
+ {
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (archimonde && archimonde->GetHealthPct() < 90.0f)
+ return 1.0f;
+
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (azgalor && azgalor->GetHealthPct() < 90.0f)
+ return 1.0f;
+
+ Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal");
+ if (kazrogal && kazrogal->GetHealthPct() < 90.0f)
+ return 1.0f;
+
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (anetheron && anetheron->GetHealthPct() < 85.0f)
+ return 1.0f;
+
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ if (winterchill && winterchill->GetHealthPct() < 90.0f)
+ return 1.0f;
+
+ return 0.0f;
+ }
+
+ return 1.0f;
+}
+
+// Rage Winterchill
+
+float RageWinterchillDisableCombatFormationMoveMultiplier::GetValue(Action* action)
+{
+ if (!AI_VALUE2(Unit*, "find target", "rage winterchill"))
+ return 1.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+float RageWinterchillMeleeControlAvoidanceMultiplier::GetValue(Action* action)
+{
+ if (botAI->IsRanged(bot))
+ return 1.0f;
+
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ if (!winterchill)
+ return 1.0f;
+
+ if (IsInDeathAndDecay(bot, DEATH_AND_DECAY_SAFE_RADIUS + 2.0f))
+ {
+ if (dynamic_cast(action))
+ return 0.0f;
+
+ if (botAI->IsMainTank(bot) || winterchill->GetVictim() == bot)
+ return 1.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+
+ if (dynamic_cast(action))
+ return 0.0f;
+ }
+
+ return 1.0f;
+}
+
+// Anetheron
+
+float AnetheronDisableTankActionsMultiplier::GetValue(Action* action)
+{
+ if (!botAI->IsTank(bot) || !AI_VALUE2(Unit*, "find target", "anetheron"))
+ return 1.0f;
+
+ if (dynamic_cast(action))
+ return 0.0f;
+
+ if (bot->GetVictim() != nullptr &&
+ dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+float AnetheronDisableCombatFormationMoveMultiplier::GetValue(Action* action)
+{
+ if (!AI_VALUE2(Unit*, "find target", "anetheron"))
+ return 1.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+float AnetheronControlMisdirectionMultiplier::GetValue(Action* action)
+{
+ if (bot->getClass() != CLASS_HUNTER ||
+ !AI_VALUE2(Unit*, "find target", "anetheron"))
+ return 1.0f;
+
+ if (dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+// Kaz'rogal
+
+float KazrogalLowManaBotStayAwayFromGroupMultiplier::GetValue(Action* action)
+{
+ if (bot->getClass() == CLASS_WARRIOR || bot->getClass() == CLASS_ROGUE ||
+ bot->getClass() == CLASS_DEATH_KNIGHT || bot->getClass() == CLASS_HUNTER)
+ return 1.0f;
+
+ uint8 tab = AiFactory::GetPlayerSpecTab(bot);
+ if (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL)
+ return 1.0f;
+
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return 1.0f;
+
+ if (!isBelowManaThreshold.count(bot->GetGUID()))
+ return 1.0f;
+
+ if (dynamic_cast(action) ||
+ (dynamic_cast(action) &&
+ !dynamic_cast(action) &&
+ !dynamic_cast(action)))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+float KazrogalKeepAspectOfTheViperActiveMultiplier::GetValue(Action* action)
+{
+ if (bot->getClass() != CLASS_HUNTER || bot->GetPower(POWER_MANA) > 4000 ||
+ !AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return 1.0f;
+
+ if (dynamic_cast(action) ||
+ dynamic_cast(action) ||
+ dynamic_cast(action) ||
+ dynamic_cast(action) ||
+ dynamic_cast(action) ||
+ dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+float KazrogalControlMovementMultiplier::GetValue(Action* action)
+{
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return 1.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+
+ if (dynamic_cast(action))
+ return 0.0f;
+
+ if (botAI->IsRanged(bot) && dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+// Azgalor
+
+float AzgalorDisableTankActionsMultiplier::GetValue(Action* action)
+{
+ if (bot->GetVictim() == nullptr)
+ return 1.0f;
+
+ if (!botAI->IsTank(bot) || !AI_VALUE2(Unit*, "find target", "azgalor"))
+ return 1.0f;
+
+ if (dynamic_cast(action))
+ return 0.0f;
+
+ if (dynamic_cast(action) || dynamic_cast(action))
+ {
+ if (botAI->IsMainTank(bot))
+ {
+ return 0.0f;
+ }
+ else if (botAI->IsAssistTank(bot) && (AnyGroupMemberHasDoom(bot) ||
+ AI_VALUE2(Unit*, "find target", "lesser doomguard")))
+ {
+ return 0.0f;
+ }
+ }
+
+ return 1.0f;
+}
+
+float AzgalorDoomedBotPrioritizePositioningMultiplier::GetValue(Action* action)
+{
+ if (!bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM)))
+ return 1.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action) &&
+ !dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
+
+float AzgalorMeleeDpsControlAvoidanceMultiplier::GetValue(Action* action)
+{
+ if (botAI->IsRanged(bot) || botAI->IsTank(bot))
+ return 1.0f;
+
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor)
+ return 1.0f;
+
+ constexpr float singleTickMoveAwayDist = 6.0f;
+ if (IsInRainOfFire(bot, RAIN_OF_FIRE_RADIUS + singleTickMoveAwayDist))
+ {
+ if (dynamic_cast(action) ||
+ dynamic_cast(action))
+ return 0.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+ }
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank || !GET_PLAYERBOT_AI(mainTank))
+ return 1.0f;
+
+ TankPositionState tankState = GetAzgalorTankPositionState(botAI, bot);
+ if ((tankState == TankPositionState::Unknown ||
+ tankState == TankPositionState::MovingToTransition) &&
+ dynamic_cast(action) &&
+ !dynamic_cast(action))
+ {
+ return 0.0f;
+ }
+
+ return 1.0f;
+}
+
+// Archimonde
+
+float ArchimondeDisableCombatFormationMoveMultiplier::GetValue(Action* action)
+{
+ if (!AI_VALUE2(Unit*, "find target", "archimonde"))
+ return 1.0f;
+
+ if (dynamic_cast(action) &&
+ !dynamic_cast(action))
+ return 0.0f;
+
+ return 1.0f;
+}
diff --git a/src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.h b/src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.h
new file mode 100644
index 000000000..9fa9dd885
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Multiplier/RaidHyjalSummitMultipliers.h
@@ -0,0 +1,125 @@
+/*
+ * 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_RAIDHYJALSUMMITMULTIPLIERS_H
+#define _PLAYERBOT_RAIDHYJALSUMMITMULTIPLIERS_H
+
+#include "Multiplier.h"
+
+class HyjalSummitTimeBloodlustAndHeroismMultiplier : public Multiplier
+{
+public:
+ HyjalSummitTimeBloodlustAndHeroismMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "hyjal summit time bloodlust and heroism multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+// Rage Winterchill
+
+class RageWinterchillDisableCombatFormationMoveMultiplier : public Multiplier
+{
+public:
+ RageWinterchillDisableCombatFormationMoveMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "rage winterchill disable combat formation move multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class RageWinterchillMeleeControlAvoidanceMultiplier : public Multiplier
+{
+public:
+ RageWinterchillMeleeControlAvoidanceMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "rage winterchill melee control avoidance multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+// Anetheron
+
+class AnetheronDisableTankActionsMultiplier : public Multiplier
+{
+public:
+ AnetheronDisableTankActionsMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "anetheron disable tank actions multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class AnetheronDisableCombatFormationMoveMultiplier : public Multiplier
+{
+public:
+ AnetheronDisableCombatFormationMoveMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "anetheron disable combat formation move multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class AnetheronControlMisdirectionMultiplier : public Multiplier
+{
+public:
+ AnetheronControlMisdirectionMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "anetheron control misdirection multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+// Kaz'rogal
+
+class KazrogalLowManaBotStayAwayFromGroupMultiplier : public Multiplier
+{
+public:
+ KazrogalLowManaBotStayAwayFromGroupMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "kaz'rogal low mana bot stay away from group multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class KazrogalKeepAspectOfTheViperActiveMultiplier : public Multiplier
+{
+public:
+ KazrogalKeepAspectOfTheViperActiveMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "kaz'rogal keep aspect of the viper active multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class KazrogalControlMovementMultiplier : public Multiplier
+{
+public:
+ KazrogalControlMovementMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "kaz'rogal control movement multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+// Azgalor
+
+class AzgalorDisableTankActionsMultiplier : public Multiplier
+{
+public:
+ AzgalorDisableTankActionsMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "azgalor disable tank actions multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class AzgalorDoomedBotPrioritizePositioningMultiplier : public Multiplier
+{
+public:
+ AzgalorDoomedBotPrioritizePositioningMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "azgalor doomed bot prioritize positioning multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+class AzgalorMeleeDpsControlAvoidanceMultiplier : public Multiplier
+{
+public:
+ AzgalorMeleeDpsControlAvoidanceMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "azgalor melee dps control avoidance multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+// Archimonde
+
+class ArchimondeDisableCombatFormationMoveMultiplier : public Multiplier
+{
+public:
+ ArchimondeDisableCombatFormationMoveMultiplier(
+ PlayerbotAI* botAI) : Multiplier(botAI, "archimonde disable combat formation move multiplier") {}
+ virtual float GetValue(Action* action);
+};
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/RaidHyjalSummitActionContext.h b/src/Ai/Raid/HyjalSummit/RaidHyjalSummitActionContext.h
new file mode 100644
index 000000000..02164df46
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/RaidHyjalSummitActionContext.h
@@ -0,0 +1,218 @@
+/*
+ * 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_RAIDHYJALSUMMITACTIONCONTEXT_H
+#define _PLAYERBOT_RAIDHYJALSUMMITACTIONCONTEXT_H
+
+#include "RaidHyjalSummitActions.h"
+#include "NamedObjectContext.h"
+
+class RaidHyjalSummitActionContext : public NamedObjectContext
+{
+public:
+ RaidHyjalSummitActionContext()
+ {
+ // General
+ creators["hyjal summit erase trackers"] =
+ &RaidHyjalSummitActionContext::hyjal_summit_erase_trackers;
+
+ // Rage Winterchill
+ creators["rage winterchill misdirect boss to main tank"] =
+ &RaidHyjalSummitActionContext::rage_winterchill_misdirect_boss_to_main_tank;
+
+ creators["rage winterchill main tank position boss"] =
+ &RaidHyjalSummitActionContext::rage_winterchill_main_tank_position_boss;
+
+ creators["rage winterchill spread ranged in circle"] =
+ &RaidHyjalSummitActionContext::rage_winterchill_spread_ranged_in_circle;
+
+ creators["rage winterchill melee get out of death and decay"] =
+ &RaidHyjalSummitActionContext::rage_winterchill_melee_get_out_of_death_and_decay;
+
+ // Anetheron
+ creators["anetheron misdirect boss and infernals to tanks"] =
+ &RaidHyjalSummitActionContext::anetheron_misdirect_boss_and_infernals_to_tanks;
+
+ creators["anetheron main tank position boss"] =
+ &RaidHyjalSummitActionContext::anetheron_main_tank_position_boss;
+
+ creators["anetheron spread ranged in circle"] =
+ &RaidHyjalSummitActionContext::anetheron_spread_ranged_in_circle;
+
+ creators["anetheron bring infernal to infernal tank"] =
+ &RaidHyjalSummitActionContext::anetheron_bring_infernal_to_infernal_tank;
+
+ creators["anetheron first assist tank pick up infernals"] =
+ &RaidHyjalSummitActionContext::anetheron_first_assist_tank_pick_up_infernals;
+
+ creators["anetheron assign dps priority"] =
+ &RaidHyjalSummitActionContext::anetheron_assign_dps_priority;
+
+ // Kaz'rogal
+ creators["kaz'rogal misdirect boss to main tank"] =
+ &RaidHyjalSummitActionContext::kazrogal_misdirect_boss_to_main_tank;
+
+ creators["kaz'rogal main tank position boss"] =
+ &RaidHyjalSummitActionContext::kazrogal_main_tank_position_boss;
+
+ creators["kaz'rogal assist tanks move in front of boss"] =
+ &RaidHyjalSummitActionContext::kazrogal_assist_tanks_move_in_front_of_boss;
+
+ creators["kaz'rogal spread ranged in arc"] =
+ &RaidHyjalSummitActionContext::kazrogal_spread_ranged_in_arc;
+
+ creators["kaz'rogal low mana bot take defensive measures"] =
+ &RaidHyjalSummitActionContext::kazrogal_low_mana_bot_take_defensive_measures;
+
+ creators["kaz'rogal cast shadow protection spell"] =
+ &RaidHyjalSummitActionContext::kazrogal_cast_shadow_protection_spell;
+
+ // Azgalor
+ creators["azgalor misdirect boss to main tank"] =
+ &RaidHyjalSummitActionContext::azgalor_misdirect_boss_to_main_tank;
+
+ creators["azgalor main tank position boss"] =
+ &RaidHyjalSummitActionContext::azgalor_main_tank_position_boss;
+
+ creators["azgalor wait at safe position"] =
+ &RaidHyjalSummitActionContext::azgalor_wait_at_safe_position;
+
+ creators["azgalor disperse ranged"] =
+ &RaidHyjalSummitActionContext::azgalor_disperse_ranged;
+
+ creators["azgalor melee get out of fire and swap targets"] =
+ &RaidHyjalSummitActionContext::azgalor_melee_get_out_of_fire_and_swap_targets;
+
+ creators["azgalor move to doomguard tank"] =
+ &RaidHyjalSummitActionContext::azgalor_move_to_doomguard_tank;
+
+ creators["azgalor first assist tank position doomguard"] =
+ &RaidHyjalSummitActionContext::azgalor_first_assist_tank_position_doomguard;
+
+ creators["azgalor ranged dps prioritize doomguards"] =
+ &RaidHyjalSummitActionContext::azgalor_ranged_dps_prioritize_doomguards;
+
+ // Archimonde
+ creators["archimonde misdirect boss to main tank"] =
+ &RaidHyjalSummitActionContext::archimonde_misdirect_boss_to_main_tank;
+
+ creators["archimonde move boss to initial position"] =
+ &RaidHyjalSummitActionContext::archimonde_move_boss_to_initial_position;
+
+ creators["archimonde cast fear immunity spell"] =
+ &RaidHyjalSummitActionContext::archimonde_cast_fear_immunity_spell;
+
+ creators["archimonde spread to avoid air burst"] =
+ &RaidHyjalSummitActionContext::archimonde_spread_to_avoid_air_burst;
+
+ creators["archimonde avoid doomfire"] =
+ &RaidHyjalSummitActionContext::archimonde_avoid_doomfire;
+
+ creators["archimonde remove doomfire dot"] =
+ &RaidHyjalSummitActionContext::archimonde_remove_doomfire_dot;
+ }
+
+private:
+ // General
+ static Action* hyjal_summit_erase_trackers(
+ PlayerbotAI* botAI) { return new HyjalSummitEraseTrackersAction(botAI); }
+
+ // Rage Winterchill
+ static Action* rage_winterchill_misdirect_boss_to_main_tank(
+ PlayerbotAI* botAI) { return new RageWinterchillMisdirectBossToMainTankAction(botAI); }
+
+ static Action* rage_winterchill_main_tank_position_boss(
+ PlayerbotAI* botAI) { return new RageWinterchillMainTankPositionBossAction(botAI); }
+
+ static Action* rage_winterchill_spread_ranged_in_circle(
+ PlayerbotAI* botAI) { return new RageWinterchillSpreadRangedInCircleAction(botAI); }
+
+ static Action* rage_winterchill_melee_get_out_of_death_and_decay(
+ PlayerbotAI* botAI) { return new RageWinterchillMeleeGetOutOfDeathAndDecayAction(botAI); }
+
+ // Anetheron
+ static Action* anetheron_misdirect_boss_and_infernals_to_tanks(
+ PlayerbotAI* botAI) { return new AnetheronMisdirectBossAndInfernalsToTanksAction(botAI); }
+
+ static Action* anetheron_main_tank_position_boss(
+ PlayerbotAI* botAI) { return new AnetheronMainTankPositionBossAction(botAI); }
+
+ static Action* anetheron_spread_ranged_in_circle(
+ PlayerbotAI* botAI) { return new AnetheronSpreadRangedInCircleAction(botAI); }
+
+ static Action* anetheron_bring_infernal_to_infernal_tank(
+ PlayerbotAI* botAI) { return new AnetheronBringInfernalToInfernalTankAction(botAI); }
+
+ static Action* anetheron_first_assist_tank_pick_up_infernals(
+ PlayerbotAI* botAI) { return new AnetheronFirstAssistTankPickUpInfernalsAction(botAI); }
+
+ static Action* anetheron_assign_dps_priority(
+ PlayerbotAI* botAI) { return new AnetheronAssignDpsPriorityAction(botAI); }
+
+ // Kaz'rogal
+ static Action* kazrogal_misdirect_boss_to_main_tank(
+ PlayerbotAI* botAI) { return new KazrogalMisdirectBossToMainTankAction(botAI); }
+
+ static Action* kazrogal_main_tank_position_boss(
+ PlayerbotAI* botAI) { return new KazrogalMainTankPositionBossAction(botAI); }
+
+ static Action* kazrogal_assist_tanks_move_in_front_of_boss(
+ PlayerbotAI* botAI) { return new KazrogalAssistTanksMoveInFrontOfBossAction(botAI); }
+
+ static Action* kazrogal_spread_ranged_in_arc(
+ PlayerbotAI* botAI) { return new KazrogalSpreadRangedInArcAction(botAI); }
+
+ static Action* kazrogal_low_mana_bot_take_defensive_measures(
+ PlayerbotAI* botAI) { return new KazrogalLowManaBotTakeDefensiveMeasuresAction(botAI); }
+
+ static Action* kazrogal_cast_shadow_protection_spell(
+ PlayerbotAI* botAI) { return new KazrogalCastShadowProtectionSpellAction(botAI); }
+
+ // Azgalor
+ static Action* azgalor_misdirect_boss_to_main_tank(
+ PlayerbotAI* botAI) { return new AzgalorMisdirectBossToMainTankAction(botAI); }
+
+ static Action* azgalor_main_tank_position_boss(
+ PlayerbotAI* botAI) { return new AzgalorMainTankPositionBossAction(botAI); }
+
+ static Action* azgalor_wait_at_safe_position(
+ PlayerbotAI* botAI) { return new AzgalorWaitAtSafePositionAction(botAI); }
+
+ static Action* azgalor_disperse_ranged(
+ PlayerbotAI* botAI) { return new AzgalorDisperseRangedAction(botAI); }
+
+ static Action* azgalor_melee_get_out_of_fire_and_swap_targets(
+ PlayerbotAI* botAI) { return new AzgalorMeleeGetOutOfFireAndSwapTargetsAction(botAI); }
+
+ static Action* azgalor_move_to_doomguard_tank(
+ PlayerbotAI* botAI) { return new AzgalorMoveToDoomguardTankAction(botAI); }
+
+ static Action* azgalor_first_assist_tank_position_doomguard(
+ PlayerbotAI* botAI) { return new AzgalorFirstAssistTankPositionDoomguardAction(botAI); }
+
+ static Action* azgalor_ranged_dps_prioritize_doomguards(
+ PlayerbotAI* botAI) { return new AzgalorRangedDpsPrioritizeDoomguardsAction(botAI); }
+
+ // Archimonde
+ static Action* archimonde_misdirect_boss_to_main_tank(
+ PlayerbotAI* botAI) { return new ArchimondeMisdirectBossToMainTankAction(botAI); }
+
+ static Action* archimonde_move_boss_to_initial_position(
+ PlayerbotAI* botAI) { return new ArchimondeMoveBossToInitialPositionAction(botAI); }
+
+ static Action* archimonde_cast_fear_immunity_spell(
+ PlayerbotAI* botAI) { return new ArchimondeCastFearImmunitySpellAction(botAI); }
+
+ static Action* archimonde_spread_to_avoid_air_burst(
+ PlayerbotAI* botAI) { return new ArchimondeSpreadToAvoidAirBurstAction(botAI); }
+
+ static Action* archimonde_avoid_doomfire(
+ PlayerbotAI* botAI) { return new ArchimondeAvoidDoomfireAction(botAI); }
+
+ static Action* archimonde_remove_doomfire_dot(
+ PlayerbotAI* botAI) { return new ArchimondeRemoveDoomfireDotAction(botAI); }
+};
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/RaidHyjalSummitTriggerContext.h b/src/Ai/Raid/HyjalSummit/RaidHyjalSummitTriggerContext.h
new file mode 100644
index 000000000..a7c564ee0
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/RaidHyjalSummitTriggerContext.h
@@ -0,0 +1,218 @@
+/*
+ * 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_RAIDHYJALSUMMITTRIGGERCONTEXT_H
+#define _PLAYERBOT_RAIDHYJALSUMMITTRIGGERCONTEXT_H
+
+#include "RaidHyjalSummitTriggers.h"
+#include "NamedObjectContext.h"
+
+class RaidHyjalSummitTriggerContext : public NamedObjectContext
+{
+public:
+ RaidHyjalSummitTriggerContext()
+ {
+ // General
+ creators["hyjal summit bot is not in combat"] =
+ &RaidHyjalSummitTriggerContext::hyjal_summit_bot_is_not_in_combat;
+
+ // Rage Winterchill
+ creators["rage winterchill pulling boss"] =
+ &RaidHyjalSummitTriggerContext::rage_winterchill_pulling_boss;
+
+ creators["rage winterchill boss engaged by main tank"] =
+ &RaidHyjalSummitTriggerContext::rage_winterchill_boss_engaged_by_main_tank;
+
+ creators["rage winterchill boss casts death and decay on ranged"] =
+ &RaidHyjalSummitTriggerContext::rage_winterchill_boss_casts_death_and_decay_on_ranged;
+
+ creators["rage winterchill melee is standing in death and decay"] =
+ &RaidHyjalSummitTriggerContext::rage_winterchill_melee_is_standing_in_death_and_decay;
+
+ // Anetheron
+ creators["anetheron pulling boss or infernal"] =
+ &RaidHyjalSummitTriggerContext::anetheron_pulling_boss_or_infernal;
+
+ creators["anetheron boss engaged by main tank"] =
+ &RaidHyjalSummitTriggerContext::anetheron_boss_engaged_by_main_tank;
+
+ creators["anetheron boss casts carrion swarm"] =
+ &RaidHyjalSummitTriggerContext::anetheron_boss_casts_carrion_swarm;
+
+ creators["anetheron bot is targeted by infernal"] =
+ &RaidHyjalSummitTriggerContext::anetheron_bot_is_targeted_by_infernal;
+
+ creators["anetheron infernals need to be kept away from raid"] =
+ &RaidHyjalSummitTriggerContext::anetheron_infernals_need_to_be_kept_away_from_raid;
+
+ creators["anetheron infernals continue to spawn"] =
+ &RaidHyjalSummitTriggerContext::anetheron_infernals_continue_to_spawn;
+
+ // Kaz'rogal
+ creators["kaz'rogal pulling boss"] =
+ &RaidHyjalSummitTriggerContext::kazrogal_pulling_boss;
+
+ creators["kaz'rogal boss engaged by main tank"] =
+ &RaidHyjalSummitTriggerContext::kazrogal_boss_engaged_by_main_tank;
+
+ creators["kaz'rogal boss engaged by assist tanks"] =
+ &RaidHyjalSummitTriggerContext::kazrogal_boss_engaged_by_assist_tanks;
+
+ creators["kaz'rogal bot is low on mana"] =
+ &RaidHyjalSummitTriggerContext::kazrogal_bot_is_low_on_mana;
+
+ creators["kaz'rogal low mana bots need escape path"] =
+ &RaidHyjalSummitTriggerContext::kazrogal_low_mana_bots_need_escape_path;
+
+ creators["kaz'rogal mark deals shadow damage"] =
+ &RaidHyjalSummitTriggerContext::kazrogal_mark_deals_shadow_damage;
+
+ // Azgalor
+ creators["azgalor pulling boss"] =
+ &RaidHyjalSummitTriggerContext::azgalor_pulling_boss;
+
+ creators["azgalor boss engaged by main tank"] =
+ &RaidHyjalSummitTriggerContext::azgalor_boss_engaged_by_main_tank;
+
+ creators["azgalor main tank is positioning boss"] =
+ &RaidHyjalSummitTriggerContext::azgalor_main_tank_is_positioning_boss;
+
+ creators["azgalor boss engaged by ranged"] =
+ &RaidHyjalSummitTriggerContext::azgalor_boss_engaged_by_ranged;
+
+ creators["azgalor boss casts rain of fire on melee"] =
+ &RaidHyjalSummitTriggerContext::azgalor_boss_casts_rain_of_fire_on_melee;
+
+ creators["azgalor bot is doomed"] =
+ &RaidHyjalSummitTriggerContext::azgalor_bot_is_doomed;
+
+ creators["azgalor doomguards must be controlled"] =
+ &RaidHyjalSummitTriggerContext::azgalor_doomguards_must_be_controlled;
+
+ creators["azgalor doomguards must die"] =
+ &RaidHyjalSummitTriggerContext::azgalor_doomguards_must_die;
+
+ // Archimonde
+ creators["archimonde pulling boss"] =
+ &RaidHyjalSummitTriggerContext::archimonde_pulling_boss;
+
+ creators["archimonde boss engaged by main tank"] =
+ &RaidHyjalSummitTriggerContext::archimonde_boss_engaged_by_main_tank;
+
+ creators["archimonde boss casts fear"] =
+ &RaidHyjalSummitTriggerContext::archimonde_boss_casts_fear;
+
+ creators["archimonde boss casts air burst"] =
+ &RaidHyjalSummitTriggerContext::archimonde_boss_casts_air_burst;
+
+ creators["archimonde boss summoned doomfire"] =
+ &RaidHyjalSummitTriggerContext::archimonde_boss_summoned_doomfire;
+
+ creators["archimonde bot stood in doomfire"] =
+ &RaidHyjalSummitTriggerContext::archimonde_bot_stood_in_doomfire;
+ }
+
+private:
+ // General
+ static Trigger* hyjal_summit_bot_is_not_in_combat(
+ PlayerbotAI* botAI) { return new HyjalSummitBotIsNotInCombatTrigger(botAI); }
+
+ // Rage Winterchill
+ static Trigger* rage_winterchill_pulling_boss(
+ PlayerbotAI* botAI) { return new RageWinterchillPullingBossTrigger(botAI); }
+
+ static Trigger* rage_winterchill_boss_engaged_by_main_tank(
+ PlayerbotAI* botAI) { return new RageWinterchillBossEngagedByMainTankTrigger(botAI); }
+
+ static Trigger* rage_winterchill_boss_casts_death_and_decay_on_ranged(
+ PlayerbotAI* botAI) { return new RageWinterchillBossCastsDeathAndDecayOnRangedTrigger(botAI); }
+
+ static Trigger* rage_winterchill_melee_is_standing_in_death_and_decay(
+ PlayerbotAI* botAI) { return new RageWinterchillMeleeIsStandingInDeathAndDecayTrigger(botAI); }
+
+ // Anetheron
+ static Trigger* anetheron_pulling_boss_or_infernal(
+ PlayerbotAI* botAI) { return new AnetheronPullingBossOrInfernalTrigger(botAI); }
+
+ static Trigger* anetheron_boss_engaged_by_main_tank(
+ PlayerbotAI* botAI) { return new AnetheronBossEngagedByMainTankTrigger(botAI); }
+
+ static Trigger* anetheron_boss_casts_carrion_swarm(
+ PlayerbotAI* botAI) { return new AnetheronBossCastsCarrionSwarmTrigger(botAI); }
+
+ static Trigger* anetheron_bot_is_targeted_by_infernal(
+ PlayerbotAI* botAI) { return new AnetheronBotIsTargetedByInfernalTrigger(botAI); }
+
+ static Trigger* anetheron_infernals_need_to_be_kept_away_from_raid(
+ PlayerbotAI* botAI) { return new AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger(botAI); }
+
+ static Trigger* anetheron_infernals_continue_to_spawn(
+ PlayerbotAI* botAI) { return new AnetheronInfernalsContinueToSpawnTrigger(botAI); }
+
+ // Kaz'rogal
+ static Trigger* kazrogal_pulling_boss(
+ PlayerbotAI* botAI) { return new KazrogalPullingBossTrigger(botAI); }
+
+ static Trigger* kazrogal_boss_engaged_by_main_tank(
+ PlayerbotAI* botAI) { return new KazrogalBossEngagedByMainTankTrigger(botAI); }
+
+ static Trigger* kazrogal_boss_engaged_by_assist_tanks(
+ PlayerbotAI* botAI) { return new KazrogalBossEngagedByAssistTanksTrigger(botAI); }
+
+ static Trigger* kazrogal_low_mana_bots_need_escape_path(
+ PlayerbotAI* botAI) { return new KazrogalLowManaBotsNeedEscapePathTrigger(botAI); }
+
+ static Trigger* kazrogal_bot_is_low_on_mana(
+ PlayerbotAI* botAI) { return new KazrogalBotIsLowOnManaTrigger(botAI); }
+
+ static Trigger* kazrogal_mark_deals_shadow_damage(
+ PlayerbotAI* botAI) { return new KazrogalMarkDealsShadowDamageTrigger(botAI); }
+
+ // Azgalor
+ static Trigger* azgalor_pulling_boss(
+ PlayerbotAI* botAI) { return new AzgalorPullingBossTrigger(botAI); }
+
+ static Trigger* azgalor_boss_engaged_by_main_tank(
+ PlayerbotAI* botAI) { return new AzgalorBossEngagedByMainTankTrigger(botAI); }
+
+ static Trigger* azgalor_main_tank_is_positioning_boss(
+ PlayerbotAI* botAI) { return new AzgalorMainTankIsPositioningBossTrigger(botAI); }
+
+ static Trigger* azgalor_boss_engaged_by_ranged(
+ PlayerbotAI* botAI) { return new AzgalorBossEngagedByRangedTrigger(botAI); }
+
+ static Trigger* azgalor_boss_casts_rain_of_fire_on_melee(
+ PlayerbotAI* botAI) { return new AzgalorBossCastsRainOfFireOnMeleeTrigger(botAI); }
+
+ static Trigger* azgalor_bot_is_doomed(
+ PlayerbotAI* botAI) { return new AzgalorBotIsDoomedTrigger(botAI); }
+
+ static Trigger* azgalor_doomguards_must_be_controlled(
+ PlayerbotAI* botAI) { return new AzgalorDoomguardsMustBeControlledTrigger(botAI); }
+
+ static Trigger* azgalor_doomguards_must_die(
+ PlayerbotAI* botAI) { return new AzgalorDoomguardsMustDieTrigger(botAI); }
+
+ // Archimonde
+ static Trigger* archimonde_pulling_boss(
+ PlayerbotAI* botAI) { return new ArchimondePullingBossTrigger(botAI); }
+
+ static Trigger* archimonde_boss_engaged_by_main_tank(
+ PlayerbotAI* botAI) { return new ArchimondeBossEngagedByMainTankTrigger(botAI); }
+
+ static Trigger* archimonde_boss_casts_fear(
+ PlayerbotAI* botAI) { return new ArchimondeBossCastsFearTrigger(botAI); }
+
+ static Trigger* archimonde_boss_casts_air_burst(
+ PlayerbotAI* botAI) { return new ArchimondeBossCastsAirBurstTrigger(botAI); }
+
+ static Trigger* archimonde_boss_summoned_doomfire(
+ PlayerbotAI* botAI) { return new ArchimondeBossSummonedDoomfireTrigger(botAI); }
+
+ static Trigger* archimonde_bot_stood_in_doomfire(
+ PlayerbotAI* botAI) { return new ArchimondeBotStoodInDoomfireTrigger(botAI); }
+};
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.cpp b/src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.cpp
new file mode 100644
index 000000000..77486fb48
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "RaidHyjalSummitStrategy.h"
+#include "RaidHyjalSummitMultipliers.h"
+
+void RaidHyjalSummitStrategy::InitTriggers(std::vector& triggers)
+{
+ // General
+ triggers.push_back(new TriggerNode("hyjal summit bot is not in combat", {
+ NextAction("hyjal summit erase trackers", ACTION_EMERGENCY + 11) }));
+
+ // Rage Winterchill
+ triggers.push_back(new TriggerNode("rage winterchill pulling boss", {
+ NextAction("rage winterchill misdirect boss to main tank", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("rage winterchill boss engaged by main tank", {
+ NextAction("rage winterchill main tank position boss", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("rage winterchill boss casts death and decay on ranged", {
+ NextAction("rage winterchill spread ranged in circle", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("rage winterchill melee is standing in death and decay", {
+ NextAction("rage winterchill melee get out of death and decay", ACTION_EMERGENCY + 1) }));
+
+ // Anetheron
+ triggers.push_back(new TriggerNode("anetheron pulling boss or infernal", {
+ NextAction("anetheron misdirect boss and infernals to tanks", ACTION_RAID + 3) }));
+
+ triggers.push_back(new TriggerNode("anetheron boss engaged by main tank", {
+ NextAction("anetheron main tank position boss", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("anetheron boss casts carrion swarm", {
+ NextAction("anetheron spread ranged in circle", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("anetheron bot is targeted by infernal", {
+ NextAction("anetheron bring infernal to infernal tank", ACTION_EMERGENCY + 2) }));
+
+ triggers.push_back(new TriggerNode("anetheron infernals need to be kept away from raid", {
+ NextAction("anetheron first assist tank pick up infernals", ACTION_EMERGENCY + 1) }));
+
+ triggers.push_back(new TriggerNode("anetheron infernals continue to spawn", {
+ NextAction("anetheron assign dps priority", ACTION_RAID + 1) }));
+
+ // Kaz'rogal
+ triggers.push_back(new TriggerNode("kaz'rogal pulling boss", {
+ NextAction("kaz'rogal misdirect boss to main tank", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("kaz'rogal boss engaged by main tank", {
+ NextAction("kaz'rogal main tank position boss", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("kaz'rogal boss engaged by assist tanks", {
+ NextAction("kaz'rogal assist tanks move in front of boss", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("kaz'rogal low mana bots need escape path", {
+ NextAction("kaz'rogal spread ranged in arc", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("kaz'rogal bot is low on mana", {
+ NextAction("kaz'rogal low mana bot take defensive measures", ACTION_EMERGENCY + 1) }));
+
+ triggers.push_back(new TriggerNode("kaz'rogal mark deals shadow damage", {
+ NextAction("kaz'rogal cast shadow protection spell", ACTION_EMERGENCY + 6) }));
+
+ // Azgalor
+ triggers.push_back(new TriggerNode("azgalor pulling boss", {
+ NextAction("azgalor misdirect boss to main tank", ACTION_RAID + 3) }));
+
+ triggers.push_back(new TriggerNode("azgalor boss engaged by main tank", {
+ NextAction("azgalor main tank position boss", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("azgalor main tank is positioning boss", {
+ NextAction("azgalor wait at safe position", ACTION_EMERGENCY + 1) }));
+
+ triggers.push_back(new TriggerNode("azgalor boss engaged by ranged", {
+ NextAction("azgalor disperse ranged", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("azgalor boss casts rain of fire on melee", {
+ NextAction("azgalor melee get out of fire and swap targets", ACTION_EMERGENCY + 2) }));
+
+ triggers.push_back(new TriggerNode("azgalor bot is doomed", {
+ NextAction("azgalor move to doomguard tank", ACTION_EMERGENCY + 3) }));
+
+ triggers.push_back(new TriggerNode("azgalor doomguards must be controlled", {
+ NextAction("azgalor first assist tank position doomguard", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("azgalor doomguards must die", {
+ NextAction("azgalor ranged dps prioritize doomguards", ACTION_RAID + 1) }));
+
+ // Archimonde
+ triggers.push_back(new TriggerNode("archimonde pulling boss", {
+ NextAction("archimonde misdirect boss to main tank", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("archimonde boss engaged by main tank", {
+ NextAction("archimonde move boss to initial position", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("archimonde boss casts fear", {
+ NextAction("archimonde cast fear immunity spell", ACTION_RAID + 2) }));
+
+ triggers.push_back(new TriggerNode("archimonde boss casts air burst", {
+ NextAction("archimonde spread to avoid air burst", ACTION_RAID + 1) }));
+
+ triggers.push_back(new TriggerNode("archimonde boss summoned doomfire", {
+ NextAction("archimonde avoid doomfire", ACTION_EMERGENCY + 6) }));
+
+ triggers.push_back(new TriggerNode("archimonde bot stood in doomfire", {
+ NextAction("archimonde remove doomfire dot", ACTION_EMERGENCY + 7) }));
+}
+
+void RaidHyjalSummitStrategy::InitMultipliers(std::vector& multipliers)
+{
+ // Trash
+ multipliers.push_back(new HyjalSummitTimeBloodlustAndHeroismMultiplier(botAI));
+
+ // Rage Winterchill
+ multipliers.push_back(new RageWinterchillDisableCombatFormationMoveMultiplier(botAI));
+ multipliers.push_back(new RageWinterchillMeleeControlAvoidanceMultiplier(botAI));
+
+ // Anetheron
+ multipliers.push_back(new AnetheronDisableTankActionsMultiplier(botAI));
+ multipliers.push_back(new AnetheronDisableCombatFormationMoveMultiplier(botAI));
+ multipliers.push_back(new AnetheronControlMisdirectionMultiplier(botAI));
+
+ // Kaz'rogal
+ multipliers.push_back(new KazrogalLowManaBotStayAwayFromGroupMultiplier(botAI));
+ multipliers.push_back(new KazrogalKeepAspectOfTheViperActiveMultiplier(botAI));
+ multipliers.push_back(new KazrogalControlMovementMultiplier(botAI));
+
+ // Azgalor
+ multipliers.push_back(new AzgalorDisableTankActionsMultiplier(botAI));
+ multipliers.push_back(new AzgalorDoomedBotPrioritizePositioningMultiplier(botAI));
+ multipliers.push_back(new AzgalorMeleeDpsControlAvoidanceMultiplier(botAI));
+
+ // Archimonde
+ multipliers.push_back(new ArchimondeDisableCombatFormationMoveMultiplier(botAI));
+}
diff --git a/src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.h b/src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.h
new file mode 100644
index 000000000..8f0e0d882
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Strategy/RaidHyjalSummitStrategy.h
@@ -0,0 +1,22 @@
+/*
+ * 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_RAIDHYJALSUMMITSTRATEGY_H_
+#define _PLAYERBOT_RAIDHYJALSUMMITSTRATEGY_H_
+
+#include "Strategy.h"
+
+class RaidHyjalSummitStrategy : public Strategy
+{
+public:
+ RaidHyjalSummitStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
+
+ std::string const getName() override { return "hyjal"; }
+
+ void InitTriggers(std::vector& triggers) override;
+ void InitMultipliers(std::vector& multipliers) override;
+};
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.cpp b/src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.cpp
new file mode 100644
index 000000000..3b9a6455d
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.cpp
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "RaidHyjalSummitTriggers.h"
+#include "RaidHyjalSummitHelpers.h"
+#include "RaidHyjalSummitActions.h"
+#include "AiFactory.h"
+#include "Playerbots.h"
+#include "RaidBossHelpers.h"
+
+using namespace HyjalSummitHelpers;
+
+// General
+
+bool HyjalSummitBotIsNotInCombatTrigger::IsActive()
+{
+ return !bot->IsInCombat() && bot->GetMapId() == HYJAL_SUMMIT_MAP_ID;
+}
+
+// Rage Winterchill
+
+bool RageWinterchillPullingBossTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_HUNTER)
+ return false;
+
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ return winterchill && winterchill->GetHealthPct() > 95.0f;
+}
+
+bool RageWinterchillBossEngagedByMainTankTrigger::IsActive()
+{
+ return botAI->IsMainTank(bot) &&
+ AI_VALUE2(Unit*, "find target", "rage winterchill");
+}
+
+bool RageWinterchillBossCastsDeathAndDecayOnRangedTrigger::IsActive()
+{
+ return botAI->IsRanged(bot) &&
+ AI_VALUE2(Unit*, "find target", "rage winterchill");
+}
+
+bool RageWinterchillMeleeIsStandingInDeathAndDecayTrigger::IsActive()
+{
+ if (botAI->IsRanged(bot))
+ return false;
+
+ Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill");
+ if (!winterchill || winterchill->GetVictim() == bot)
+ return false;
+
+ if (botAI->IsMainTank(bot))
+ return false;
+
+ return IsInDeathAndDecay(bot, DEATH_AND_DECAY_SAFE_RADIUS);
+}
+
+// Anetheron
+
+bool AnetheronPullingBossOrInfernalTrigger::IsActive()
+{
+ return bot->getClass() == CLASS_HUNTER &&
+ AI_VALUE2(Unit*, "find target", "anetheron");
+}
+
+bool AnetheronBossEngagedByMainTankTrigger::IsActive()
+{
+ return botAI->IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "anetheron");
+}
+
+bool AnetheronBossCastsCarrionSwarmTrigger::IsActive()
+{
+ if (botAI->IsMelee(bot))
+ return false;
+
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (!anetheron)
+ return false;
+
+ return GetInfernoTarget(anetheron) != bot;
+}
+
+bool AnetheronBotIsTargetedByInfernalTrigger::IsActive()
+{
+ Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron");
+ if (!anetheron || botAI->IsMainTank(bot))
+ return false;
+
+ return GetInfernoTarget(anetheron) == bot;
+}
+
+bool AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger::IsActive()
+{
+ return botAI->IsAssistTankOfIndex(bot, 0, true) &&
+ AI_VALUE2(Unit*, "find target", "towering infernal");
+}
+
+bool AnetheronInfernalsContinueToSpawnTrigger::IsActive()
+{
+ return !botAI->IsTank(bot) && AI_VALUE2(Unit*, "find target", "anetheron");
+}
+
+// Kaz'rogal
+
+bool KazrogalPullingBossTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_HUNTER)
+ return false;
+
+ Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal");
+ return kazrogal && kazrogal->GetHealthPct() > 95.0f;
+}
+
+bool KazrogalBossEngagedByMainTankTrigger::IsActive()
+{
+ return botAI->IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "kaz'rogal");
+}
+
+bool KazrogalBossEngagedByAssistTanksTrigger::IsActive()
+{
+ if (!botAI->IsAssistTank(bot))
+ return false;
+
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return false;
+
+ return bot->GetPower(POWER_MANA) > 3000;
+}
+
+bool KazrogalLowManaBotsNeedEscapePathTrigger::IsActive()
+{
+ if (bot->getClass() == CLASS_WARRIOR || bot->getClass() == CLASS_ROGUE ||
+ bot->getClass() == CLASS_DEATH_KNIGHT)
+ return false;
+
+ uint8 tab = AiFactory::GetPlayerSpecTab(bot);
+ if (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL)
+ return false;
+
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return false;
+
+ if (bot->getClass() == CLASS_HUNTER)
+ {
+ return true;
+ }
+ else if (bot->GetPower(POWER_MANA) > 4000)
+ {
+ isBelowManaThreshold.erase(bot->GetGUID());
+ if (botAI->IsMelee(bot))
+ return false;
+ else
+ return true;
+ }
+
+ return false;
+}
+
+bool KazrogalBotIsLowOnManaTrigger::IsActive()
+{
+ if (bot->getClass() == CLASS_WARRIOR || bot->getClass() == CLASS_ROGUE ||
+ bot->getClass() == CLASS_DEATH_KNIGHT)
+ return false;
+
+ uint8 tab = AiFactory::GetPlayerSpecTab(bot);
+ if (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL)
+ return false;
+
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return false;
+
+ if (botAI->HasAnyAuraOf(bot, "ice block", "divine shield", nullptr))
+ return false;
+
+ if (isBelowManaThreshold.count(bot->GetGUID()) ||
+ bot->GetPower(POWER_MANA) <= 3200)
+ return true;
+
+ return false;
+}
+
+bool KazrogalMarkDealsShadowDamageTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_PALADIN && bot->getClass() != CLASS_WARLOCK)
+ return false;
+
+ if (!AI_VALUE2(Unit*, "find target", "kaz'rogal"))
+ return false;
+
+ if (bot->getClass() == CLASS_PALADIN &&
+ (botAI->HasAura("shadow resistance aura", bot) ||
+ botAI->HasAura("prayer of shadow protection", bot) ||
+ botAI->HasAura("shadow protection", bot)))
+ return false;
+
+ return bot->HasAura(
+ static_cast(HyjalSummitSpells::SPELL_MARK_OF_KAZROGAL));
+}
+
+// Azgalor
+
+bool AzgalorPullingBossTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_HUNTER)
+ return false;
+
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ return azgalor && azgalor->GetHealthPct() > 95.0f;
+}
+
+bool AzgalorBossEngagedByMainTankTrigger::IsActive()
+{
+ return botAI->IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "azgalor");
+}
+
+bool AzgalorMainTankIsPositioningBossTrigger::IsActive()
+{
+ if (botAI->IsRanged(bot))
+ return false;
+
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor || azgalor->GetVictim() == bot)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank || !GET_PLAYERBOT_AI(mainTank) || botAI->IsMainTank(bot))
+ return false;
+
+ TankPositionState tankState = GetAzgalorTankPositionState(botAI, bot);
+ return tankState == TankPositionState::Unknown ||
+ tankState == TankPositionState::MovingToTransition;
+}
+
+bool AzgalorBossEngagedByRangedTrigger::IsActive()
+{
+ if (botAI->IsMelee(bot))
+ return false;
+
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ return azgalor && azgalor->GetVictim() != bot &&
+ !bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM));
+}
+
+bool AzgalorBossCastsRainOfFireOnMeleeTrigger::IsActive()
+{
+ if (botAI->IsRanged(bot) || botAI->IsTank(bot))
+ return false;
+
+ Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor");
+ if (!azgalor || azgalor->GetVictim() == bot ||
+ bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM)))
+ return false;
+
+ return IsInRainOfFire(bot, RAIN_OF_FIRE_RADIUS);
+}
+
+bool AzgalorBotIsDoomedTrigger::IsActive()
+{
+ return bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM));
+}
+
+bool AzgalorDoomguardsMustBeControlledTrigger::IsActive()
+{
+ if (!botAI->IsAssistTank(bot) ||
+ !AI_VALUE2(Unit*, "find target", "azgalor"))
+ return false;
+
+ if (botAI->IsAssistTankOfIndex(bot, 0, true))
+ {
+ return AI_VALUE2(Unit*, "find target", "lesser doomguard") ||
+ AnyGroupMemberHasDoom(bot);
+ }
+
+ if (botAI->IsAssistTankOfIndex(bot, 1, true))
+ {
+ // Trigger for second assist tank only if first assist tank has Doom
+ Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0);
+ if (firstAssistTank &&
+ !firstAssistTank->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM)))
+ return false;
+
+ return AI_VALUE2(Unit*, "find target", "lesser doomguard") ||
+ AnyGroupMemberHasDoom(bot);
+ }
+
+ return false;
+}
+
+bool AzgalorDoomguardsMustDieTrigger::IsActive()
+{
+ return botAI->IsRangedDps(bot) && AI_VALUE2(Unit*, "find target", "azgalor");
+}
+
+// Archimonde
+
+bool ArchimondePullingBossTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_HUNTER)
+ return false;
+
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ return archimonde && archimonde->GetHealthPct() > 95.0f;
+}
+
+bool ArchimondeBossEngagedByMainTankTrigger::IsActive()
+{
+ if (!botAI->IsMainTank(bot))
+ return false;
+
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ return archimonde && archimonde->GetHealthPct() > 95.0f;
+}
+
+bool ArchimondeBossCastsFearTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_PRIEST &&
+ bot->getClass() != CLASS_SHAMAN)
+ return false;
+
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ return archimonde && archimonde->GetHealthPct() > 10.0f;
+}
+
+bool ArchimondeBossCastsAirBurstTrigger::IsActive()
+{
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (!archimonde || archimonde->GetHealthPct() <= 10.0f ||
+ archimonde->GetVictim() == bot)
+ return false;
+
+ return !botAI->IsMainTank(bot);
+}
+
+bool ArchimondeBossSummonedDoomfireTrigger::IsActive()
+{
+ Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde");
+ if (!archimonde || archimonde->GetHealthPct() <= 10.0f)
+ return false;
+
+ // If I don't make an exception, bots actually refuse to enter the
+ // Doomfire even when feared
+ return !bot->HasAura(
+ static_cast(HyjalSummitSpells::SPELL_ARCHIMONDE_FEAR));
+}
+
+bool ArchimondeBotStoodInDoomfireTrigger::IsActive()
+{
+ if (bot->getClass() != CLASS_MAGE && bot->getClass() != CLASS_ROGUE &&
+ bot->getClass() != CLASS_PALADIN)
+ return false;
+
+ return bot->GetHealthPct() < 40.0f &&
+ (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOMFIRE)) ||
+ bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOMFIRE_DOT)));
+}
diff --git a/src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.h b/src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.h
new file mode 100644
index 000000000..5c8fc9f01
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Trigger/RaidHyjalSummitTriggers.h
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#ifndef _PLAYERBOT_RAIDHYJALSUMMITTRIGGERS_H
+#define _PLAYERBOT_RAIDHYJALSUMMITTRIGGERS_H
+
+#include "Trigger.h"
+
+// General
+
+class HyjalSummitBotIsNotInCombatTrigger : public Trigger
+{
+public:
+ HyjalSummitBotIsNotInCombatTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "hyjal summit bot is not in combat") {}
+ bool IsActive() override;
+};
+
+// Rage Winterchill
+
+class RageWinterchillPullingBossTrigger : public Trigger
+{
+public:
+ RageWinterchillPullingBossTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill pulling boss") {}
+ bool IsActive() override;
+};
+
+class RageWinterchillBossEngagedByMainTankTrigger : public Trigger
+{
+public:
+ RageWinterchillBossEngagedByMainTankTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill boss engaged by main tank") {}
+ bool IsActive() override;
+};
+
+class RageWinterchillBossCastsDeathAndDecayOnRangedTrigger : public Trigger
+{
+public:
+ RageWinterchillBossCastsDeathAndDecayOnRangedTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill boss casts death and decay on ranged") {}
+ bool IsActive() override;
+};
+
+class RageWinterchillMeleeIsStandingInDeathAndDecayTrigger : public Trigger
+{
+public:
+ RageWinterchillMeleeIsStandingInDeathAndDecayTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill melee is standing in death and decay") {}
+ bool IsActive() override;
+};
+
+// Anetheron
+
+class AnetheronPullingBossOrInfernalTrigger : public Trigger
+{
+public:
+ AnetheronPullingBossOrInfernalTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "anetheron pulling boss or infernal") {}
+ bool IsActive() override;
+};
+
+class AnetheronBossEngagedByMainTankTrigger : public Trigger
+{
+public:
+ AnetheronBossEngagedByMainTankTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "anetheron boss engaged by main tank") {}
+ bool IsActive() override;
+};
+
+class AnetheronBossCastsCarrionSwarmTrigger : public Trigger
+{
+public:
+ AnetheronBossCastsCarrionSwarmTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "anetheron boss casts carrion swarm") {}
+ bool IsActive() override;
+};
+
+class AnetheronBotIsTargetedByInfernalTrigger : public Trigger
+{
+public:
+ AnetheronBotIsTargetedByInfernalTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "anetheron bot is targeted by infernal") {}
+ bool IsActive() override;
+};
+
+class AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger : public Trigger
+{
+public:
+ AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "anetheron infernals need to be kept away from raid") {}
+ bool IsActive() override;
+};
+
+class AnetheronInfernalsContinueToSpawnTrigger : public Trigger
+{
+public:
+ AnetheronInfernalsContinueToSpawnTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "anetheron infernals continue to spawn") {}
+ bool IsActive() override;
+};
+
+// Kaz'rogal
+
+class KazrogalPullingBossTrigger : public Trigger
+{
+public:
+ KazrogalPullingBossTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal pulling boss") {}
+ bool IsActive() override;
+};
+
+class KazrogalBossEngagedByMainTankTrigger : public Trigger
+{
+public:
+ KazrogalBossEngagedByMainTankTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal boss engaged by main tank") {}
+ bool IsActive() override;
+};
+
+class KazrogalBossEngagedByAssistTanksTrigger : public Trigger
+{
+public:
+ KazrogalBossEngagedByAssistTanksTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal boss engaged by assist tanks") {}
+ bool IsActive() override;
+};
+
+class KazrogalLowManaBotsNeedEscapePathTrigger : public Trigger
+{
+public:
+ KazrogalLowManaBotsNeedEscapePathTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal low mana bots need escape path") {}
+ bool IsActive() override;
+};
+
+class KazrogalBotIsLowOnManaTrigger : public Trigger
+{
+public:
+ KazrogalBotIsLowOnManaTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal bot is low on mana") {}
+ bool IsActive() override;
+};
+
+class KazrogalMarkDealsShadowDamageTrigger : public Trigger
+{
+public:
+ KazrogalMarkDealsShadowDamageTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal mark deals shadow damage") {}
+ bool IsActive() override;
+};
+
+// Azgalor
+
+class AzgalorPullingBossTrigger : public Trigger
+{
+public:
+ AzgalorPullingBossTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor pulling boss") {}
+ bool IsActive() override;
+};
+
+class AzgalorBossEngagedByMainTankTrigger : public Trigger
+{
+public:
+ AzgalorBossEngagedByMainTankTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor boss engaged by main tank") {}
+ bool IsActive() override;
+};
+
+class AzgalorMainTankIsPositioningBossTrigger : public Trigger
+{
+public:
+ AzgalorMainTankIsPositioningBossTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor main tank is positioning boss") {}
+ bool IsActive() override;
+};
+
+class AzgalorBossEngagedByRangedTrigger : public Trigger
+{
+public:
+ AzgalorBossEngagedByRangedTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor boss engaged by ranged") {}
+ bool IsActive() override;
+};
+
+class AzgalorBossCastsRainOfFireOnMeleeTrigger : public Trigger
+{
+public:
+ AzgalorBossCastsRainOfFireOnMeleeTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor boss casts rain of fire on melee") {}
+ bool IsActive() override;
+};
+
+class AzgalorBotIsDoomedTrigger : public Trigger
+{
+public:
+ AzgalorBotIsDoomedTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor bot is doomed") {}
+ bool IsActive() override;
+};
+
+class AzgalorDoomguardsMustBeControlledTrigger : public Trigger
+{
+public:
+ AzgalorDoomguardsMustBeControlledTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor doomguards must be controlled") {}
+ bool IsActive() override;
+};
+
+class AzgalorDoomguardsMustDieTrigger : public Trigger
+{
+public:
+ AzgalorDoomguardsMustDieTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "azgalor doomguards must die") {}
+ bool IsActive() override;
+};
+
+// Archimonde
+
+class ArchimondePullingBossTrigger : public Trigger
+{
+public:
+ ArchimondePullingBossTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "archimonde pulling boss") {}
+ bool IsActive() override;
+};
+
+class ArchimondeBossEngagedByMainTankTrigger : public Trigger
+{
+public:
+ ArchimondeBossEngagedByMainTankTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss engaged by main tank") {}
+ bool IsActive() override;
+};
+
+class ArchimondeBossCastsFearTrigger : public Trigger
+{
+public:
+ ArchimondeBossCastsFearTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss casts fear") {}
+ bool IsActive() override;
+};
+
+class ArchimondeBossCastsAirBurstTrigger : public Trigger
+{
+public:
+ ArchimondeBossCastsAirBurstTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss casts air burst") {}
+ bool IsActive() override;
+};
+
+class ArchimondeBossSummonedDoomfireTrigger : public Trigger
+{
+public:
+ ArchimondeBossSummonedDoomfireTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss summoned doomfire") {}
+ bool IsActive() override;
+};
+
+class ArchimondeBotStoodInDoomfireTrigger : public Trigger
+{
+public:
+ ArchimondeBotStoodInDoomfireTrigger(
+ PlayerbotAI* botAI) : Trigger(botAI, "archimonde bot stood in doomfire") {}
+ bool IsActive() override;
+};
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.cpp b/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.cpp
new file mode 100644
index 000000000..04ed97cd0
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.cpp
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "RaidHyjalSummitHelpers.h"
+
+#include
+
+#include "Playerbots.h"
+#include "RaidBossHelpers.h"
+#include "Timer.h"
+
+namespace HyjalSummitHelpers
+{
+ // General
+
+ bool GetGroundedStepPosition(
+ Player* bot, float destinationX, float destinationY, float moveDist,
+ float& stepX, float& stepY, float& stepZ)
+ {
+ const float distance = bot->GetExactDist2d(destinationX, destinationY);
+ if (distance <= 0.0f)
+ return false;
+
+ const float stepDistance = std::min(moveDist, distance);
+ const float deltaX = destinationX - bot->GetPositionX();
+ const float deltaY = destinationY - bot->GetPositionY();
+ stepX = bot->GetPositionX() + (deltaX / distance) * stepDistance;
+ stepY = bot->GetPositionY() + (deltaY / distance) * stepDistance;
+ stepZ = bot->GetMapWaterOrGroundLevel(stepX, stepY, bot->GetPositionZ());
+ if (stepZ <= INVALID_HEIGHT)
+ stepZ = bot->GetPositionZ();
+
+ bot->GetMap()->CheckCollisionAndGetValidCoords(
+ bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
+ stepX, stepY, stepZ, false);
+
+ return true;
+ }
+
+ RangedGroups GetRangedGroups(PlayerbotAI* botAI, Player* bot)
+ {
+ RangedGroups result;
+ Group* group = bot->GetGroup();
+ if (!group)
+ return result;
+
+ for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
+ {
+ Player* member = ref->GetSource();
+ if (!member || !botAI->IsRanged(member))
+ continue;
+
+ if (botAI->IsHeal(member))
+ result.healers.push_back(member);
+ else
+ result.rangedDps.push_back(member);
+ }
+
+ return result;
+ }
+
+ std::pair GetBotCircleIndexAndCount(PlayerbotAI* botAI, Player* bot,
+ const RangedGroups& groups)
+ {
+ const std::vector& vec = botAI->IsHeal(bot) ? groups.healers : groups.rangedDps;
+ auto it = std::find(vec.begin(), vec.end(), bot);
+ size_t index = (it != vec.end()) ? std::distance(vec.begin(), it) : 0;
+
+ return {index, vec.size()};
+ }
+
+ // Rage Winterchill
+
+ const Position WINTERCHILL_TANK_POSITION = { 5031.061f, -1784.521f, 1321.626f };
+ std::unordered_map hasReachedWinterchillPosition;
+ std::unordered_map deathAndDecayPosition;
+
+ DeathAndDecayData* GetActiveWinterchillDeathAndDecay(uint32 instanceId)
+ {
+ auto instanceIt = deathAndDecayPosition.find(instanceId);
+ if (instanceIt == deathAndDecayPosition.end())
+ return nullptr;
+
+ const uint32 now = getMSTime();
+ const uint32 elapsed = getMSTimeDiff(instanceIt->second.spawnTime, now);
+ if (elapsed >= DEATH_AND_DECAY_REACQUIRE_DELAY)
+ {
+ deathAndDecayPosition.erase(instanceIt);
+ return nullptr;
+ }
+
+ if (elapsed >= DEATH_AND_DECAY_DURATION)
+ return nullptr;
+
+ return &instanceIt->second;
+ }
+
+ bool IsInDeathAndDecay(Player* bot, float radius)
+ {
+ const uint32 instanceId = bot->GetMap()->GetInstanceId();
+ Aura* aura = bot->GetAura(static_cast(HyjalSummitSpells::SPELL_DEATH_AND_DECAY));
+ if (aura)
+ {
+ DynamicObject* dynObj = aura->GetDynobjOwner();
+ if (dynObj && dynObj->IsInWorld())
+ {
+ const uint32 now = getMSTime();
+ auto instanceIt = deathAndDecayPosition.find(instanceId);
+ if (instanceIt == deathAndDecayPosition.end() ||
+ getMSTimeDiff(instanceIt->second.spawnTime, now) >= DEATH_AND_DECAY_REACQUIRE_DELAY)
+ {
+ deathAndDecayPosition[instanceId] =
+ DeathAndDecayData{ dynObj->GetPosition(), now };
+ }
+ }
+ }
+
+ DeathAndDecayData* data = GetActiveWinterchillDeathAndDecay(instanceId);
+ if (!data)
+ return false;
+
+ return bot->GetExactDist2d(data->position) < radius;
+ }
+
+ // Anetheron
+
+ const Position ANETHERON_TANK_POSITION = { 5033.177f, -1765.996f, 1324.195f };
+ const Position ANETHERON_E_INFERNAL_POSITION = { 5016.578f, -1800.233f, 1323.070f };
+ const Position ANETHERON_W_INFERNAL_POSITION = { 5048.911f, -1722.164f, 1321.408f };
+ std::unordered_map hasReachedAnetheronPosition;
+
+ Player* GetInfernoTarget(Unit* anetheron)
+ {
+ if (!anetheron)
+ return nullptr;
+
+ Spell* spell = anetheron->GetCurrentSpell(CURRENT_GENERIC_SPELL);
+ if (spell && spell->m_spellInfo->Id ==
+ static_cast(HyjalSummitSpells::SPELL_INFERNO))
+ {
+ Unit* spellTarget = spell->m_targets.GetUnitTarget();
+ if (spellTarget && spellTarget->IsPlayer())
+ return spellTarget->ToPlayer();
+ }
+
+ return nullptr;
+ }
+
+ const Position& GetClosestInfernalTankPosition(Player* bot)
+ {
+ const Position& east = ANETHERON_E_INFERNAL_POSITION;
+ const Position& west = ANETHERON_W_INFERNAL_POSITION;
+ return (bot->GetExactDist2d(east.GetPositionX(), east.GetPositionY()) <=
+ bot->GetExactDist2d(west.GetPositionX(), west.GetPositionY())) ? east : west;
+ }
+
+ // Kaz'rogal
+
+ const Position KAZROGAL_TANK_TRANSITION_POSITION = { 5528.792f, -2636.486f, 1481.293f };
+ const Position KAZROGAL_TANK_FINAL_POSITION = { 5511.514f, -2662.466f, 1480.288f };
+ std::unordered_map kazrogalTankStep;
+ std::unordered_map isBelowManaThreshold;
+
+ TankPositionState GetKazrogalTankPositionState(PlayerbotAI* botAI, Player* bot)
+ {
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return TankPositionState::Unknown;
+
+ auto it = kazrogalTankStep.find(mainTank->GetGUID());
+ if (it != kazrogalTankStep.end())
+ return it->second;
+
+ return TankPositionState::Unknown;
+ }
+
+ // Azgalor
+
+ const Position AZGALOR_TANK_TRANSITION_POSITION = { 5486.787f, -2696.215f, 1482.007f };
+ const Position AZGALOR_TANK_FINAL_POSITION = { 5496.379f, -2675.265f, 1481.053f };
+ const Position AZGALOR_DOOMGUARD_POSITION = { 5485.555f, -2731.659f, 1485.555f };
+ std::unordered_map azgalorTankStep;
+ std::unordered_map rainOfFirePosition;
+
+ RainOfFireData* GetActiveAzgalorRainOfFire(uint32 instanceId)
+ {
+ auto instanceIt = rainOfFirePosition.find(instanceId);
+ if (instanceIt == rainOfFirePosition.end())
+ return nullptr;
+
+ const uint32 now = getMSTime();
+ const uint32 elapsed = getMSTimeDiff(instanceIt->second.spawnTime, now);
+ if (elapsed >= RAIN_OF_FIRE_REACQUIRE_DELAY)
+ {
+ rainOfFirePosition.erase(instanceIt);
+ return nullptr;
+ }
+
+ if (elapsed >= RAIN_OF_FIRE_DURATION)
+ return nullptr;
+
+ return &instanceIt->second;
+ }
+
+ TankPositionState GetAzgalorTankPositionState(PlayerbotAI* botAI, Player* bot)
+ {
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank)
+ return TankPositionState::Unknown;
+
+ auto it = azgalorTankStep.find(mainTank->GetGUID());
+ if (it != azgalorTankStep.end())
+ return it->second;
+
+ return TankPositionState::Unknown;
+ }
+
+ bool IsInRainOfFire(Player* bot, float radius)
+ {
+ RainOfFireData* data = GetActiveAzgalorRainOfFire(bot->GetMap()->GetInstanceId());
+ if (!data)
+ return false;
+
+ return bot->GetExactDist2d(data->position) < radius;
+ }
+
+ bool AnyGroupMemberHasDoom(Player* bot)
+ {
+ if (Group* group = bot->GetGroup())
+ {
+ for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
+ {
+ Player* member = ref->GetSource();
+ if (member &&
+ member->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM)))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Archimonde
+
+ const Position ARCHIMONDE_INITIAL_POSITION = { 5640.502f, -3421.238f, 1587.453f };
+ std::unordered_map archimondeAirBurstTargets;
+ std::unordered_map> doomfireTrails;
+ std::unordered_map doomfireLastSampleTime;
+
+ AirBurstData* GetRecentArchimondeAirBurst(uint32 instanceId)
+ {
+ auto instanceIt = archimondeAirBurstTargets.find(instanceId);
+ if (instanceIt == archimondeAirBurstTargets.end())
+ return nullptr;
+
+ constexpr uint32 airBurstReactionWindow = 2000;
+ const uint32 now = getMSTime();
+ if (getMSTimeDiff(instanceIt->second.castTime, now) >= airBurstReactionWindow)
+ {
+ archimondeAirBurstTargets.erase(instanceIt);
+ return nullptr;
+ }
+
+ return &instanceIt->second;
+ }
+}
diff --git a/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.h b/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.h
new file mode 100644
index 000000000..d5c8f9997
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitHelpers.h
@@ -0,0 +1,143 @@
+/*
+ * 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_RAIDHYJALSUMMITHELPERS_H_
+#define _PLAYERBOT_RAIDHYJALSUMMITHELPERS_H_
+
+#include
+#include
+#include
+
+#include "AiObject.h"
+#include "Position.h"
+#include "Unit.h"
+
+namespace HyjalSummitHelpers
+{
+ enum class HyjalSummitSpells : uint32
+ {
+ // Rage Winterchill
+ SPELL_DEATH_AND_DECAY = 31258,
+
+ // Anetheron
+ SPELL_INFERNO = 31299,
+
+ // Kaz'rogal
+ SPELL_MARK_OF_KAZROGAL = 31447,
+
+ // Azgalor
+ SPELL_RAIN_OF_FIRE = 31340,
+ SPELL_DOOM = 31347,
+
+ // Archimonde
+ SPELL_DOOMFIRE = 31944, // Damaging part of trail
+ SPELL_DOOMFIRE_DOT = 31969, // DoT after exiting trail
+ SPELL_ARCHIMONDE_FEAR = 31970,
+ SPELL_AIR_BURST = 32014,
+
+ // Hunter
+ SPELL_MISDIRECTION = 35079,
+
+ // Priest
+ SPELL_FEAR_WARD = 6346,
+ };
+
+ enum class HyjalSummitNpcs : uint32
+ {
+ // Archimonde
+ NPC_DOOMFIRE = 18095,
+ };
+
+ enum class TankPositionState : uint8
+ {
+ MovingToTransition = 0,
+ MovingToFinal = 1,
+ Positioned = 2,
+ Unknown = 255,
+ };
+
+ // General
+ constexpr uint32 HYJAL_SUMMIT_MAP_ID = 534;
+ struct RangedGroups
+ {
+ std::vector healers;
+ std::vector rangedDps;
+ };
+ bool GetGroundedStepPosition(
+ Player* bot, float destinationX, float destinationY, float moveDist,
+ float& stepX, float& stepY, float& stepZ);
+ RangedGroups GetRangedGroups(PlayerbotAI* botAI, Player* bot);
+ std::pair GetBotCircleIndexAndCount(PlayerbotAI* botAI, Player* bot,
+ const RangedGroups& groups);
+
+ // Rage Winterchill
+ extern const Position WINTERCHILL_TANK_POSITION;
+ extern std::unordered_map hasReachedWinterchillPosition;
+ constexpr uint32 DEATH_AND_DECAY_DURATION = 15000;
+ constexpr uint32 DEATH_AND_DECAY_REACQUIRE_DELAY = 20000;
+ constexpr float DEATH_AND_DECAY_SAFE_RADIUS = 22.0f; // 20y radius + 1.5y player hitbox + 0.5y buffer
+ struct DeathAndDecayData
+ {
+ Position position;
+ uint32 spawnTime;
+ };
+ extern std::unordered_map deathAndDecayPosition;
+ DeathAndDecayData* GetActiveWinterchillDeathAndDecay(uint32 instanceId);
+ bool IsInDeathAndDecay(Player* bot, float radius);
+
+ // Anetheron
+ extern const Position ANETHERON_TANK_POSITION;
+ extern const Position ANETHERON_E_INFERNAL_POSITION;
+ extern const Position ANETHERON_W_INFERNAL_POSITION;
+ extern std::unordered_map hasReachedAnetheronPosition;
+ Player* GetInfernoTarget(Unit* anetheron);
+ const Position& GetClosestInfernalTankPosition(Player* bot);
+
+ // Kaz'rogal
+ extern const Position KAZROGAL_TANK_TRANSITION_POSITION;
+ extern const Position KAZROGAL_TANK_FINAL_POSITION;
+ extern std::unordered_map kazrogalTankStep;
+ extern std::unordered_map isBelowManaThreshold;
+ TankPositionState GetKazrogalTankPositionState(PlayerbotAI* botAI, Player* bot);
+
+ // Azgalor
+ extern const Position AZGALOR_TANK_TRANSITION_POSITION;
+ extern const Position AZGALOR_TANK_FINAL_POSITION;
+ extern const Position AZGALOR_DOOMGUARD_POSITION;
+ extern std::unordered_map azgalorTankStep;
+ constexpr uint32 RAIN_OF_FIRE_DURATION = 10000;
+ constexpr uint32 RAIN_OF_FIRE_REACQUIRE_DELAY = 15000;
+ constexpr float RAIN_OF_FIRE_RADIUS = 17.0f; // 15y radius + 1.5y player hitbox + 0.5y buffer
+ struct RainOfFireData
+ {
+ Position position;
+ uint32 spawnTime;
+ };
+ extern std::unordered_map rainOfFirePosition;
+ TankPositionState GetAzgalorTankPositionState(PlayerbotAI* botAI, Player* bot);
+ RainOfFireData* GetActiveAzgalorRainOfFire(uint32 instanceId);
+ bool IsInRainOfFire(Player* bot, float radius);
+ bool AnyGroupMemberHasDoom(Player* bot);
+
+ // Archimonde
+ constexpr float AIR_BURST_SAFE_DISTANCE = 15.0f;
+ struct AirBurstData
+ {
+ ObjectGuid targetGuid;
+ uint32 castTime;
+ };
+ struct DoomfireTrailData
+ {
+ Position position;
+ uint32 recordTime;
+ };
+ extern const Position ARCHIMONDE_INITIAL_POSITION;
+ extern std::unordered_map archimondeAirBurstTargets;
+ extern std::unordered_map> doomfireTrails;
+ extern std::unordered_map doomfireLastSampleTime;
+ AirBurstData* GetRecentArchimondeAirBurst(uint32 instanceId);
+}
+
+#endif
diff --git a/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitScripts.cpp b/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitScripts.cpp
new file mode 100644
index 000000000..a1ccf4005
--- /dev/null
+++ b/src/Ai/Raid/HyjalSummit/Util/RaidHyjalSummitScripts.cpp
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it
+ * and/or modify it under version 3 of the License, or (at your option), any later version.
+ */
+
+#include "RaidHyjalSummitHelpers.h"
+#include "AllCreatureScript.h"
+#include "ObjectAccessor.h"
+#include "Player.h"
+#include "RaidBossHelpers.h"
+#include "DynamicObjectScript.h"
+#include "Playerbots.h"
+#include "ScriptMgr.h"
+#include "Spell.h"
+#include "Timer.h"
+
+using namespace HyjalSummitHelpers;
+
+static Player* GetFirstPlayerSpellTarget(Spell* spell, Unit* caster)
+{
+ if (!spell || !caster)
+ return nullptr;
+
+ if (Unit* unitTarget = spell->m_targets.GetUnitTarget())
+ return unitTarget->ToPlayer();
+
+ std::list const& targets = *spell->GetUniqueTargetInfo();
+ for (TargetInfo const& targetInfo : targets)
+ {
+ if (Player* target = ObjectAccessor::GetPlayer(*caster, targetInfo.targetGUID))
+ return target;
+ }
+
+ return nullptr;
+}
+
+static bool ShouldInterruptForArchimondeAirBurst(PlayerbotAI* botAI, Player* bot, Player* target)
+{
+ if (!target)
+ return false;
+
+ Player* mainTank = GetGroupMainTank(botAI, bot);
+ if (!mainTank || bot == mainTank)
+ return false;
+
+ float distanceToMainTank = bot->GetExactDist2d(mainTank);
+
+ return (target == mainTank || target == bot) &&
+ distanceToMainTank < AIR_BURST_SAFE_DISTANCE;
+}
+
+// Records the active Rain of Fire dynamic object so that melee bots can avoid it by running
+// away from Azgalor or swapping to a Doomguard; the standard FleePosition() logic to avoid aoe
+// can take melee in front of Azgalor, resulting in them getting cleaved
+class AzgalorRainOfFireScript : public DynamicObjectScript
+{
+public:
+ AzgalorRainOfFireScript() : DynamicObjectScript("AzgalorRainOfFireScript") {}
+
+ void OnUpdate(DynamicObject* dynobj, uint32 /*diff*/) override
+ {
+ if (dynobj->GetSpellId() != static_cast(HyjalSummitSpells::SPELL_RAIN_OF_FIRE))
+ return;
+
+ uint32 instanceId = dynobj->GetMap()->GetInstanceId();
+ if (GetActiveAzgalorRainOfFire(instanceId))
+ return;
+
+ uint32 now = getMSTime();
+ auto instanceIt = rainOfFirePosition.find(instanceId);
+ if (instanceIt != rainOfFirePosition.end() &&
+ getMSTimeDiff(instanceIt->second.spawnTime, now) < RAIN_OF_FIRE_REACQUIRE_DELAY)
+ {
+ return;
+ }
+
+ bool shouldTrackRainOfFire = false;
+ Map::PlayerList const& players = dynobj->GetMap()->GetPlayers();
+ for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it)
+ {
+ Player* player = it->GetSource();
+ if (!player || !player->IsAlive())
+ continue;
+
+ PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
+ if (!botAI || !botAI->HasStrategy("hyjal", BOT_STATE_COMBAT))
+ continue;
+
+ shouldTrackRainOfFire = true;
+ break;
+ }
+
+ if (!shouldTrackRainOfFire)
+ return;
+
+ rainOfFirePosition[instanceId] = RainOfFireData{ dynobj->GetPosition(), now };
+ }
+};
+
+// Records the position of each Doomfire NPC at regular intervals so that bots can avoid
+// the persistent fire trail it leaves behind. Each sample is tagged with a timestamp and
+// expires after TRAIL_DURATION ms, matching the lifetime of a Doomfire DynamicObject (18s)
+class ArchimondeDoomfireTrailScript : public AllCreatureScript
+{
+public:
+ ArchimondeDoomfireTrailScript() : AllCreatureScript("ArchimondeDoomfireTrailScript") {}
+
+ void OnAllCreatureUpdate(Creature* creature, uint32 /*diff*/) override
+ {
+ if (creature->GetEntry() != static_cast(HyjalSummitNpcs::NPC_DOOMFIRE))
+ return;
+
+ uint32 now = getMSTime();
+ ObjectGuid guid = creature->GetGUID();
+
+ auto& lastSample = doomfireLastSampleTime[guid];
+ if (getMSTimeDiff(lastSample, now) < 500)
+ return;
+
+ lastSample = now;
+
+ uint32 instanceId = creature->GetMap()->GetInstanceId();
+ auto& trail = doomfireTrails[instanceId];
+
+ DoomfireTrailData data;
+ data.position = creature->GetPosition();
+ data.recordTime = now;
+ trail.push_back(data);
+
+ constexpr uint32 TRAIL_DURATION = 18000;
+ trail.erase(std::remove_if(trail.begin(), trail.end(),
+ [now](const DoomfireTrailData& d)
+ {
+ return getMSTimeDiff(d.recordTime, now) > TRAIL_DURATION;
+ }), trail.end());
+
+ constexpr float DOOMFIRE_DANGER_RANGE = 10.0f;
+ Map::PlayerList const& players = creature->GetMap()->GetPlayers();
+ for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it)
+ {
+ Player* player = it->GetSource();
+ if (!player || !player->IsAlive())
+ continue;
+
+ PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
+ if (!botAI || !botAI->HasStrategy("hyjal", BOT_STATE_COMBAT) ||
+ creature->GetDistance(player) > DOOMFIRE_DANGER_RANGE)
+ {
+ continue;
+ }
+
+ botAI->RequestSpellInterrupt();
+ }
+ }
+
+ void OnCreatureRemoveWorld(Creature* creature) override
+ {
+ if (creature->GetEntry() != static_cast(HyjalSummitNpcs::NPC_DOOMFIRE))
+ return;
+
+ doomfireLastSampleTime.erase(creature->GetGUID());
+ }
+};
+
+class ArchimondeAirBurstSpellListenerScript : public AllSpellScript
+{
+public:
+ ArchimondeAirBurstSpellListenerScript() :
+ AllSpellScript("ArchimondeAirBurstSpellListenerScript") {}
+
+ void OnSpellCast(
+ Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override
+ {
+ if (!spell || !caster || !spellInfo)
+ return;
+
+ if (spellInfo->Id != static_cast(HyjalSummitSpells::SPELL_AIR_BURST))
+ return;
+
+ Player* target = GetFirstPlayerSpellTarget(spell, caster);
+ if (!target)
+ return;
+
+ archimondeAirBurstTargets[caster->GetMap()->GetInstanceId()] =
+ AirBurstData{ target->GetGUID(), getMSTime() };
+
+ Map::PlayerList const& players = caster->GetMap()->GetPlayers();
+ for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it)
+ {
+ Player* player = it->GetSource();
+ if (!player || !player->IsAlive())
+ continue;
+
+ PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
+ if (!botAI || !botAI->HasStrategy("hyjal", BOT_STATE_COMBAT) ||
+ !ShouldInterruptForArchimondeAirBurst(botAI, player, target))
+ {
+ continue;
+ }
+
+ botAI->RequestSpellInterrupt();
+ }
+ }
+};
+
+void AddSC_HyjalSummitBotScripts()
+{
+ new AzgalorRainOfFireScript();
+ new ArchimondeDoomfireTrailScript();
+ new ArchimondeAirBurstSpellListenerScript();
+}
diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h
index 970ecf4aa..bdc76c4a7 100644
--- a/src/Ai/Raid/RaidStrategyContext.h
+++ b/src/Ai/Raid/RaidStrategyContext.h
@@ -11,6 +11,7 @@
#include "RaidNaxxStrategy.h"
#include "RaidSSCStrategy.h"
#include "RaidTempestKeepStrategy.h"
+#include "RaidHyjalSummitStrategy.h"
#include "RaidZulAmanStrategy.h"
#include "RaidOsStrategy.h"
#include "RaidEoEStrategy.h"
@@ -33,6 +34,7 @@ public:
creators["naxx"] = &RaidStrategyContext::naxx;
creators["ssc"] = &RaidStrategyContext::ssc;
creators["tempestkeep"] = &RaidStrategyContext::tempestkeep;
+ creators["hyjal"] = &RaidStrategyContext::hyjal;
creators["zulaman"] = &RaidStrategyContext::zulaman;
creators["wotlk-os"] = &RaidStrategyContext::wotlk_os;
creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe;
@@ -52,6 +54,7 @@ private:
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* hyjal(PlayerbotAI* botAI) { return new RaidHyjalSummitStrategy(botAI); }
static Strategy* zulaman(PlayerbotAI* botAI) { return new RaidZulAmanStrategy(botAI); }
static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); }
static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); }
diff --git a/src/Bot/Engine/BuildSharedActionContexts.cpp b/src/Bot/Engine/BuildSharedActionContexts.cpp
index 7e243eadb..4c2c8fad3 100644
--- a/src/Bot/Engine/BuildSharedActionContexts.cpp
+++ b/src/Bot/Engine/BuildSharedActionContexts.cpp
@@ -11,6 +11,7 @@
#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h"
#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h"
#include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h"
+#include "Ai/Raid/HyjalSummit/RaidHyjalSummitActionContext.h"
#include "Ai/Raid/ZulAman/RaidZulAmanActionContext.h"
#include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h"
#include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h"
@@ -34,6 +35,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList allInstanceStrategies =
{
- "aq20", "bwl", "karazhan", "gruulslair", "icc", "magtheridon", "moltencore",
- "naxx", "onyxia", "ssc", "tbc-ac", "tempestkeep", "ulduar", "voa", "wotlk-an", "wotlk-cos",
- "wotlk-dtk", "wotlk-eoe", "wotlk-fos", "wotlk-gd", "wotlk-hol", "wotlk-hor",
- "wotlk-hos", "wotlk-nex", "wotlk-occ", "wotlk-ok", "wotlk-os", "wotlk-pos",
- "wotlk-toc", "wotlk-uk", "wotlk-up", "wotlk-vh", "zulaman"
+ "aq20", "bwl", "karazhan", "gruulslair", "hyjal", "icc", "magtheridon",
+ "moltencore", "naxx", "onyxia", "ssc", "tbc-ac", "tempestkeep", "ulduar",
+ "voa", "wotlk-an", "wotlk-cos", "wotlk-dtk", "wotlk-eoe", "wotlk-fos",
+ "wotlk-gd", "wotlk-hol", "wotlk-hor", "wotlk-hos", "wotlk-nex", "wotlk-occ",
+ "wotlk-ok", "wotlk-os", "wotlk-pos", "wotlk-toc", "wotlk-uk", "wotlk-up",
+ "wotlk-vh", "zulaman"
};
for (const std::string& strat : allInstanceStrategies)
@@ -1620,6 +1621,9 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster)
case 533:
strategyName = "naxx"; // Naxxramas
break;
+ case 534:
+ strategyName = "hyjal"; // The Battle for Mount Hyjal (Hyjal Summit)
+ break;
case 544:
strategyName = "magtheridon"; // Magtheridon's Lair
break;
diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp
index 5be7e8855..ab92729a7 100644
--- a/src/Script/Playerbots.cpp
+++ b/src/Script/Playerbots.cpp
@@ -526,6 +526,7 @@ public:
void AddPlayerbotsSecureLoginScripts();
void AddSC_TempestKeepBotScripts();
+void AddSC_HyjalSummitBotScripts();
void AddPlayerbotsScripts()
{
@@ -541,4 +542,5 @@ void AddPlayerbotsScripts()
AddPlayerbotsCommandscripts();
PlayerBotsGuildValidationScript();
AddSC_TempestKeepBotScripts();
+ AddSC_HyjalSummitBotScripts();
}
From 8caf37af97b74545f8fa65172095803466ad8a06 Mon Sep 17 00:00:00 2001
From: Ivan Novokhatski
Date: Sat, 9 May 2026 07:39:55 +0200
Subject: [PATCH 11/19] Add EnableAutoTradeOnItemMention config option (#2323)
## Pull Request Description
This PR adds a config parameter
`AiPlayerbot.EnableAutoTradeOnItemMention` that controls whether trade
dialogues and inventory listings will be triggered for messages that
contain keywords anywhere in their text (for example "got some food?").
The default value is `1/true`, so for existing installs there will be no
change.
This is useful for other mods that could utilise game chats for other
purposes, specifically my
[mod-playerbots-characters](https://github.com/deseven/mod-playerbots-characters)
and @DustinHendrickson 's
[mod-ollama-chat](https://github.com/DustinHendrickson/mod-ollama-chat).
Individual users might also benefit from the ability to disable this
functionality.
## Feature Evaluation
N/A
## How to Test the Changes
1. Start the server with default config and join the game.
2. Get into a party with one or more bots.
3. Write `got some food?` to the party chat.
4. A trade dialogue along with the whispers from the bots should pop up.
5. Stop the server, change `AiPlayerbot.EnableAutoTradeOnItemMention` to
`0`.
6. Start the server, join the game.
7. Get into a party with one or more bots.
8. Write `got some food?` to the party chat.
9. Nothing should happen.
> [!NOTE]
> In both cases the commands `t something` and `c something` should
still work.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
N/A
---
conf/playerbots.conf.dist | 8 ++++++++
src/Bot/Engine/ExternalEventHelper.cpp | 7 +++++--
src/PlayerbotAIConfig.cpp | 1 +
src/PlayerbotAIConfig.h | 1 +
4 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist
index 3e72a5058..41ca0b6b4 100644
--- a/conf/playerbots.conf.dist
+++ b/conf/playerbots.conf.dist
@@ -2240,6 +2240,14 @@ AiPlayerbot.CommandPrefix = ""
# Separator for bot chat commands
AiPlayerbot.CommandSeparator = "\\\\"
+# Enable automatic item count/trade trigger when a chat message contains
+# item-related keywords. When enabled (1), mentioning items in chat
+# (e.g. "food", "potion", "ammo") will automatically show inventory and
+# open a trade window with the bot. Explicit "c" and "t" commands still
+# work regardless of this setting.
+# Default: 1 (enabled)
+AiPlayerbot.EnableAutoTradeOnItemMention = 1
+
# Enable bots talking (say / yell / general chatting / lfg)
AiPlayerbot.RandomBotTalk = 1
# Enable bots emoting
diff --git a/src/Bot/Engine/ExternalEventHelper.cpp b/src/Bot/Engine/ExternalEventHelper.cpp
index 3a62fbda9..912f65877 100644
--- a/src/Bot/Engine/ExternalEventHelper.cpp
+++ b/src/Bot/Engine/ExternalEventHelper.cpp
@@ -33,8 +33,11 @@ bool ExternalEventHelper::ParseChatCommand(std::string const command, Player* ow
if (!ChatHelper::parseableItem(command))
return false;
- HandleCommand("c", command, owner);
- HandleCommand("t", command, owner);
+ if (sPlayerbotAIConfig.enableAutoTradeOnItemMention)
+ {
+ HandleCommand("c", command, owner);
+ HandleCommand("t", command, owner);
+ }
return true;
}
diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp
index 242febd17..8a0c6b0a0 100644
--- a/src/PlayerbotAIConfig.cpp
+++ b/src/PlayerbotAIConfig.cpp
@@ -519,6 +519,7 @@ bool PlayerbotAIConfig::Initialize()
LoadListString>(sConfigMgr->GetOption("AiPlayerbot.AllowedLogFiles", ""),
allowedLogFiles);
+ enableAutoTradeOnItemMention = sConfigMgr->GetOption("AiPlayerbot.EnableAutoTradeOnItemMention", true);
LoadListString>(sConfigMgr->GetOption("AiPlayerbot.TradeActionExcludedPrefixes", ""),
tradeActionExcludedPrefixes);
diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h
index 210e03ef9..1a343db4d 100644
--- a/src/PlayerbotAIConfig.h
+++ b/src/PlayerbotAIConfig.h
@@ -306,6 +306,7 @@ public:
uint32 iterationsPerTick;
std::mutex m_logMtx;
+ bool enableAutoTradeOnItemMention;
std::vector tradeActionExcludedPrefixes;
std::vector allowedLogFiles;
std::unordered_map> logFiles;
From 7af675e712023fc553fc7ac595ff2e638bd4f0f5 Mon Sep 17 00:00:00 2001
From: Ivan Novokhatski
Date: Sat, 9 May 2026 07:40:16 +0200
Subject: [PATCH 12/19] Respect worldserver's PreventAFKLogout value (#2328)
## Pull Request Description
This adds checks to prevent bots from logging out when the master isn't
actually logging out, respecting the `PreventAFKLogout` setting in
`worldserver.conf`. Otherwise, returning to the game after a long pause
means your bots are offline and you have to re-add them, which is
annoying.
## Feature Evaluation
N/A
## How to Test the Changes
1. Set `PreventAFKLogout` to 1 or 2 in `worldserver.conf`.
2. Start the server, log in, add some bots to your party.
3. Go to a sanctuary if you set `PreventAFKLogout` to 1 or just start
idling anywhere otherwise.
4. Both you and the bots will stay in-game no matter how much time has
passed.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
Researching the issue and determining what checks need to be
implemented.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
N/A
---
src/Bot/PlayerbotMgr.cpp | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp
index 68eb2fd1e..9d3d61ca3 100644
--- a/src/Bot/PlayerbotMgr.cpp
+++ b/src/Bot/PlayerbotMgr.cpp
@@ -1542,6 +1542,24 @@ void PlayerbotMgr::HandleMasterIncomingPacket(WorldPacket const& packet)
// if master is logging out, log out all bots
case CMSG_LOGOUT_REQUEST:
{
+ Player* master = GetMaster();
+ if (master)
+ {
+ // Replicate the AFK logout prevention checks from WorldSession::HandleLogoutRequestOpcode
+ // so bots are not logged out when the master's own logout is going to be prevented.
+ AreaTableEntry const* areaEntry = sAreaTableStore.LookupEntry(master->GetAreaId());
+ bool preventAfkSanctuaryLogout = sWorld->getIntConfig(CONFIG_AFK_PREVENT_LOGOUT) == 1
+ && master->isAFK() && areaEntry && areaEntry->IsSanctuary();
+
+ bool preventAfkLogout = sWorld->getIntConfig(CONFIG_AFK_PREVENT_LOGOUT) == 2
+ && master->isAFK();
+
+ if (preventAfkSanctuaryLogout || preventAfkLogout)
+ {
+ break;
+ }
+ }
+
LogoutAllBots();
break;
}
From 826887133d01405e1021b731f19d213bdabc0772 Mon Sep 17 00:00:00 2001
From: Crow
Date: Sat, 9 May 2026 00:40:35 -0500
Subject: [PATCH 13/19] Exclude Invalid Weapons from Shaman Enchants & Refactor
Temporary Enchant Spellcasting (#2345)
## Pull Request Description
Fix for issue #2343
I excluded the MISC and FISHING POLE weapon subclasses from weapon
enchants. MISC includes the entry profession "weapons" (skinning knife,
mining pick, blacksmithing hammer, arclight spanner) and some other crap
that I suspect is not enchantable, but even if it is there's no good
reason to do so (like Brewfest steins). The subclass doesn't include
weapons that can be used for professions but you might actually want to
use for fighting (like Finkle's Skinner).
To clean things up overall, I removed the intermediate class
CastEnchantItemAction between CastSpellAction and
CastEnchantItemMainHandAction and CastEnchantItemOffHandAction.
CastEnchantItemAction is not doing anything helpful that can't easily be
replicated in the MH/OH classes, and I can't think of any future reason
for keeping CastEnchantItemAction. I also brought the CanCastSpell check
into the MH/OH classes--previously it just wasn't run for the weapon
enchant spells, and I can't think of any good reason why it shouldn't
be.
I also added Execute functions to both CastEnchantItemMainHandAction and
CastEnchantItemOffHandAction so they actually directly cast the enchant
on the specified hand instead of running through CastSpellAction's
Execute (and thus going through item for spell). I wasn't having
problems with the wrong hand being applied under the prior approach, but
this is a more direct and better approach anyway.
Other changes are just formatting.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
The new path is very similar to the old one but just adds a check that
is common to all spells and early returns to avoid invalid results.
## How to Test the Changes
1. Log into a Shaman and activate selfbot
2. Check to make sure the correct enchantments are applied (e.g., MH
Windfury and OH Flametongue for a dual-wielding Enhancement Shaman)
3. Equip a profession weapon such as a skinning knife and make sure the
Shaman does not attempt to enchant it
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
This just stops Shamans from trying to enchant stuff that they can't.
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
I kicked around some ideas with GPT-5.4 with respect to the refactoring
aspect of the PR after I had fixed the bug.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/Base/Actions/GenericSpellActions.cpp | 83 ++++++++++-----------
src/Ai/Base/Actions/GenericSpellActions.h | 17 ++---
2 files changed, 46 insertions(+), 54 deletions(-)
diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp
index c81aca214..587862a29 100644
--- a/src/Ai/Base/Actions/GenericSpellActions.cpp
+++ b/src/Ai/Base/Actions/GenericSpellActions.cpp
@@ -17,7 +17,7 @@
#include "WorldPacket.h"
#include "Group.h"
#include "Chat.h"
-#include "Ai/Base/Util/GenericBuffUtils.h"
+#include "GenericBuffUtils.h"
#include "PlayerbotAI.h"
using ai::buff::MakeAuraQualifierForBuff;
@@ -134,7 +134,8 @@ bool CastSpellAction::isPossible()
return botAI->CanCastSpell(spell, GetTarget());
}
-CastMeleeSpellAction::CastMeleeSpellAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell)
+CastMeleeSpellAction::CastMeleeSpellAction(
+ PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell)
{
range = ATTACK_DISTANCE;
}
@@ -182,56 +183,47 @@ bool CastAuraSpellAction::isUseful()
return false;
}
-CastEnchantItemAction::CastEnchantItemAction(PlayerbotAI* botAI, std::string const spell)
- : CastSpellAction(botAI, spell)
+CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(
+ PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
+
+bool CastEnchantItemMainHandAction::Execute(Event /*event*/)
{
- range = botAI->GetRange("spell");
+ Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);
+ return item && botAI->CastSpell(spell, bot, item);
}
-bool CastEnchantItemAction::isPossible()
-{
- // if (!CastSpellAction::isPossible())
- // {
- // botAI->TellMasterNoFacing("Impossible: " + spell);
- // return false;
- // }
-
- uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
-
- // bool ok = AI_VALUE2(Item*, "item for spell", spellId);
- // Item* item = AI_VALUE2(Item*, "item for spell", spellId);
- // botAI->TellMasterNoFacing("spell: " + spell + ", spell id: " + std::to_string(spellId) + " item for spell: " +
- // std::to_string(ok));
- return spellId && AI_VALUE2(Item*, "item for spell", spellId);
-}
-
-CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell)
- : CastEnchantItemAction(botAI, spell) {}
-
bool CastEnchantItemMainHandAction::isPossible()
{
- if (!CastEnchantItemAction::isPossible())
- return false;
-
Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);
- return item && !item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT) &&
- item->GetTemplate()->Class == ITEM_CLASS_WEAPON;
+ if (!item || item->GetTemplate()->SubClass == ITEM_SUBCLASS_WEAPON_MISC ||
+ item->GetTemplate()->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE ||
+ item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT))
+ {
+ return false;
+ }
+
+ return botAI->CanCastSpell(spell, bot, item);
}
-CastEnchantItemOffHandAction::CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell)
- : CastEnchantItemAction(botAI, spell) {}
+CastEnchantItemOffHandAction::CastEnchantItemOffHandAction(
+ PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
+
+bool CastEnchantItemOffHandAction::Execute(Event /*event*/)
+{
+ Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND);
+ return item && botAI->CastSpell(spell, bot, item);
+}
bool CastEnchantItemOffHandAction::isPossible()
{
- if (!CastEnchantItemAction::isPossible())
- return false;
-
Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND);
- if (!item || item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT))
+ if (!item || item->GetTemplate()->SubClass == ITEM_SUBCLASS_WEAPON_MISC ||
+ item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT))
+ {
return false;
+ }
- uint32 invType = item->GetTemplate()->InventoryType;
- return invType == INVTYPE_WEAPON || invType == INVTYPE_WEAPONOFFHAND;
+ return botAI->CanCastSpell(spell, bot, item);
}
CastHealingSpellAction::CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount,
@@ -245,7 +237,8 @@ bool CastHealingSpellAction::isUseful() { return CastAuraSpellAction::isUseful()
bool CastAoeHealSpellAction::isUseful() { return CastSpellAction::isUseful(); }
-CastCureSpellAction::CastCureSpellAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell)
+CastCureSpellAction::CastCureSpellAction(
+ PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell)
{
range = botAI->GetRange("heal");
}
@@ -267,13 +260,15 @@ bool BuffOnPartyAction::Execute(Event /*event*/)
std::string castName = spell; // default = mono
auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot);
- castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP);
+ castName = ai::buff::UpgradeToGroupIfAppropriate(
+ bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP);
return botAI->CastSpell(castName, GetTarget());
}
// End greater buff fix
-CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
+CastShootAction::CastShootAction(
+ PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
{
if (Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED))
{
@@ -327,7 +322,8 @@ Value* CastDebuffSpellOnMeleeAttackerAction::GetTargetValue()
return context->GetValue("melee attacker without aura", spell);
}
-CastBuffSpellAction::CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner, uint32 beforeDuration)
+CastBuffSpellAction::CastBuffSpellAction(
+ PlayerbotAI* botAI, std::string const spell, bool checkIsOwner, uint32 beforeDuration)
: CastAuraSpellAction(botAI, spell, checkIsOwner, false, beforeDuration)
{
range = botAI->GetRange("spell");
@@ -448,7 +444,8 @@ bool UseTrinketAction::UseTrinket(Item* item)
uint32 spellId = 0;
for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
{
- if (item->GetTemplate()->Spells[i].SpellId > 0 && item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE)
+ if (item->GetTemplate()->Spells[i].SpellId > 0 &&
+ item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE)
{
spellId = item->GetTemplate()->Spells[i].SpellId;
const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId);
diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h
index e9dacb7d2..b87bd0a1f 100644
--- a/src/Ai/Base/Actions/GenericSpellActions.h
+++ b/src/Ai/Base/Actions/GenericSpellActions.h
@@ -121,26 +121,21 @@ public:
std::string const GetTargetName() override { return "self target"; }
};
-class CastEnchantItemAction : public CastSpellAction
-{
-public:
- CastEnchantItemAction(PlayerbotAI* botAI, std::string const spell);
-
- bool isPossible() override;
- std::string const GetTargetName() override { return "self target"; }
-};
-
-class CastEnchantItemMainHandAction : public CastEnchantItemAction
+class CastEnchantItemMainHandAction : public CastSpellAction
{
public:
CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell);
+ std::string const GetTargetName() override { return "self target"; }
+ bool Execute(Event event) override;
bool isPossible() override;
};
-class CastEnchantItemOffHandAction : public CastEnchantItemAction
+class CastEnchantItemOffHandAction : public CastSpellAction
{
public:
CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell);
+ std::string const GetTargetName() override { return "self target"; }
+ bool Execute(Event event) override;
bool isPossible() override;
};
From b8ff5996f802de76608cb1ccfd09256b0595d5a6 Mon Sep 17 00:00:00 2001
From: Keleborn <22352763+Celandriel@users.noreply.github.com>
Date: Fri, 8 May 2026 22:41:13 -0700
Subject: [PATCH 14/19] Flying mount fixes and self-bot (#2351)
## Pull Request Description
This PR does a few things.
1. Enable Selfbots to mount up. Because they have masters, but are their
own masters, they would never mount up because their master never
mounted.
2. Fix flag state handling after processing the aura change.
3. Add in the Dismount packet handler. This is intended to implement
fall animations and have bots touch the ground when dismounting instead
of floating off the ground. (It was cleared anyway after the first move,
but this should make it more seamless.)
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
self bot should mount up, and select area appropriate mounts.
Bots in your team should mount up, and on your dismount properly snap to
the ground.
should test at low Z (<1.0 off the ground) and higher z (> 1.0 off the
ground)
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [ ] No, not at all
- - [x] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
The processing of a fall path has some impact, but I dont think itll be
too much.
- Does this change modify default bot behavior?
- - [ ] No
- - [x] Yes (**explain why**)
Add natural falling when dismounting. May incurr fall damange....
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
Comparison of code bases, searching for flags, adding diagnostic
logging, and processing of said logging.
iterating and brainstorming.
Code was also written, but fully reviewed by me, and fixed where
appropriate.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---------
Co-authored-by: Claude Opus 4.7 (1M context)
---
src/Ai/Base/Actions/CheckMountStateAction.cpp | 94 +++++++++++++++----
src/Ai/Base/Actions/CheckMountStateAction.h | 2 +
src/Bot/PlayerbotAI.cpp | 12 +++
3 files changed, 90 insertions(+), 18 deletions(-)
diff --git a/src/Ai/Base/Actions/CheckMountStateAction.cpp b/src/Ai/Base/Actions/CheckMountStateAction.cpp
index 0d7fe4321..0e3abb724 100644
--- a/src/Ai/Base/Actions/CheckMountStateAction.cpp
+++ b/src/Ai/Base/Actions/CheckMountStateAction.cpp
@@ -4,9 +4,11 @@
*/
#include "CheckMountStateAction.h"
+#include "AreaDefines.h"
#include "BattleGroundTactics.h"
#include "BattlegroundEY.h"
#include "BattlegroundWS.h"
+#include "DBCStores.h"
#include "Event.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
@@ -14,6 +16,8 @@
#include "ServerFacade.h"
#include "SpellAuraEffects.h"
+static constexpr uint32 SPELL_COLD_WEATHER_FLYING = 54197;
+
// Define the static map / init bool for caching bot preferred mount data globally
std::unordered_map CheckMountStateAction::mountCache;
bool CheckMountStateAction::preferredMountTableChecked = false;
@@ -94,9 +98,10 @@ bool CheckMountStateAction::Execute(Event /*event*/)
}
bool inBattleground = bot->InBattleground();
+ bool const noRealMaster = (!master || master == bot);
// If there is a master and bot not in BG, follow master's mount state regardless of group leader
- if (master && !inBattleground)
+ if (!noRealMaster && !inBattleground)
{
if (ShouldFollowMasterMountState(master, noAttackers, shouldMount))
return Mount();
@@ -110,8 +115,8 @@ bool CheckMountStateAction::Execute(Event /*event*/)
return false;
}
- // If there is no master or bot in BG
- if ((!master || inBattleground) && !bot->IsMounted() &&
+ // No real master (random bot or self-bot) OR bot in BG
+ if ((noRealMaster || inBattleground) && !bot->IsMounted() &&
noAttackers && shouldMount && !bot->IsInCombat())
return Mount();
@@ -228,6 +233,39 @@ void CheckMountStateAction::Dismount()
WorldPacket emptyPacket;
bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket);
+
+ bool const wantsFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura();
+ bool const isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
+ bool const isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING);
+ bool const hasGravityDisabled = bot->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
+ if (!wantsFly && !isWaterWalking && (isFlying || hasGravityDisabled))
+ {
+ bot->RemoveUnitMovementFlag(
+ MOVEMENTFLAG_FLYING | MOVEMENTFLAG_CAN_FLY | MOVEMENTFLAG_DISABLE_GRAVITY);
+ if (!bot->IsRooted())
+ bot->SendMovementFlagUpdate();
+ }
+}
+
+void CheckMountStateAction::CompleteDismount(Player* bot)
+{
+ if (!bot || !bot->IsInWorld())
+ return;
+
+ float const x = bot->GetPositionX();
+ float const y = bot->GetPositionY();
+ float const startZ = bot->GetPositionZ();
+
+ float groundZ = startZ;
+ bot->UpdateAllowedPositionZ(x, y, groundZ);
+
+ bot->GetMotionMaster()->MoveFall();
+ MovementInfo fallInfo = bot->m_movementInfo;
+ // Need to set the start of the fall, otherwise the fall may start from too high of a Z and kill the bot.
+ bot->SetFallInformation(0, startZ);
+ fallInfo.pos.Relocate(x, y, groundZ);
+ bot->HandleFall(fallInfo);
+ bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR);
}
bool CheckMountStateAction::TryForms(Player* master, int32 masterMountType, int32 masterSpeed) const
@@ -434,6 +472,24 @@ bool CheckMountStateAction::ShouldDismountForMaster(Player* master) const
return !isMasterMounted && bot->IsMounted();
}
+static bool BotCanUseFlyingMount(Player const* bot)
+{
+ if (bot->GetPureSkillValue(SKILL_RIDING) < 225)
+ return false;
+
+ AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetAreaId());
+ if (!area || !area->IsFlyable())
+ return false;
+ if (area->flags & AREA_FLAG_NO_FLY_ZONE)
+ return false;
+
+ uint32 const vmap = GetVirtualMapForMapAndZone(bot->GetMapId(), bot->GetZoneId());
+ if (vmap == MAP_NORTHREND && !bot->HasSpell(SPELL_COLD_WEATHER_FLYING))
+ return false;
+
+ return true;
+}
+
int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const MountData& mountData) const
{
// Check riding skill and level requirements
@@ -443,8 +499,10 @@ int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const Mou
if (ridingSkill <= 75 && botLevel < static_cast(sPlayerbotAIConfig.useFastGroundMountAtMinLevel))
return 59;
- // If there is a master and bot not in BG, use master's aura effects.
- if (master && !bot->InBattleground())
+ // check if bot has master and if master is self
+ bool const noRealMaster = (!master || master == bot);
+
+ if (!noRealMaster && !bot->InBattleground())
{
auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED);
if (!auraEffects.empty())
@@ -458,27 +516,27 @@ int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const Mou
return 279;
else if (masterInShapeshiftForm == FORM_FLIGHT)
return 149;
- }
- else
- {
- // Bots on their own.
- int32 speed = mountData.maxSpeed;
- if (bot->InBattleground() && speed > 99)
- return 99;
-
- return speed;
+ return 59; // walk pace
}
- return 59;
+ // No real master OR battleground: pick speed by skill tier.
+ if (!bot->InBattleground() && BotCanUseFlyingMount(bot))
+ return (ridingSkill >= 300) ? 279 : 149;
+
+ int32 maxGround = (ridingSkill >= 150) ? 99 : 59;
+ if (bot->InBattleground() && maxGround > 99)
+ maxGround = 99;
+ return maxGround;
}
uint32 CheckMountStateAction::GetMountType(Player* master) const
{
- if (!master)
- return 0;
+ bool const noRealMaster = (!master || master == bot);
+
+ if (noRealMaster)
+ return (!bot->InBattleground() && BotCanUseFlyingMount(bot)) ? 1 : 0;
auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED);
-
if (!auraEffects.empty())
{
SpellInfo const* masterSpell = auraEffects.front()->GetSpellInfo();
diff --git a/src/Ai/Base/Actions/CheckMountStateAction.h b/src/Ai/Base/Actions/CheckMountStateAction.h
index 9a21838e1..d1faa3798 100644
--- a/src/Ai/Base/Actions/CheckMountStateAction.h
+++ b/src/Ai/Base/Actions/CheckMountStateAction.h
@@ -42,6 +42,8 @@ public:
bool isPossible() override { return true; }
bool Mount();
+ static void CompleteDismount(Player* bot);
+
private:
Player* master;
ShapeshiftForm masterInShapeshiftForm;
diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp
index b2631f7d1..8b20b2e88 100644
--- a/src/Bot/PlayerbotAI.cpp
+++ b/src/Bot/PlayerbotAI.cpp
@@ -15,6 +15,7 @@
#include "ChannelMgr.h"
#include "CharacterPackets.h"
#include "ChatHelper.h"
+#include "CheckMountStateAction.h"
#include "Common.h"
#include "CreatureData.h"
#include "EmoteAction.h"
@@ -1365,6 +1366,17 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
// */
return;
}
+ case SMSG_DISMOUNT:
+ {
+ WorldPacket p(packet);
+ p.rpos(0);
+ ObjectGuid guid;
+ p >> guid.ReadAsPacked();
+ if (guid != bot->GetGUID())
+ return;
+ CheckMountStateAction::CompleteDismount(bot);
+ return;
+ }
default:
botOutgoingPacketHandlers.AddPacket(packet);
}
From 6b0df4ff6c1c4d3d1646d22ed37e770dc261c6c5 Mon Sep 17 00:00:00 2001
From: Crow
Date: Sat, 9 May 2026 00:41:54 -0500
Subject: [PATCH 15/19] Fix ambiguous item parsing in bot text (#2356)
## Pull Request Description
This change fixes two cases of broken bot text with respect to inventory
items.
1. Reserved inventory qualifiers such as "mount," "food," "drink," etc.
no longer also trigger generic item name matching. I first noticed this
problem when my resto Shaman who had the "Mounting Vengeance" weapon in
her inventory would repeatedly give error messages of failing to use it
while mounting (because mounting also causes bots to use items that fit
the reserved "mount," which due to this bug, also caused bots to try to
use any item with "mount" in its name).
2. Custom cast output text no longer reports an inferred bag item as the
spell target for normal unit-targeted casts such as "cast chain heal on
Keleborn." There was a bug where the action would first parse the actual
target and then parse the spell text and then try to match the last word
of the string to a bag item (so the bot would say it was casting chain
heal on a healing potion, even though the heal was in fact cast
correctly on a player).
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Add a reserved-qualifier check in InventoryAction::parseItems() so
reserved selectors do not also run through FindNamedItemVisitor.
- In custom cast output text, choose the displayed target based on the
actual target type already resolved for the cast.
- It does not change mount selection behavior itself.
- It does not add new spell-target parsing rules.
- Describe the **processing cost** when this logic executes across many
bots.
- None.
## How to Test the Changes
Reserved qualifiers:
1. Give a bot in your party the "Mounting Vengeance" weapon.
2. Mount up, and the bot should mount too without saying anything
(before the fix, the bot would say it is using the weapon and that the
item was not found).
Spell cast text:
1. Give a bot an inventory item whose name overlaps with part of a spell
name, such as a healing potion.
2. Command a bot to cast some heal on a player.
3. The bot should cast the spell on the intended player (as was the case
previously), and the status text names the player instead of the
inventory item.
## 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**)
- Does this change modify default bot behavior?
- [x] No
- [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- [ ] No
- [x] Yes (**explain below**)
Very minor changes. InventoryAction gets one explicit reserved-qualifier
guard. Custom cast text selection becomes more explicit about which
target type should be displayed.
## AI Assistance
Was AI assistance used while working on this change?
- [ ] No
- [x] Yes (**explain below**)
GPT-5.4 was used to trace the relevant code paths for the errors and
propose the changes.
## Final Checklist
- [x] Stability is not compromised.
- [x] Performance impact is understood, tested, and acceptable.
- [x] Added logic complexity is justified and explained.
- [x] Any new bot dialogue lines are translated.
- [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---------
Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash
Co-authored-by: Revision
Co-authored-by: kadeshar
---
src/Ai/Base/Actions/CastCustomSpellAction.cpp | 11 +++---
src/Ai/Base/Actions/InventoryAction.cpp | 34 +++++++++++++++++--
2 files changed, 38 insertions(+), 7 deletions(-)
diff --git a/src/Ai/Base/Actions/CastCustomSpellAction.cpp b/src/Ai/Base/Actions/CastCustomSpellAction.cpp
index 15c35ee43..3defe55d1 100644
--- a/src/Ai/Base/Actions/CastCustomSpellAction.cpp
+++ b/src/Ai/Base/Actions/CastCustomSpellAction.cpp
@@ -143,14 +143,17 @@ bool CastCustomSpellAction::Execute(Event event)
std::ostringstream spellName;
spellName << ChatHelper::FormatSpell(spellInfo) << " on ";
+ bool const hasItemTarget = itemTarget &&
+ (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM);
+
if (bot->GetTrader())
spellName << "trade item";
- else if (itemTarget)
+ else if (hasItemTarget)
spellName << chat->FormatItem(itemTarget->GetTemplate());
- else if (target == bot)
- spellName << "self";
- else
+ else if (target != bot)
spellName << target->GetName();
+ else
+ spellName << "self";
if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget))
{
diff --git a/src/Ai/Base/Actions/InventoryAction.cpp b/src/Ai/Base/Actions/InventoryAction.cpp
index 83fc00f12..8d3046061 100644
--- a/src/Ai/Base/Actions/InventoryAction.cpp
+++ b/src/Ai/Base/Actions/InventoryAction.cpp
@@ -10,6 +10,31 @@
#include "ItemVisitors.h"
#include "Playerbots.h"
+namespace
+{
+bool isReservedQualifier(std::string const& text)
+{
+ static std::array const exactQualifiers = {
+ "ammo",
+ "conjured drink",
+ "conjured food",
+ "conjured water",
+ "drink",
+ "food",
+ "healing potion",
+ "mount",
+ "mana potion",
+ "pet",
+ "quest",
+ "recipe",
+ "water"
+ };
+
+ return std::find(exactQualifiers.begin(), exactQualifiers.end(), text) != exactQualifiers.end() ||
+ text.rfind("usage ", 0) == 0;
+}
+}
+
void InventoryAction::IterateItems(IterateItemsVisitor* visitor, IterateItemsMask mask)
{
if (mask & ITERATE_ITEMS_IN_BAGS)
@@ -292,9 +317,12 @@ std::vector- InventoryAction::parseItems(std::string const text, IterateIt
found.insert(visitor.GetResult().begin(), visitor.GetResult().end());
}
- FindNamedItemVisitor visitor(bot, text);
- IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS);
- found.insert(visitor.GetResult().begin(), visitor.GetResult().end());
+ if (!isReservedQualifier(text))
+ {
+ FindNamedItemVisitor visitor(bot, text);
+ IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS);
+ found.insert(visitor.GetResult().begin(), visitor.GetResult().end());
+ }
uint32 quality = chat->parseItemQuality(text);
if (quality != MAX_ITEM_QUALITY)
From d2e54431099fde5200c9c51179bcc7b5fb742108 Mon Sep 17 00:00:00 2001
From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com>
Date: Sat, 9 May 2026 07:42:07 +0200
Subject: [PATCH 16/19] =?UTF-8?q?Fix=20contradictory=20leader=20bot=20chec?=
=?UTF-8?q?k=20in=20`LeaveLargeGuildTrigger::IsActi=E2=80=A6=20(#2361)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Pull Request Description
The previous logic contained two contradictory guards back-to-back:
```
// First check: passes only if IsRealPlayer() == true
if (!leader || !GET_PLAYERBOT_AI(leader) || !GET_PLAYERBOT_AI(leader)->IsRealPlayer())
return false;
// Second check: returns false if IsRealPlayer() == true
PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(leader);
if (!leaderBotAI || leaderBotAI->IsRealPlayer())
return false;
```
The first guard (due to the erroneous `!` before `IsRealPlayer()`) only
passes when the leader **is** a real player. The second guard then
immediately returns `false` for the same reason, making the function
incapable of ever returning `true`.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Ai/Base/Trigger/GuildTriggers.cpp | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/Ai/Base/Trigger/GuildTriggers.cpp b/src/Ai/Base/Trigger/GuildTriggers.cpp
index afa405581..cae955e86 100644
--- a/src/Ai/Base/Trigger/GuildTriggers.cpp
+++ b/src/Ai/Base/Trigger/GuildTriggers.cpp
@@ -39,11 +39,8 @@ bool LeaveLargeGuildTrigger::IsActive()
Player* leader = ObjectAccessor::FindPlayer(guild->GetLeaderGUID());
- // Only leave the guild if we know the leader is not a real player.
- if (!leader || !GET_PLAYERBOT_AI(leader) || !GET_PLAYERBOT_AI(leader)->IsRealPlayer())
- return false;
-
- PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(leader);
+ // Only leave the guild if the leader is an online bot (not a real player).
+ PlayerbotAI* leaderBotAI = leader ? GET_PLAYERBOT_AI(leader) : nullptr;
if (!leaderBotAI || leaderBotAI->IsRealPlayer())
return false;
From 66d41e1d79868500200b067e8cd31c41c33bf2be Mon Sep 17 00:00:00 2001
From: NoxMax <50133316+NoxMax@users.noreply.github.com>
Date: Fri, 8 May 2026 23:42:18 -0600
Subject: [PATCH 17/19] Feat: Selective reset to default of combat or
non-combat strategies (#2365)
## Pull Request Description
Adds the commands `co !` and `nc !`, which would reset either the combat
or non-combat strategies of a follower bot, without affecting the other
strategies or any other values.
Also ChangeStrategyAction.cpp was refactored for duplicate code by
introducing the helper function `HandleStrategyCommon`, that gets called
by `ChangeCombatStrategyAction` and `ChangeNonCombatStrategyAction`
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
`reset botAI` already resets strategies back to default, but it resets
ALL strategies and wipes values such as formations, stances, and
everything else under the `value` key in playerbots_db_store>value. The
new commands don't run across many bots, only on the bot the command is
run on.
## How to Test the Changes
1. Run either `co ?` and `nc ?` to see current list of combat and
non-combat strategies the bot has.
2. Add and remove strategies to both `co` and `nc`.
3. Confirm your changes with `co ?` and `nc ?`.
4. Run `co !` only.
5. Run `co ?` to confirm combat strategies have been reset to default,
and `nc ?` to confirm it has not been affected. Then run `nc !` to reset
it as well.
6. Do another test [inside an
instance](https://github.com/mod-playerbots/mod-playerbots/wiki/Playerbot-Commands#raid-specific-strategies).
Remove a bunch of `nc` strategies, including the strategy for the raid
itself.
7. Run `nc !` and check that the defaults have been reset, but that the
instance strategy has been re-added as well.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [ ] No
- - [x] Yes (**explain below**)
Technically it adds a new case to ChangeCombatStrategyAction, but it's
straightforward.
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
Review only in case I was missing something, and then to easily refactor
duplicate code with HandleStrategyCommon.
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
[Commands
wiki](https://github.com/mod-playerbots/mod-playerbots/wiki/Playerbot-Commands#strategies)
need to be modified to read:
---
You can query the bot to report what strategies are currently being
used:
```
co ?
nc ?
```
You can reset either of the bot's strategies back to defaults:
```
co !
nc !
```
---
Tangentially I also recommend [this
section](https://github.com/mod-playerbots/mod-playerbots/wiki/Playerbot-Commands#non-combat-strategies)
to be edit to this for more accuracy:
---
General
strategy | description
:---|:---
``food`` | enable bot's ability to eat/drink
``pvp`` | enable bot's ability to engage in PVP combat. Note: PVP mode
wouldn't appear active until the bot starts combat
``loot`` | enable bot's ability to loot. Note: adding or removing that
strategy for randombots requires GM level
---
src/Ai/Base/Actions/ChangeStrategyAction.cpp | 60 +++++++++-----------
src/Bot/PlayerbotAI.cpp | 21 +++++++
src/Bot/PlayerbotAI.h | 1 +
3 files changed, 48 insertions(+), 34 deletions(-)
diff --git a/src/Ai/Base/Actions/ChangeStrategyAction.cpp b/src/Ai/Base/Actions/ChangeStrategyAction.cpp
index b4fe5faaf..e81096b2a 100644
--- a/src/Ai/Base/Actions/ChangeStrategyAction.cpp
+++ b/src/Ai/Base/Actions/ChangeStrategyAction.cpp
@@ -9,28 +9,36 @@
#include "PlayerbotRepository.h"
#include "Playerbots.h"
+// Helper function for prefixes used by combat and non-combat strategy commands.
+static void HandleStrategyCommon(PlayerbotAI* botAI, std::string const& text, BotState state)
+{
+ std::vector splitted = split(text, ',');
+ for (std::vector::iterator i = splitted.begin(); i != splitted.end(); i++)
+ {
+ const char* name = i->c_str();
+ switch (name[0])
+ {
+ case '+':
+ case '-':
+ case '~':
+ PlayerbotRepository::instance().Save(botAI);
+ break;
+ case '!':
+ botAI->SelectiveResetStrategies(state);
+ PlayerbotRepository::instance().Save(botAI);
+ break;
+ case '?':
+ break;
+ }
+ }
+}
+
bool ChangeCombatStrategyAction::Execute(Event event)
{
std::string const text = event.getParam();
botAI->ChangeStrategy(text.empty() ? getName() : text, BOT_STATE_COMBAT);
if (event.GetSource() == "co")
- {
- std::vector splitted = split(text, ',');
- for (std::vector::iterator i = splitted.begin(); i != splitted.end(); i++)
- {
- const char* name = i->c_str();
- switch (name[0])
- {
- case '+':
- case '-':
- case '~':
- PlayerbotRepository::instance().Save(botAI);
- break;
- case '?':
- break;
- }
- }
- }
+ HandleStrategyCommon(botAI, text, BOT_STATE_COMBAT);
return true;
}
@@ -52,23 +60,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event)
botAI->ChangeStrategy(text, BOT_STATE_NON_COMBAT);
if (event.GetSource() == "nc")
- {
- std::vector splitted = split(text, ',');
- for (std::vector::iterator i = splitted.begin(); i != splitted.end(); i++)
- {
- const char* name = i->c_str();
- switch (name[0])
- {
- case '+':
- case '-':
- case '~':
- PlayerbotRepository::instance().Save(botAI);
- break;
- case '?':
- break;
- }
- }
- }
+ HandleStrategyCommon(botAI, text, BOT_STATE_NON_COMBAT);
return true;
}
diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp
index 8b20b2e88..5937f0ef6 100644
--- a/src/Bot/PlayerbotAI.cpp
+++ b/src/Bot/PlayerbotAI.cpp
@@ -1585,6 +1585,27 @@ void PlayerbotAI::ClearStrategies(BotState type)
e->removeAllStrategies();
}
+// Resets only the combat or non-combat engine: wipe strategies, repopulate with class/spec defaults,
+// re-apply current map's instance strategy (if any), and call Init() to rebuild trigger/action lists.
+void PlayerbotAI::SelectiveResetStrategies(BotState type)
+{
+ Engine* e = engines[type];
+ if (!e)
+ return;
+
+ e->removeAllStrategies();
+
+ if (type == BOT_STATE_COMBAT)
+ AiFactory::AddDefaultCombatStrategies(bot, this, e);
+ else if (type == BOT_STATE_NON_COMBAT)
+ AiFactory::AddDefaultNonCombatStrategies(bot, this, e);
+
+ if (sPlayerbotAIConfig.applyInstanceStrategies)
+ ApplyInstanceStrategies(bot->GetMapId());
+
+ e->Init();
+}
+
std::vector PlayerbotAI::GetStrategies(BotState type)
{
Engine* e = engines[type];
diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h
index dc7770ed8..01924c46f 100644
--- a/src/Bot/PlayerbotAI.h
+++ b/src/Bot/PlayerbotAI.h
@@ -404,6 +404,7 @@ public:
std::string const qualifier = "");
void ChangeStrategy(std::string const name, BotState type);
void ClearStrategies(BotState type);
+ void SelectiveResetStrategies(BotState type);
std::vector GetStrategies(BotState type);
Strategy* GetStrategy(std::string const name, BotState type);
void ApplyInstanceStrategies(uint32 mapId, bool tellMaster = false);
From 8cb847db5d3043e59a7a3e4571068ffd48599f9c Mon Sep 17 00:00:00 2001
From: Keleborn <22352763+Celandriel@users.noreply.github.com>
Date: Sat, 9 May 2026 00:48:59 -0700
Subject: [PATCH 18/19] Fix location cache. (#2374)
## Pull Request Description
Bot locations were not correctly registered, so they werent picking it
as often as they should.
In part related to #2369
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [ ] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [ ] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [ ] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [ ] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [ ] Stability is not compromised.
- - [ ] Performance impact is understood, tested, and acceptable.
- - [ ] Added logic complexity is justified and explained.
- - [ ] Any new bot dialogue lines are translated.
- - [ ] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Mgr/Travel/TravelMgr.cpp | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp
index adc1e4a3e..24ebf3ee1 100644
--- a/src/Mgr/Travel/TravelMgr.cpp
+++ b/src/Mgr/Travel/TravelMgr.cpp
@@ -4821,7 +4821,10 @@ void TravelMgr::PrepareDestinationCache()
if (l < 1 || l > maxLevel)
continue;
- locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple)));
+ locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple),
+ static_cast(std::get<1>(gridTuple)) * 50.0f,
+ static_cast(std::get<2>(gridTuple)) * 50.0f,
+ static_cast(std::get<3>(gridTuple)) * 50.0f));
}
}
}
From 9118c9671a99a6f5ad12cb5cd043628cda3aa50f Mon Sep 17 00:00:00 2001
From: Keleborn <22352763+Celandriel@users.noreply.github.com>
Date: Sat, 9 May 2026 10:14:05 -0700
Subject: [PATCH 19/19] Fix/travelValType (#2376)
## Pull Request Description
Incorrect var types when I refactored away from SQL lookup.
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.
## How to Test the Changes
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- - [x] No, not at all
- - [ ] Minimal impact (**explain below**)
- - [ ] Moderate impact (**explain below**)
- Does this change modify default bot behavior?
- - [x] No
- - [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- - [x] No
- - [ ] Yes (**explain below**)
## AI Assistance
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
## Final Checklist
- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---
src/Mgr/Travel/TravelMgr.cpp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp
index 24ebf3ee1..7192c2c26 100644
--- a/src/Mgr/Travel/TravelMgr.cpp
+++ b/src/Mgr/Travel/TravelMgr.cpp
@@ -4687,9 +4687,9 @@ void TravelMgr::PrepareDestinationCache()
(creatureTemplate->unit_flags & 4096) == 0 &&
creatureTemplate->rank == 0)
{
- uint32 roundX = static_cast(std::round(x / 50.0f));
- uint32 roundY = static_cast(std::round(y / 50.0f));
- uint32 roundZ = static_cast(std::round(z / 50.0f));
+ int32 roundX = static_cast