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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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
{