diff --git a/README.md b/README.md
index 8c857063..d5c1f182 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
English
|
中文
+ |
+ Español
@@ -16,23 +18,25 @@
# Playerbots Module
-`mod-playerbots` is an [AzerothCore](https://www.azerothcore.org/) module that adds player-like bots to a server. The project is based off [IKE3's Playerbots](https://github.com/ike3/mangosbot). Features include:
+`mod-playerbots` is an [AzerothCore](https://www.azerothcore.org/) module that adds player-like bots to a server. The project is based off [IKE3's Playerbots](https://github.com/ike3/mangosbot) and requires a custom branch of AzerothCore to compile and run: [liyunfan1223/azerothcore-wotlk/tree/Playerbot](https://github.com/liyunfan1223/azerothcore-wotlk/tree/Playerbot).
-- Bots that utilize real player data, allowing players to interact with their other characters, form parties, level up, and more;
-- Random bots that wander through the world and behave like players, simulating the MMO experience;
-- Bots capable of running raids and battlegrounds;
+Features include:
+
+- The ability to log in alt characters as bots, allowing players to interact with their other characters, form parties, level up, and more;
+- Random bots that wander through the world, complete quests, and otherwise behave like players, simulating the MMO experience;
+- Bots capable of running most raids and battlegrounds;
- Highly configurable settings to define how bots behave;
- Excellent performance, even when running thousands of bots.
**This project is still under development**. If you encounter any errors or experience crashes, we kindly request that you [report them as GitHub issues](https://github.com/liyunfan1223/mod-playerbots/issues/new?template=bug_report.md). Your valuable feedback will help us improve this project collaboratively.
-**Playerbots Module** has a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can discuss the project.
+`mod-playerbots` has a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can discuss the project, ask questions, and get involved in the community!
## Installation
### Classic Installation
-`mod-playerbots` requires a custom branch of AzerothCore to work: [liyunfan1223/azerothcore-wotlk/tree/Playerbot](https://github.com/liyunfan1223/azerothcore-wotlk/tree/Playerbot). To install the module, simply run:
+As noted above, `mod-playerbots` requires a custom branch of AzerothCore: [liyunfan1223/azerothcore-wotlk/tree/Playerbot](https://github.com/liyunfan1223/azerothcore-wotlk/tree/Playerbot). To install the module, simply run:
```bash
git clone https://github.com/liyunfan1223/azerothcore-wotlk.git --branch=Playerbot
@@ -81,21 +85,21 @@ Use `docker compose up -d --build` to build and run the server. For more informa
## Documentation
-The [Playerbots Wiki](https://github.com/liyunfan1223/mod-playerbots/wiki) contains an extensive overview of addons, commands, and recommended configurations. Please note that documentation may be incomplete or out-of-date in some sections. Contributions are welcome.
+The [Playerbots Wiki](https://github.com/liyunfan1223/mod-playerbots/wiki) contains an extensive overview of addons, commands, raids with programmed bot strategies, and recommended performance configurations. Please note that documentation may be incomplete or out-of-date in some sections. Contributions are welcome.
## Frequently Asked Questions
- **Why aren't my bots casting spells?** Please make sure that the necessary English DBC file (enUS) is present.
- **What platforms are supported?** We support Ubuntu, Windows, and macOS. Other Linux distros may work, but will not receive support.
-- **Why isn't my source compiling?** Please [check the build status of our CI](https://github.com/liyunfan1223/mod-playerbots/actions). If the latest build is failing, rever to the last successful commit until we address the issue.
+- **Why isn't my source compiling?** Please ensure that you are compiling with the required [custom branch of AzerothCore](https://github.com/liyunfan1223/azerothcore-wotlk/tree/Playerbot). Additionally, please [check the build status of our CI](https://github.com/liyunfan1223/mod-playerbots/actions). If the latest build is failing, rever to the last successful commit until we address the issue.
## Addons
Typically, bots are controlled via chat commands. For larger bot groups, this can be unwieldy. As an alternative, community members have developed client Add-Ons to allow controlling bots through the in-game UI. We recommend you check out their projects:
-- [Multibot](https://github.com/Macx-Lio/MultiBot) (by Macx-Lio)
-- [Unbot Addon (zh)](https://github.com/liyunfan1223/unbot-addon) (Chinese version by Liyunfan)
-- [Unbot Addon (en)](https://github.com/noisiver/unbot-addon/tree/english) (English version translated by @Revision)
+- [Multibot](https://github.com/Macx-Lio/MultiBot) (by Macx-Lio), which includes English, Chinese, French, German, Korean, Russian, and Spanish support [note: active development is temporarily continuing on a fork in Macx-Lio's absence (https://github.com/Wishmaster117/MultiBot)]
+- [Unbot Addon (zh)](https://github.com/liyunfan1223/unbot-addon) (Chinese version by Liyunfan) [note: no longer under active development]
+- [Unbot Addon (en)](https://github.com/noisiver/unbot-addon/tree/english) (English version translated by @Revision) [note: no longer under active development]
## Acknowledgements
diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist
index 2ed55529..b35f7494 100644
--- a/conf/playerbots.conf.dist
+++ b/conf/playerbots.conf.dist
@@ -710,6 +710,16 @@ AiPlayerbot.AutoUpgradeEquip = 1
# Default: 0 (disabled)
AiPlayerbot.HunterWolfPet = 0
+# Default pet stance when a bot summons a pet
+# 0 = Passive, 1 = Defensive, 2 = Aggressive
+# Default: 1 (Defensive)
+AiPlayerbot.DefaultPetStance = 1
+
+# Enable/disable debug messages about pet commands
+# 0 = Disabled, 1 = Enabled
+# Default = 0 (disabled)
+AiPlayerbot.PetChatCommandDebug = 0
+
# Prohibit hunter bots from creating pets with any family ID listed below in ExcludedHunterPetFamilies
# See the creature_family database table for all pet families by ID (note: ID for spiders is 3)
AiPlayerbot.ExcludedHunterPetFamilies = ""
diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp
index 998310f9..09a6ef82 100644
--- a/src/PlayerbotAI.cpp
+++ b/src/PlayerbotAI.cpp
@@ -4214,6 +4214,19 @@ bool PlayerbotAI::AllowActive(ActivityType activityType)
}
}
+ // only keep updating till initializing time has completed,
+ // which prevents unneeded expensive GameTime calls.
+ if (_isBotInitializing)
+ {
+ _isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig->maxRandomBots * 0.11;
+
+ // no activity allowed during bot initialization
+ if (_isBotInitializing)
+ {
+ return false;
+ }
+ }
+
// General exceptions
if (activityType == PACKET_ACTIVITY)
{
diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h
index c7bd5e62..3fa7b5b6 100644
--- a/src/PlayerbotAI.h
+++ b/src/PlayerbotAI.h
@@ -611,6 +611,7 @@ private:
Item* FindItemInInventory(std::function checkItem) const;
void HandleCommands();
void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL);
+ bool _isBotInitializing = false;
protected:
Player* bot;
diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp
index b6ffec37..269e8d8c 100644
--- a/src/PlayerbotAIConfig.cpp
+++ b/src/PlayerbotAIConfig.cpp
@@ -578,6 +578,8 @@ bool PlayerbotAIConfig::Initialize()
autoPickTalents = sConfigMgr->GetOption("AiPlayerbot.AutoPickTalents", true);
autoUpgradeEquip = sConfigMgr->GetOption("AiPlayerbot.AutoUpgradeEquip", false);
hunterWolfPet = sConfigMgr->GetOption("AiPlayerbot.HunterWolfPet", 0);
+ defaultPetStance = sConfigMgr->GetOption("AiPlayerbot.DefaultPetStance", 1);
+ petChatCommandDebug = sConfigMgr->GetOption("AiPlayerbot.PetChatCommandDebug", 0);
autoLearnTrainerSpells = sConfigMgr->GetOption("AiPlayerbot.AutoLearnTrainerSpells", true);
autoLearnQuestSpells = sConfigMgr->GetOption("AiPlayerbot.AutoLearnQuestSpells", false);
autoTeleportForLevel = sConfigMgr->GetOption("AiPlayerbot.AutoTeleportForLevel", false);
diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h
index 63e11063..71539d9e 100644
--- a/src/PlayerbotAIConfig.h
+++ b/src/PlayerbotAIConfig.h
@@ -335,6 +335,8 @@ public:
bool autoPickTalents;
bool autoUpgradeEquip;
int32 hunterWolfPet;
+ int32 defaultPetStance;
+ int32 petChatCommandDebug;
bool autoLearnTrainerSpells;
bool autoDoQuests;
bool enableNewRpgStrategy;
diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp
index 4fabe6d9..bace622c 100644
--- a/src/Playerbots.cpp
+++ b/src/Playerbots.cpp
@@ -109,7 +109,7 @@ public:
"|cffcccccchttps://github.com/liyunfan1223/mod-playerbots|r");
}
- /*if (sPlayerbotAIConfig->enabled || sPlayerbotAIConfig->randomBotAutologin)
+ if (sPlayerbotAIConfig->enabled || sPlayerbotAIConfig->randomBotAutologin)
{
std::string roundedTime =
std::to_string(std::ceil((sPlayerbotAIConfig->maxRandomBots * 0.11 / 60) * 10) / 10.0);
@@ -118,7 +118,7 @@ public:
ChatHandler(player->GetSession()).SendSysMessage(
"|cff00ff00Playerbots:|r bot initialization at server startup takes about '"
+ roundedTime + "' minutes.");
- }*/
+ }
}
}
diff --git a/src/factory/PlayerbotFactory.cpp b/src/factory/PlayerbotFactory.cpp
index 24c130d7..e239d449 100644
--- a/src/factory/PlayerbotFactory.cpp
+++ b/src/factory/PlayerbotFactory.cpp
@@ -166,18 +166,27 @@ void PlayerbotFactory::Init()
}
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(gemId);
- if (proto) {
- if (proto->ItemLevel < 60)
- continue;
-
- if (proto->Flags & ITEM_FLAG_UNIQUE_EQUIPPABLE)
- continue;
+ if (!proto)
+ {
+ continue;
+ }
+
+ if (proto->ItemLevel < 60)
+ {
+ continue;
+ }
+
+ if (proto->Flags & ITEM_FLAG_UNIQUE_EQUIPPABLE)
+ {
+ continue;
}
if (sRandomItemMgr->IsTestItem(gemId))
- continue;
-
- if (!proto || !sGemPropertiesStore.LookupEntry(proto->GemProperties))
+ {
+ continue;
+ }
+
+ if (!sGemPropertiesStore.LookupEntry(proto->GemProperties))
{
continue;
}
@@ -185,7 +194,6 @@ void PlayerbotFactory::Init()
// LOG_INFO("playerbots", "Add {} to enchantment gems", gemId);
enchantGemIdCache.push_back(gemId);
}
-
LOG_INFO("playerbots", "Loading {} enchantment gems", enchantGemIdCache.size());
}
@@ -1020,15 +1028,18 @@ void PlayerbotFactory::ClearSkills()
}
bot->SetUInt32Value(PLAYER_SKILL_INDEX(0), 0);
bot->SetUInt32Value(PLAYER_SKILL_INDEX(1), 0);
+
// unlearn default race/class skills
- PlayerInfo const* info = sObjectMgr->GetPlayerInfo(bot->getRace(), bot->getClass());
- for (PlayerCreateInfoSkills::const_iterator itr = info->skills.begin(); itr != info->skills.end(); ++itr)
- {
- uint32 skillId = itr->SkillId;
- if (!bot->HasSkill(skillId))
- continue;
- bot->SetSkill(skillId, 0, 0, 0);
- }
+ if (PlayerInfo const* info = sObjectMgr->GetPlayerInfo(bot->getRace(), bot->getClass()))
+ {
+ for (PlayerCreateInfoSkills::const_iterator itr = info->skills.begin(); itr != info->skills.end(); ++itr)
+ {
+ uint32 skillId = itr->SkillId;
+ if (!bot->HasSkill(skillId))
+ continue;
+ bot->SetSkill(skillId, 0, 0, 0);
+ }
+ }
}
void PlayerbotFactory::ClearEverything()
diff --git a/src/strategy/NamedObjectContext.cpp b/src/strategy/NamedObjectContext.cpp
index d1ea0e1c..ae3a7079 100644
--- a/src/strategy/NamedObjectContext.cpp
+++ b/src/strategy/NamedObjectContext.cpp
@@ -14,10 +14,10 @@ void Qualified::Qualify(int qual)
qualifier = out.str();
}
-std::string const Qualified::MultiQualify(std::vector qualifiers, const std::string& separator, const std::string_view brackets)
+std::string const Qualified::MultiQualify(const std::vector& qualifiers, const std::string& separator, const std::string_view brackets)
{
std::stringstream out;
- for (uint8 i = 0; i < qualifiers.size(); i++)
+ for (uint8 i = 0; i < qualifiers.size(); ++i)
{
const std::string& qualifier = qualifiers[i];
if (i == qualifiers.size() - 1)
@@ -40,13 +40,13 @@ std::string const Qualified::MultiQualify(std::vector qualifiers, c
}
}
-std::vector Qualified::getMultiQualifiers(std::string const qualifier1)
+std::vector Qualified::getMultiQualifiers(const std::string& qualifier1)
{
std::istringstream iss(qualifier1);
return {std::istream_iterator{iss}, std::istream_iterator{}};
}
-int32 Qualified::getMultiQualifier(std::string const qualifier1, uint32 pos)
+int32 Qualified::getMultiQualifier(const std::string& qualifier1, uint32 pos)
{
return std::stoi(getMultiQualifiers(qualifier1)[pos]);
}
diff --git a/src/strategy/NamedObjectContext.h b/src/strategy/NamedObjectContext.h
index c7ecffc6..69b38ce1 100644
--- a/src/strategy/NamedObjectContext.h
+++ b/src/strategy/NamedObjectContext.h
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include "Common.h"
@@ -29,10 +30,10 @@ public:
std::string const getQualifier() { return qualifier; }
- static std::string const MultiQualify(std::vector qualifiers, const std::string& separator,
+ static std::string const MultiQualify(const std::vector& qualifiers, const std::string& separator,
const std::string_view brackets = "{}");
- static std::vector getMultiQualifiers(std::string const qualifier1);
- static int32 getMultiQualifier(std::string const qualifier1, uint32 pos);
+ static std::vector getMultiQualifiers(const std::string& qualifier1);
+ static int32 getMultiQualifier(const std::string& qualifier1, uint32 pos);
protected:
std::string qualifier;
@@ -42,11 +43,11 @@ template
class NamedObjectFactory
{
public:
- typedef T* (*ObjectCreator)(PlayerbotAI* botAI);
+ using ObjectCreator = std::function;
std::unordered_map creators;
public:
- T* create(std::string name, PlayerbotAI* botAI)
+ virtual T* create(std::string name, PlayerbotAI* botAI)
{
size_t found = name.find("::");
std::string qualifier;
@@ -59,11 +60,9 @@ public:
if (creators.find(name) == creators.end())
return nullptr;
- ObjectCreator creator = creators[name];
- if (!creator)
- return nullptr;
+ ObjectCreator& creator = creators[name];
- T* object = (*creator)(botAI);
+ T* object = creator(botAI);
Qualified* q = dynamic_cast(object);
if (q && found != std::string::npos)
q->Qualify(qualifier);
@@ -74,7 +73,7 @@ public:
std::set supports()
{
std::set keys;
- for (typename std::unordered_map::iterator it = creators.begin();
+ for (typename std::unordered_map::const_iterator it = creators.begin();
it != creators.end(); it++)
keys.insert(it->first);
@@ -93,7 +92,7 @@ public:
virtual ~NamedObjectContext() { Clear(); }
- T* create(std::string const name, PlayerbotAI* botAI)
+ virtual T* create(std::string name, PlayerbotAI* botAI) override
{
if (created.find(name) == created.end())
return created[name] = NamedObjectFactory::create(name, botAI);
@@ -103,7 +102,7 @@ public:
void Clear()
{
- for (typename std::unordered_map::iterator i = created.begin(); i != created.end(); i++)
+ for (typename std::unordered_map::const_iterator i = created.begin(); i != created.end(); i++)
{
if (i->second)
delete i->second;
@@ -134,13 +133,13 @@ template
class SharedNamedObjectContextList
{
public:
- typedef T* (*ObjectCreator)(PlayerbotAI* botAI);
+ using ObjectCreator = std::function;
std::unordered_map creators;
std::vector*> contexts;
~SharedNamedObjectContextList()
{
- for (typename std::vector*>::iterator i = contexts.begin(); i != contexts.end(); i++)
+ for (typename std::vector*>::const_iterator i = contexts.begin(); i != contexts.end(); i++)
delete *i;
}
@@ -158,7 +157,7 @@ template
class NamedObjectContextList
{
public:
- typedef T* (*ObjectCreator)(PlayerbotAI* botAI);
+ using ObjectCreator = std::function;
const std::unordered_map& creators;
const std::vector*>& contexts;
std::unordered_map created;
@@ -170,7 +169,7 @@ public:
~NamedObjectContextList()
{
- for (typename std::unordered_map::iterator i = created.begin(); i != created.end(); i++)
+ for (typename std::unordered_map::const_iterator i = created.begin(); i != created.end(); i++)
{
if (i->second)
delete i->second;
@@ -192,11 +191,9 @@ public:
if (creators.find(name) == creators.end())
return nullptr;
- ObjectCreator creator = creators.at(name);
- if (!creator)
- return nullptr;
+ const ObjectCreator& creator = creators.at(name);
- T* object = (*creator)(botAI);
+ T* object = creator(botAI);
Qualified* q = dynamic_cast(object);
if (q && found != std::string::npos)
q->Qualify(qualifier);
@@ -204,7 +201,7 @@ public:
return object;
}
- T* GetContextObject(std::string const name, PlayerbotAI* botAI)
+ T* GetContextObject(const std::string& name, PlayerbotAI* botAI)
{
if (created.find(name) == created.end())
{
@@ -214,7 +211,7 @@ public:
return created[name];
}
- std::set GetSiblings(std::string const name)
+ std::set GetSiblings(const std::string& name)
{
for (auto i = contexts.begin(); i != contexts.end(); i++)
{
@@ -240,7 +237,7 @@ public:
{
std::set supported = (*i)->supports();
- for (std::set::iterator j = supported.begin(); j != supported.end(); j++)
+ for (std::set::const_iterator j = supported.begin(); j != supported.end(); ++j)
result.insert(*j);
}
@@ -250,7 +247,7 @@ public:
std::set GetCreated()
{
std::set result;
- for (typename std::unordered_map::iterator i = created.begin(); i != created.end(); i++)
+ for (typename std::unordered_map::const_iterator i = created.begin(); i != created.end(); i++)
{
result.insert(i->first);
}
@@ -263,13 +260,13 @@ template
class NamedObjectFactoryList
{
public:
- typedef T* (*ObjectCreator)(PlayerbotAI* botAI);
+ using ObjectCreator = std::function;
std::vector*> factories;
std::unordered_map creators;
virtual ~NamedObjectFactoryList()
{
- for (typename std::vector*>::iterator i = factories.begin(); i != factories.end(); i++)
+ for (typename std::vector*>::const_iterator i = factories.begin(); i != factories.end(); i++)
delete *i;
}
@@ -286,11 +283,9 @@ public:
if (creators.find(name) == creators.end())
return nullptr;
- ObjectCreator creator = creators[name];
- if (!creator)
- return nullptr;
+ const ObjectCreator& creator = creators[name];
- T* object = (*creator)(botAI);
+ T* object = creator(botAI);
Qualified* q = dynamic_cast(object);
if (q && found != std::string::npos)
q->Qualify(qualifier);
@@ -307,7 +302,7 @@ public:
}
}
- T* GetContextObject(std::string const name, PlayerbotAI* botAI)
+ T* GetContextObject(const std::string& name, PlayerbotAI* botAI)
{
if (T* object = create(name, botAI))
return object;
diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h
index 832d799a..7d458e2b 100644
--- a/src/strategy/actions/ActionContext.h
+++ b/src/strategy/actions/ActionContext.h
@@ -246,7 +246,8 @@ public:
creators["rpg mount anim"] = &ActionContext::rpg_mount_anim;
creators["toggle pet spell"] = &ActionContext::toggle_pet_spell;
- creators["pet attack"] = &ActionContext::pet_attack;
+ creators["pet attack"] = &ActionContext::pet_attack;
+ creators["set pet stance"] = &ActionContext::set_pet_stance;
creators["new rpg status update"] = &ActionContext::new_rpg_status_update;
creators["new rpg go grind"] = &ActionContext::new_rpg_go_grind;
@@ -434,6 +435,7 @@ private:
static Action* toggle_pet_spell(PlayerbotAI* ai) { return new TogglePetSpellAutoCastAction(ai); }
static Action* pet_attack(PlayerbotAI* ai) { return new PetAttackAction(ai); }
+ static Action* set_pet_stance(PlayerbotAI* ai) { return new SetPetStanceAction(ai); }
static Action* new_rpg_status_update(PlayerbotAI* ai) { return new NewRpgStatusUpdateAction(ai); }
static Action* new_rpg_go_grind(PlayerbotAI* ai) { return new NewRpgGoGrindAction(ai); }
diff --git a/src/strategy/actions/ChatActionContext.h b/src/strategy/actions/ChatActionContext.h
index 520bc586..cf9d8c51 100644
--- a/src/strategy/actions/ChatActionContext.h
+++ b/src/strategy/actions/ChatActionContext.h
@@ -79,9 +79,10 @@
#include "OpenItemAction.h"
#include "UnlockItemAction.h"
#include "UnlockTradedItemAction.h"
-#include "PetAction.h"
+#include "TameAction.h"
#include "TellGlyphsAction.h"
#include "EquipGlyphsAction.h"
+#include "PetAction.h"
class ChatActionContext : public NamedObjectContext
{
@@ -191,9 +192,11 @@ public:
creators["lfg"] = &ChatActionContext::lfg;
creators["calc"] = &ChatActionContext::calc;
creators["wipe"] = &ChatActionContext::wipe;
- creators["pet"] = &ChatActionContext::pet;
+ creators["tame"] = &ChatActionContext::tame;
creators["glyphs"] = &ChatActionContext::glyphs; // Added for custom Glyphs
creators["glyph equip"] = &ChatActionContext::glyph_equip; // Added for custom Glyphs
+ creators["pet"] = &ChatActionContext::pet;
+ creators["pet attack"] = &ChatActionContext::pet_attack;
creators["roll"] = &ChatActionContext::roll_action;
}
@@ -301,9 +304,11 @@ private:
static Action* join(PlayerbotAI* ai) { return new JoinGroupAction(ai); }
static Action* calc(PlayerbotAI* ai) { return new TellCalculateItemAction(ai); }
static Action* wipe(PlayerbotAI* ai) { return new WipeAction(ai); }
- static Action* pet(PlayerbotAI* botAI) { return new PetAction(botAI); }
+ static Action* tame(PlayerbotAI* botAI) { return new TameAction(botAI); }
static Action* glyphs(PlayerbotAI* botAI) { return new TellGlyphsAction(botAI); } // Added for custom Glyphs
static Action* glyph_equip(PlayerbotAI* ai) { return new EquipGlyphsAction(ai); } // Added for custom Glyphs
+ static Action* pet(PlayerbotAI* botAI) { return new PetAction(botAI); }
+ static Action* pet_attack(PlayerbotAI* botAI) { return new PetAction(botAI, "attack"); }
static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); }
};
diff --git a/src/strategy/actions/FollowActions.cpp b/src/strategy/actions/FollowActions.cpp
index 5302748d..24079293 100644
--- a/src/strategy/actions/FollowActions.cpp
+++ b/src/strategy/actions/FollowActions.cpp
@@ -36,10 +36,13 @@ bool FollowAction::Execute(Event event)
true, priority, true);
}
- if (Pet* pet = bot->GetPet())
- {
- botAI->PetFollow();
- }
+ // This section has been commented out because it was forcing the pet to
+ // follow the bot on every "follow" action tick, overriding any attack or
+ // stay commands that might have been issued by the player.
+ // if (Pet* pet = bot->GetPet())
+ // {
+ // botAI->PetFollow();
+ // }
// if (moved)
// botAI->SetNextCheckDelay(sPlayerbotAIConfig->reactDelay);
diff --git a/src/strategy/actions/GenericActions.cpp b/src/strategy/actions/GenericActions.cpp
index a7c2a9cb..ae8010e1 100644
--- a/src/strategy/actions/GenericActions.cpp
+++ b/src/strategy/actions/GenericActions.cpp
@@ -4,9 +4,19 @@
*/
#include "GenericActions.h"
-
+#include "PlayerbotAI.h"
+#include "Player.h"
+#include "Pet.h"
+#include "PlayerbotAIConfig.h"
#include "CreatureAI.h"
#include "Playerbots.h"
+#include "CharmInfo.h"
+#include "SharedDefines.h"
+#include "ObjectGuid.h"
+#include "SpellMgr.h"
+#include "SpellInfo.h"
+#include
+#include
enum PetSpells
{
@@ -23,7 +33,8 @@ enum PetSpells
PET_DEVOUR_MAGIC_4 = 19736,
PET_DEVOUR_MAGIC_5 = 27276,
PET_DEVOUR_MAGIC_6 = 27277,
- PET_DEVOUR_MAGIC_7 = 48011
+ PET_DEVOUR_MAGIC_7 = 48011,
+ PET_SPIRIT_WOLF_LEAP = 58867
};
static std::vector disabledPetSpells = {
@@ -31,7 +42,7 @@ static std::vector disabledPetSpells = {
PET_COWER, PET_LEAP,
PET_SPELL_LOCK_1, PET_SPELL_LOCK_2,
PET_DEVOUR_MAGIC_1, PET_DEVOUR_MAGIC_2, PET_DEVOUR_MAGIC_3,
- PET_DEVOUR_MAGIC_4, PET_DEVOUR_MAGIC_5, PET_DEVOUR_MAGIC_6, PET_DEVOUR_MAGIC_7
+ PET_DEVOUR_MAGIC_4, PET_DEVOUR_MAGIC_5, PET_DEVOUR_MAGIC_6, PET_DEVOUR_MAGIC_7, PET_SPIRIT_WOLF_LEAP
};
bool MeleeAction::isUseful()
@@ -100,6 +111,11 @@ bool TogglePetSpellAutoCastAction::Execute(Event event)
toggled = true;
}
}
+
+ // Debug message if pet spells have been toggled and debug is enabled
+ if (toggled && sPlayerbotAIConfig->petChatCommandDebug == 1)
+ botAI->TellMaster("Pet autocast spells have been toggled.");
+
return toggled;
}
@@ -107,22 +123,23 @@ bool PetAttackAction::Execute(Event event)
{
Guardian* pet = bot->GetGuardianPet();
if (!pet)
- {
return false;
- }
+
+ // Do not attack if the pet's stance is set to "passive".
+ if (pet->GetReactState() == REACT_PASSIVE)
+ return false;
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
- {
return false;
- }
if (!bot->IsValidAttackTarget(target))
- {
return false;
- }
- pet->SetReactState(REACT_PASSIVE);
+ // This section has been commented because it was overriding the
+ // pet's stance to "passive" every time the attack action was executed.
+ // pet->SetReactState(REACT_PASSIVE);
+
pet->ClearUnitState(UNIT_STATE_FOLLOW);
pet->AttackStop();
pet->SetTarget(target->GetGUID());
@@ -136,3 +153,76 @@ bool PetAttackAction::Execute(Event event)
pet->ToCreature()->AI()->AttackStart(target);
return true;
}
+
+bool SetPetStanceAction::Execute(Event /*event*/)
+{
+ // Prepare a list to hold all controlled pet and guardian creatures
+ std::vector targets;
+
+ // Add the bot's main pet (if it exists) to the target list
+ Pet* pet = bot->GetPet();
+ if (pet)
+ targets.push_back(pet);
+
+ // Loop through all units controlled by the bot (could be pets, guardians, etc.)
+ for (Unit::ControlSet::const_iterator itr = bot->m_Controlled.begin(); itr != bot->m_Controlled.end(); ++itr)
+ {
+ // Only add creatures (skip players, vehicles, etc.)
+ Creature* creature = dynamic_cast(*itr);
+ if (!creature)
+ continue;
+ // Avoid adding the main pet twice
+ if (pet && creature == pet)
+ continue;
+ targets.push_back(creature);
+ }
+
+ // If there are no controlled pets or guardians, notify the player and exit
+ if (targets.empty())
+ {
+ botAI->TellError("You have no pet or guardian pet.");
+ return false;
+ }
+
+ // Get the default pet stance from the configuration
+ int32 stance = sPlayerbotAIConfig->defaultPetStance;
+ ReactStates react = REACT_DEFENSIVE;
+ std::string stanceText = "defensive (from config, fallback)";
+
+ // Map the config stance integer to a ReactStates value and a message
+ switch (stance)
+ {
+ case 0:
+ react = REACT_PASSIVE;
+ stanceText = "passive (from config)";
+ break;
+ case 1:
+ react = REACT_DEFENSIVE;
+ stanceText = "defensive (from config)";
+ break;
+ case 2:
+ react = REACT_AGGRESSIVE;
+ stanceText = "aggressive (from config)";
+ break;
+ default:
+ react = REACT_DEFENSIVE;
+ stanceText = "defensive (from config, fallback)";
+ break;
+ }
+
+ // Apply the stance to all target creatures (pets/guardians)
+ for (Creature* target : targets)
+ {
+ target->SetReactState(react);
+ CharmInfo* charmInfo = target->GetCharmInfo();
+ // If the creature has a CharmInfo, set the player-visible stance as well
+ if (charmInfo)
+ charmInfo->SetPlayerReactState(react);
+ }
+
+ // If debug is enabled in config, inform the master of the new stance
+ if (sPlayerbotAIConfig->petChatCommandDebug == 1)
+ botAI->TellMaster("Pet stance set to " + stanceText + " (applied to all pets/guardians).");
+
+ return true;
+}
diff --git a/src/strategy/actions/GenericActions.h b/src/strategy/actions/GenericActions.h
index d3347df5..1bc8d77e 100644
--- a/src/strategy/actions/GenericActions.h
+++ b/src/strategy/actions/GenericActions.h
@@ -7,6 +7,8 @@
#define _PLAYERBOT_GENERICACTIONS_H
#include "AttackAction.h"
+#include "Action.h"
+#include "PlayerbotAI.h"
class PlayerbotAI;
@@ -33,4 +35,12 @@ public:
virtual bool Execute(Event event) override;
};
+class SetPetStanceAction : public Action
+{
+public:
+ SetPetStanceAction(PlayerbotAI* botAI) : Action(botAI, "set pet stance") {}
+
+ bool Execute(Event event) override;
+};
+
#endif
diff --git a/src/strategy/actions/MoveToTravelTargetAction.cpp b/src/strategy/actions/MoveToTravelTargetAction.cpp
index 6782ea3f..9ae28919 100644
--- a/src/strategy/actions/MoveToTravelTargetAction.cpp
+++ b/src/strategy/actions/MoveToTravelTargetAction.cpp
@@ -18,7 +18,7 @@ bool MoveToTravelTargetAction::Execute(Event event)
WorldLocation location = *target->getPosition();
Group* group = bot->GetGroup();
- if (group && !urand(0, 1) && bot == botAI->GetGroupMaster())
+ if (group && !urand(0, 1) && bot == botAI->GetGroupMaster() && !bot->IsInCombat())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
diff --git a/src/strategy/actions/PetAction.cpp b/src/strategy/actions/PetAction.cpp
index dae760a7..e149b42e 100644
--- a/src/strategy/actions/PetAction.cpp
+++ b/src/strategy/actions/PetAction.cpp
@@ -4,373 +4,254 @@
*/
#include "PetAction.h"
-#include
-#include
-#include
+
+#include "CharmInfo.h"
+#include "Creature.h"
+#include "CreatureAI.h"
#include "Pet.h"
-#include "SpellMgr.h"
-#include "DBCStructure.h"
-#include "Log.h"
-#include "ObjectMgr.h"
#include "Player.h"
#include "PlayerbotAI.h"
-#include "PlayerbotFactory.h"
-#include
-#include
-#include "WorldSession.h"
-
-bool IsExoticPet(const CreatureTemplate* creature)
-{
- // Use the IsExotic() method from CreatureTemplate
- return creature && creature->IsExotic();
-}
-
-bool HasBeastMastery(Player* bot)
-{
- // Beast Mastery talent aura ID for WotLK is 53270
- return bot->HasAura(53270);
-}
+#include "SharedDefines.h"
bool PetAction::Execute(Event event)
{
+ // Extract the command parameter from the event (e.g., "aggressive", "defensive", "attack", etc.)
std::string param = event.getParam();
- std::istringstream iss(param);
- std::string mode, value;
- iss >> mode;
- std::getline(iss, value);
- value.erase(0, value.find_first_not_of(" ")); // trim leading spaces
+ if (param.empty() && !defaultCmd.empty())
+ {
+ param = defaultCmd;
+ }
- bool found = false;
-
- // Reset lastPetName/Id each time
- lastPetName = "";
- lastPetId = 0;
-
- if (mode == "name" && !value.empty())
+ if (param.empty())
{
- found = SetPetByName(value);
- }
- else if (mode == "id" && !value.empty())
- {
- try
- {
- uint32 id = std::stoul(value);
- found = SetPetById(id);
- }
- catch (...)
- {
- botAI->TellError("Invalid pet id.");
- }
- }
- else if (mode == "family" && !value.empty())
- {
- found = SetPetByFamily(value);
- }
- else if (mode == "rename" && !value.empty())
- {
- found = RenamePet(value);
- }
- else
- {
- botAI->TellError("Usage: pet name | pet id | pet family | pet rename ");
+ // If no parameter is provided, show usage instructions and return.
+ botAI->TellError("Usage: pet ");
return false;
}
- if (!found)
- return false;
-
- // For non-rename commands, initialize pet and give feedback
- if (mode != "rename")
- {
- Player* bot = botAI->GetBot();
- PlayerbotFactory factory(bot, bot->GetLevel());
- factory.InitPet();
- factory.InitPetTalents();
-
- if (!lastPetName.empty() && lastPetId != 0)
- {
- std::ostringstream oss;
- oss << "Pet changed to " << lastPetName << ", ID: " << lastPetId << ".";
- botAI->TellMaster(oss.str());
- }
- else
- {
- botAI->TellMaster("Pet changed and initialized!");
- }
- }
-
- return true;
-}
-
-bool PetAction::SetPetByName(const std::string& name)
-{
- // Convert the input to lowercase for case-insensitive comparison
- std::string lowerName = name;
- std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
-
- CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates();
Player* bot = botAI->GetBot();
- for (auto itr = creatures->begin(); itr != creatures->end(); ++itr)
- {
- const CreatureTemplate& creature = itr->second;
- std::string creatureName = creature.Name;
- std::transform(creatureName.begin(), creatureName.end(), creatureName.begin(), ::tolower);
+ // Collect all controlled pets and guardians, except totems, into the targets vector.
+ std::vector targets;
+ Pet* pet = bot->GetPet();
+ if (pet)
+ targets.push_back(pet);
- // Only match if names match (case-insensitive)
- if (creatureName == lowerName)
+ for (Unit::ControlSet::const_iterator itr = bot->m_Controlled.begin(); itr != bot->m_Controlled.end(); ++itr)
+ {
+ Creature* creature = dynamic_cast(*itr);
+ if (!creature)
+ continue;
+ if (pet && creature == pet)
+ continue;
+ if (creature->IsTotem())
+ continue;
+ targets.push_back(creature);
+ }
+
+ // If no pets or guardians are found, notify and return.
+ if (targets.empty())
+ {
+ botAI->TellError("You have no pet or guardian pet.");
+ return false;
+ }
+
+ ReactStates react;
+ std::string stanceText;
+
+ // Handle stance commands: aggressive, defensive, or passive.
+ if (param == "aggressive")
+ {
+ react = REACT_AGGRESSIVE;
+ stanceText = "aggressive";
+ }
+ else if (param == "defensive")
+ {
+ react = REACT_DEFENSIVE;
+ stanceText = "defensive";
+ }
+ else if (param == "passive")
+ {
+ react = REACT_PASSIVE;
+ stanceText = "passive";
+ }
+ // The "stance" command simply reports the current stance of each pet/guardian.
+ else if (param == "stance")
+ {
+ for (Creature* target : targets)
{
- // Check if the pet is tameable at all
- if (!creature.IsTameable(true))
+ std::string type = target->IsPet() ? "pet" : "guardian";
+ std::string name = target->GetName();
+ std::string stance;
+ switch (target->GetReactState())
+ {
+ case REACT_AGGRESSIVE:
+ stance = "aggressive";
+ break;
+ case REACT_DEFENSIVE:
+ stance = "defensive";
+ break;
+ case REACT_PASSIVE:
+ stance = "passive";
+ break;
+ default:
+ stance = "unknown";
+ break;
+ }
+ botAI->TellMaster("Current stance of " + type + " \"" + name + "\": " + stance + ".");
+ }
+ return true;
+ }
+ // The "attack" command forces pets/guardians to attack the master's selected target.
+ else if (param == "attack")
+ {
+ // Try to get the master's selected target.
+ Player* master = botAI->GetMaster();
+ Unit* targetUnit = nullptr;
+
+ if (master)
+ {
+ ObjectGuid masterTargetGuid = master->GetTarget();
+ if (!masterTargetGuid.IsEmpty())
+ {
+ targetUnit = botAI->GetUnit(masterTargetGuid);
+ }
+ }
+
+ // If no valid target is selected, show an error and return.
+ if (!targetUnit)
+ {
+ botAI->TellError("No valid target selected by master.");
+ return false;
+ }
+ if (!targetUnit->IsAlive())
+ {
+ botAI->TellError("Target is not alive.");
+ return false;
+ }
+ if (!bot->IsValidAttackTarget(targetUnit))
+ {
+ botAI->TellError("Target is not a valid attack target for the bot.");
+ return false;
+ }
+
+ bool didAttack = false;
+ // For each controlled pet/guardian, command them to attack the selected target.
+ for (Creature* petCreature : targets)
+ {
+ CharmInfo* charmInfo = petCreature->GetCharmInfo();
+ if (!charmInfo)
continue;
- // Exotic pet check with talent requirement
- if (IsExoticPet(&creature) && !HasBeastMastery(bot))
+ petCreature->ClearUnitState(UNIT_STATE_FOLLOW);
+ // Only command attack if not already attacking the target, or if not currently under command attack.
+ if (petCreature->GetVictim() != targetUnit ||
+ (petCreature->GetVictim() == targetUnit && !charmInfo->IsCommandAttack()))
{
- botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
- return false;
+ if (petCreature->GetVictim())
+ petCreature->AttackStop();
+
+ if (!petCreature->IsPlayer() && petCreature->ToCreature()->IsAIEnabled)
+ {
+ // For AI-enabled creatures (NPC pets/guardians): issue attack command and set flags.
+ charmInfo->SetIsCommandAttack(true);
+ charmInfo->SetIsAtStay(false);
+ charmInfo->SetIsFollowing(false);
+ charmInfo->SetIsCommandFollow(false);
+ charmInfo->SetIsReturning(false);
+
+ petCreature->ToCreature()->AI()->AttackStart(targetUnit);
+
+ didAttack = true;
+ }
+ else // For charmed player pets/guardians
+ {
+ if (petCreature->GetVictim() && petCreature->GetVictim() != targetUnit)
+ petCreature->AttackStop();
+
+ charmInfo->SetIsCommandAttack(true);
+ charmInfo->SetIsAtStay(false);
+ charmInfo->SetIsFollowing(false);
+ charmInfo->SetIsCommandFollow(false);
+ charmInfo->SetIsReturning(false);
+
+ petCreature->Attack(targetUnit, true);
+ didAttack = true;
+ }
+ }
+ }
+ // Inform the master if the command succeeded or failed.
+ if (didAttack && sPlayerbotAIConfig->petChatCommandDebug == 1)
+ botAI->TellMaster("Pet commanded to attack your target.");
+ else if (!didAttack)
+ botAI->TellError("Pet did not attack. (Already attacking or unable to attack target)");
+ return didAttack;
+ }
+ // The "follow" command makes all pets/guardians follow the bot.
+ else if (param == "follow")
+ {
+ botAI->PetFollow();
+ if (sPlayerbotAIConfig->petChatCommandDebug == 1)
+ botAI->TellMaster("Pet commanded to follow.");
+ return true;
+ }
+ // The "stay" command causes all pets/guardians to stop and stay in place.
+ else if (param == "stay")
+ {
+ for (Creature* target : targets)
+ {
+ // If not already in controlled motion, stop movement and set to idle.
+ bool controlledMotion =
+ target->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_CONTROLLED) != NULL_MOTION_TYPE;
+ if (!controlledMotion)
+ {
+ target->StopMovingOnCurrentPos();
+ target->GetMotionMaster()->Clear(false);
+ target->GetMotionMaster()->MoveIdle();
}
- // Final tameable check based on hunter's actual ability
- if (!creature.IsTameable(bot->CanTameExoticPets()))
- continue;
+ CharmInfo* charmInfo = target->GetCharmInfo();
+ if (charmInfo)
+ {
+ // Set charm/pet state flags for "stay".
+ charmInfo->SetCommandState(COMMAND_STAY);
+ charmInfo->SetIsCommandAttack(false);
+ charmInfo->SetIsCommandFollow(false);
+ charmInfo->SetIsFollowing(false);
+ charmInfo->SetIsReturning(false);
+ charmInfo->SetIsAtStay(!controlledMotion);
+ charmInfo->SaveStayPosition(controlledMotion);
+ if (target->ToPet())
+ target->ToPet()->ClearCastWhenWillAvailable();
- lastPetName = creature.Name;
- lastPetId = creature.Entry;
- return CreateAndSetPet(creature.Entry);
+ charmInfo->SetForcedSpell(0);
+ charmInfo->SetForcedTargetGUID();
+ }
}
+ if (sPlayerbotAIConfig->petChatCommandDebug == 1)
+ botAI->TellMaster("Pet commanded to stay.");
+ return true;
}
-
- botAI->TellError("No tameable pet found with name: " + name);
- return false;
-}
-
-bool PetAction::SetPetById(uint32 id)
-{
- CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(id);
- Player* bot = botAI->GetBot();
-
- if (creature)
+ // Unknown command: show usage instructions and return.
+ else
{
- // Check if the pet is tameable at all
- if (!creature->IsTameable(true))
- {
- botAI->TellError("No tameable pet found with id: " + std::to_string(id));
- return false;
- }
-
- // Exotic pet check with talent requirement
- if (IsExoticPet(creature) && !HasBeastMastery(bot))
- {
- botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
- return false;
- }
-
- // Final tameable check based on hunter's actual ability
- if (!creature->IsTameable(bot->CanTameExoticPets()))
- {
- botAI->TellError("No tameable pet found with id: " + std::to_string(id));
- return false;
- }
-
- lastPetName = creature->Name;
- lastPetId = creature->Entry;
- return CreateAndSetPet(creature->Entry);
- }
-
- botAI->TellError("No tameable pet found with id: " + std::to_string(id));
- return false;
-}
-
-bool PetAction::SetPetByFamily(const std::string& family)
-{
- std::string lowerFamily = family;
- std::transform(lowerFamily.begin(), lowerFamily.end(), lowerFamily.begin(), ::tolower);
-
- CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates();
- Player* bot = botAI->GetBot();
-
- std::vector candidates;
- bool foundExotic = false;
-
- for (auto itr = creatures->begin(); itr != creatures->end(); ++itr)
- {
- const CreatureTemplate& creature = itr->second;
-
- if (!creature.IsTameable(true)) // allow exotics for search
- continue;
-
- CreatureFamilyEntry const* familyEntry = sCreatureFamilyStore.LookupEntry(creature.family);
- if (!familyEntry)
- continue;
-
- std::string familyName = familyEntry->Name[0];
- std::transform(familyName.begin(), familyName.end(), familyName.begin(), ::tolower);
-
- if (familyName != lowerFamily)
- continue;
-
- // Exotic/BM check
- if (IsExoticPet(&creature))
- {
- foundExotic = true;
- if (!HasBeastMastery(bot))
- continue;
- }
-
- if (!creature.IsTameable(bot->CanTameExoticPets()))
- continue;
-
- candidates.push_back(&creature);
- }
-
- if (candidates.empty())
- {
- if (foundExotic && !HasBeastMastery(bot))
- botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
- else
- botAI->TellError("No tameable pet found with family: " + family);
+ botAI->TellError("Unknown pet command: " + param +
+ ". Use: pet ");
return false;
}
- // Randomly select one from candidates
- std::random_device rd;
- std::mt19937 gen(rd());
- std::uniform_int_distribution<> dis(0, candidates.size() - 1);
-
- const CreatureTemplate* selected = candidates[dis(gen)];
-
- lastPetName = selected->Name;
- lastPetId = selected->Entry;
- return CreateAndSetPet(selected->Entry);
-}
-
-bool PetAction::RenamePet(const std::string& newName)
-{
- Player* bot = botAI->GetBot();
- Pet* pet = bot->GetPet();
- if (!pet)
+ // For stance commands, apply the chosen stance to all targets.
+ for (Creature* target : targets)
{
- botAI->TellError("You have no pet to rename.");
- return false;
+ target->SetReactState(react);
+ CharmInfo* charmInfo = target->GetCharmInfo();
+ if (charmInfo)
+ charmInfo->SetPlayerReactState(react);
}
- // Length check (WoW max pet name is 12 characters)
- if (newName.empty() || newName.length() > 12)
- {
- botAI->TellError("Pet name must be between 1 and 12 alphabetic characters.");
- return false;
- }
-
- // Alphabetic character check
- for (char c : newName)
- {
- if (!std::isalpha(static_cast(c)))
- {
- botAI->TellError("Pet name must only contain alphabetic characters (A-Z, a-z).");
- return false;
- }
- }
-
- // Normalize case: capitalize first letter, lower the rest
- std::string normalized = newName;
- normalized[0] = std::toupper(normalized[0]);
- for (size_t i = 1; i < normalized.size(); ++i)
- normalized[i] = std::tolower(normalized[i]);
-
- // Forbidden name check
- if (sObjectMgr->IsReservedName(normalized))
- {
- botAI->TellError("That pet name is forbidden. Please choose another name.");
- return false;
- }
-
- // Set the pet's name, save to DB, and send instant client update
- pet->SetName(normalized);
- pet->SavePetToDB(PET_SAVE_AS_CURRENT);
- bot->GetSession()->SendPetNameQuery(pet->GetGUID(), pet->GetEntry());
-
- botAI->TellMaster("Your pet has been renamed to " + normalized + "!");
- botAI->TellMaster("If you do not see the new name, please dismiss and recall your pet.");
-
- // Dismiss pet
- bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT, true);
- // Recall pet using Hunter's Call Pet spell (spellId 883)
- if (bot->getClass() == CLASS_HUNTER && bot->HasSpell(883))
- {
- bot->CastSpell(bot, 883, true);
- }
-
- return true;
-}
-
-bool PetAction::CreateAndSetPet(uint32 creatureEntry)
-{
- Player* bot = botAI->GetBot();
- if (bot->getClass() != CLASS_HUNTER || bot->GetLevel() < 10)
- {
- botAI->TellError("Only level 10+ hunters can have pets.");
- return false;
- }
-
- CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(creatureEntry);
- if (!creature)
- {
- botAI->TellError("Creature template not found.");
- return false;
- }
-
- // Remove current pet(s)
- if (bot->GetPetStable() && bot->GetPetStable()->CurrentPet)
- {
- bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT);
- bot->RemovePet(nullptr, PET_SAVE_NOT_IN_SLOT);
- }
- if (bot->GetPetStable() && bot->GetPetStable()->GetUnslottedHunterPet())
- {
- bot->GetPetStable()->UnslottedPets.clear();
- bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT);
- bot->RemovePet(nullptr, PET_SAVE_NOT_IN_SLOT);
- }
-
- // Actually create the new pet
- Pet* pet = bot->CreateTamedPetFrom(creatureEntry, 0);
- if (!pet)
- {
- botAI->TellError("Failed to create pet.");
- return false;
- }
-
- // Set pet level and add to world
- pet->SetUInt32Value(UNIT_FIELD_LEVEL, bot->GetLevel() - 1);
- pet->GetMap()->AddToMap(pet->ToCreature());
- pet->SetUInt32Value(UNIT_FIELD_LEVEL, bot->GetLevel());
- bot->SetMinion(pet, true);
- pet->InitTalentForLevel();
- pet->SavePetToDB(PET_SAVE_AS_CURRENT);
- bot->PetSpellInitialize();
-
- // Set stats
- pet->InitStatsForLevel(bot->GetLevel());
- pet->SetLevel(bot->GetLevel());
- pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS)));
- pet->SetHealth(pet->GetMaxHealth());
-
- // Enable autocast for active spells
- for (PetSpellMap::const_iterator itr = pet->m_spells.begin(); itr != pet->m_spells.end(); ++itr)
- {
- if (itr->second.state == PETSPELL_REMOVED)
- continue;
-
- SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first);
- if (!spellInfo)
- continue;
-
- if (spellInfo->IsPassive())
- continue;
-
- pet->ToggleAutocast(spellInfo, true);
- }
+ // Inform the master of the new stance if debug is enabled.
+ if (sPlayerbotAIConfig->petChatCommandDebug == 1)
+ botAI->TellMaster("Pet stance set to " + stanceText + ".");
return true;
}
diff --git a/src/strategy/actions/PetAction.h b/src/strategy/actions/PetAction.h
index 74009e86..d3cd846a 100644
--- a/src/strategy/actions/PetAction.h
+++ b/src/strategy/actions/PetAction.h
@@ -10,25 +10,20 @@
#include "Action.h"
#include "PlayerbotFactory.h"
+#include "Unit.h"
class PlayerbotAI;
class PetAction : public Action
{
public:
- PetAction(PlayerbotAI* botAI) : Action(botAI, "pet") {}
+ PetAction(PlayerbotAI* botAI, const std::string& defaultCmd = "") : Action(botAI, "pet"), defaultCmd(defaultCmd) {}
bool Execute(Event event) override;
private:
- bool SetPetByName(const std::string& name);
- bool SetPetById(uint32 id);
- bool SetPetByFamily(const std::string& family);
- bool RenamePet(const std::string& newName);
-
- bool CreateAndSetPet(uint32 creatureEntry);
-
- std::string lastPetName;
- uint32 lastPetId = 0;
+ bool warningEnabled = true;
+ std::string defaultCmd;
};
+
#endif
diff --git a/src/strategy/actions/TameAction.cpp b/src/strategy/actions/TameAction.cpp
new file mode 100644
index 00000000..8446c24c
--- /dev/null
+++ b/src/strategy/actions/TameAction.cpp
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license, you may redistribute it
+ * and/or modify it under version 2 of the License, or (at your option), any later version.
+ */
+
+#include "TameAction.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include "DBCStructure.h"
+#include "Log.h"
+#include "ObjectMgr.h"
+#include "Pet.h"
+#include "Player.h"
+#include "PlayerbotAI.h"
+#include "PlayerbotFactory.h"
+#include "SpellMgr.h"
+#include "WorldSession.h"
+
+bool IsExoticPet(const CreatureTemplate* creature)
+{
+ // Use the IsExotic() method from CreatureTemplate
+ return creature && creature->IsExotic();
+}
+
+bool HasBeastMastery(Player* bot)
+{
+ // Beast Mastery talent aura ID for WotLK is 53270
+ return bot->HasAura(53270);
+}
+
+bool TameAction::Execute(Event event)
+{
+ // Parse the user's input command into mode and value (e.g. "name wolf", "id 1234", etc.)
+ std::string param = event.getParam();
+ std::istringstream iss(param);
+ std::string mode, value;
+ iss >> mode;
+ std::getline(iss, value);
+ value.erase(0, value.find_first_not_of(" ")); // Remove leading spaces from value
+
+ bool found = false;
+
+ // Reset any previous pet name/id state
+ lastPetName = "";
+ lastPetId = 0;
+
+ // If the command is "family" with no value, list all available pet families
+ if (mode == "family" && value.empty())
+ {
+ std::set normalFamilies;
+ std::set exoticFamilies;
+ Player* bot = botAI->GetBot();
+
+ // Loop over all creature templates and collect tameable families
+ CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates();
+ for (auto itr = creatures->begin(); itr != creatures->end(); ++itr)
+ {
+ const CreatureTemplate& creature = itr->second;
+ if (!creature.IsTameable(true))
+ continue;
+
+ CreatureFamilyEntry const* familyEntry = sCreatureFamilyStore.LookupEntry(creature.family);
+ if (!familyEntry)
+ continue;
+
+ std::string familyName = familyEntry->Name[0];
+ if (familyName.empty())
+ continue;
+
+ if (creature.IsExotic())
+ exoticFamilies.insert(familyName);
+ else
+ normalFamilies.insert(familyName);
+ }
+
+ // Build the output message for the user
+ std::ostringstream oss;
+ oss << "Available pet families: ";
+ size_t count = 0;
+ for (const auto& name : normalFamilies)
+ {
+ if (count++ != 0)
+ oss << ", ";
+ oss << name;
+ }
+ if (!exoticFamilies.empty())
+ {
+ if (!normalFamilies.empty())
+ oss << " | ";
+ oss << "Exotic: ";
+ count = 0;
+ for (const auto& name : exoticFamilies)
+ {
+ if (count++ != 0)
+ oss << ", ";
+ oss << name;
+ }
+ }
+
+ botAI->TellError(oss.str());
+ return true;
+ }
+
+ // Handle "tame abandon" command to give up your current pet
+ if (mode == "abandon")
+ {
+ return AbandonPet();
+ }
+
+ // Try to process the command based on mode and value
+ if (mode == "name" && !value.empty())
+ {
+ found = SetPetByName(value);
+ }
+ else if (mode == "id" && !value.empty())
+ {
+ // Try to convert value to an integer and set pet by ID
+ try
+ {
+ uint32 id = std::stoul(value);
+ found = SetPetById(id);
+ }
+ catch (...)
+ {
+ botAI->TellError("Invalid tame id.");
+ }
+ }
+ else if (mode == "family" && !value.empty())
+ {
+ found = SetPetByFamily(value);
+ }
+ else if (mode == "rename" && !value.empty())
+ {
+ found = RenamePet(value);
+ }
+ else
+ {
+ // Unrecognized command or missing argument; show usage
+ botAI->TellError(
+ "Usage: tame name | tame id | tame family | tame rename | tame abandon");
+ return false;
+ }
+
+ // If the requested tame/rename failed, return failure
+ if (!found)
+ return false;
+
+ // For all non-rename commands, initialize the new pet and talents, then notify the master
+ if (mode != "rename")
+ {
+ Player* bot = botAI->GetBot();
+ PlayerbotFactory factory(bot, bot->GetLevel());
+ factory.InitPet();
+ factory.InitPetTalents();
+
+ if (!lastPetName.empty() && lastPetId != 0)
+ {
+ std::ostringstream oss;
+ oss << "Pet changed to " << lastPetName << ", ID: " << lastPetId << ".";
+ botAI->TellMaster(oss.str());
+ }
+ else
+ {
+ botAI->TellMaster("Pet changed and initialized!");
+ }
+ }
+
+ return true;
+}
+
+bool TameAction::SetPetByName(const std::string& name)
+{
+ // Make a lowercase copy of the input name for case-insensitive comparison
+ std::string lowerName = name;
+ std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
+
+ // Get the full list of creature templates from the object manager
+ CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates();
+ Player* bot = botAI->GetBot();
+
+ // Iterate through all creature templates
+ for (auto itr = creatures->begin(); itr != creatures->end(); ++itr)
+ {
+ const CreatureTemplate& creature = itr->second;
+ std::string creatureName = creature.Name;
+ // Convert creature's name to lowercase for comparison
+ std::transform(creatureName.begin(), creatureName.end(), creatureName.begin(), ::tolower);
+
+ // If the input name matches this creature's name
+ if (creatureName == lowerName)
+ {
+ // Skip if the creature isn't tameable at all
+ if (!creature.IsTameable(true))
+ continue;
+
+ // If the creature is exotic and the bot doesn't have Beast Mastery, show error and fail
+ if (IsExoticPet(&creature) && !HasBeastMastery(bot))
+ {
+ botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
+ return false;
+ }
+
+ // Skip if the creature isn't tameable by this bot (respecting exotic pet rules)
+ if (!creature.IsTameable(bot->CanTameExoticPets()))
+ continue;
+
+ // Store the found pet's name and entry ID for later use/feedback
+ lastPetName = creature.Name;
+ lastPetId = creature.Entry;
+ // Create and set this pet for the bot
+ return CreateAndSetPet(creature.Entry);
+ }
+ }
+
+ // If no suitable pet found, show an error and return failure
+ botAI->TellError("No tameable pet found with name: " + name);
+ return false;
+}
+
+bool TameAction::SetPetById(uint32 id)
+{
+ // Look up the creature template by its numeric entry/id
+ CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(id);
+ Player* bot = botAI->GetBot();
+
+ // Proceed only if a valid creature was found
+ if (creature)
+ {
+ // Check if this creature is ever tameable (ignore bot's own restrictions for now)
+ if (!creature->IsTameable(true))
+ {
+ // If not tameable at all, show an error and fail
+ botAI->TellError("No tameable pet found with id: " + std::to_string(id));
+ return false;
+ }
+
+ // If it's an exotic pet, make sure the bot has the Beast Mastery talent
+ if (IsExoticPet(creature) && !HasBeastMastery(bot))
+ {
+ botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
+ return false;
+ }
+
+ // Check if the bot is actually allowed to tame this pet (honoring exotic pet rules)
+ if (!creature->IsTameable(bot->CanTameExoticPets()))
+ {
+ botAI->TellError("No tameable pet found with id: " + std::to_string(id));
+ return false;
+ }
+
+ // Remember this pet's name and id for later feedback
+ lastPetName = creature->Name;
+ lastPetId = creature->Entry;
+ // Set and create the pet for the bot
+ return CreateAndSetPet(creature->Entry);
+ }
+
+ // If no valid creature was found by id, show an error
+ botAI->TellError("No tameable pet found with id: " + std::to_string(id));
+ return false;
+}
+
+bool TameAction::SetPetByFamily(const std::string& family)
+{
+ // Convert the input family name to lowercase for case-insensitive comparison
+ std::string lowerFamily = family;
+ std::transform(lowerFamily.begin(), lowerFamily.end(), lowerFamily.begin(), ::tolower);
+
+ // Get all creature templates from the object manager
+ CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates();
+ Player* bot = botAI->GetBot();
+
+ // Prepare a list of candidate creatures and track if any exotic pet is found
+ std::vector candidates;
+ bool foundExotic = false;
+
+ // Iterate through all creature templates
+ for (auto itr = creatures->begin(); itr != creatures->end(); ++itr)
+ {
+ const CreatureTemplate& creature = itr->second;
+
+ // Skip if this creature is never tameable
+ if (!creature.IsTameable(true))
+ continue;
+
+ // Look up the family entry for this creature
+ CreatureFamilyEntry const* familyEntry = sCreatureFamilyStore.LookupEntry(creature.family);
+ if (!familyEntry)
+ continue;
+
+ // Compare the family name in a case-insensitive way
+ std::string familyName = familyEntry->Name[0];
+ std::transform(familyName.begin(), familyName.end(), familyName.begin(), ::tolower);
+
+ if (familyName != lowerFamily)
+ continue;
+
+ // If the creature is exotic, check Beast Mastery talent requirements
+ if (IsExoticPet(&creature))
+ {
+ foundExotic = true;
+ if (!HasBeastMastery(bot))
+ continue;
+ }
+
+ // Only add as candidate if this bot is allowed to tame it (including exotic rules)
+ if (!creature.IsTameable(bot->CanTameExoticPets()))
+ continue;
+
+ candidates.push_back(&creature);
+ }
+
+ // If no candidates found, inform the user of the reason and return false
+ if (candidates.empty())
+ {
+ if (foundExotic && !HasBeastMastery(bot))
+ botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
+ else
+ botAI->TellError("No tameable pet found with family: " + family);
+ return false;
+ }
+
+ // Randomly select one candidate from the list to tame
+ std::random_device rd;
+ std::mt19937 gen(rd());
+ std::uniform_int_distribution<> dis(0, candidates.size() - 1);
+
+ const CreatureTemplate* selected = candidates[dis(gen)];
+
+ // Save the selected pet's name and id for feedback
+ lastPetName = selected->Name;
+ lastPetId = selected->Entry;
+ // Attempt to create and set the new pet for the bot
+ return CreateAndSetPet(selected->Entry);
+}
+
+bool TameAction::RenamePet(const std::string& newName)
+{
+ Player* bot = botAI->GetBot();
+ Pet* pet = bot->GetPet();
+ // Check if the bot currently has a pet
+ if (!pet)
+ {
+ botAI->TellError("You have no pet to rename.");
+ return false;
+ }
+
+ // Validate the new name: must not be empty and max 12 characters
+ if (newName.empty() || newName.length() > 12)
+ {
+ botAI->TellError("Pet name must be between 1 and 12 alphabetic characters.");
+ return false;
+ }
+
+ // Ensure all characters in the new name are alphabetic
+ for (char c : newName)
+ {
+ if (!std::isalpha(static_cast(c)))
+ {
+ botAI->TellError("Pet name must only contain alphabetic characters (A-Z, a-z).");
+ return false;
+ }
+ }
+
+ // Normalize the name: capitalize the first letter, lowercase the rest
+ std::string normalized = newName;
+ normalized[0] = std::toupper(normalized[0]);
+ for (size_t i = 1; i < normalized.size(); ++i)
+ normalized[i] = std::tolower(normalized[i]);
+
+ // Check if the new name is reserved or forbidden
+ if (sObjectMgr->IsReservedName(normalized))
+ {
+ botAI->TellError("That pet name is forbidden. Please choose another name.");
+ return false;
+ }
+
+ // Set the pet's name and save it to the database
+ pet->SetName(normalized);
+ pet->SavePetToDB(PET_SAVE_AS_CURRENT);
+ bot->GetSession()->SendPetNameQuery(pet->GetGUID(), pet->GetEntry());
+
+ // Notify the master about the rename and give a tip to update the client name display
+ botAI->TellMaster("Your pet has been renamed to " + normalized + "!");
+ botAI->TellMaster("If you do not see the new name, please dismiss and recall your pet.");
+
+ // Remove the current pet and (re-)cast Call Pet spell if the bot is a hunter
+ bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT, true);
+ if (bot->getClass() == CLASS_HUNTER && bot->HasSpell(883))
+ {
+ bot->CastSpell(bot, 883, true);
+ }
+
+ return true;
+}
+
+bool TameAction::CreateAndSetPet(uint32 creatureEntry)
+{
+ Player* bot = botAI->GetBot();
+ // Ensure the player is a hunter and at least level 10 (required for pets)
+ if (bot->getClass() != CLASS_HUNTER || bot->GetLevel() < 10)
+ {
+ botAI->TellError("Only level 10+ hunters can have pets.");
+ return false;
+ }
+
+ // Retrieve the creature template for the given entry (pet species info)
+ CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(creatureEntry);
+ if (!creature)
+ {
+ botAI->TellError("Creature template not found.");
+ return false;
+ }
+
+ // If the bot already has a current pet or an unslotted pet, remove them to avoid conflicts
+ if (bot->GetPetStable() && bot->GetPetStable()->CurrentPet)
+ {
+ bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT);
+ bot->RemovePet(nullptr, PET_SAVE_NOT_IN_SLOT);
+ }
+ if (bot->GetPetStable() && bot->GetPetStable()->GetUnslottedHunterPet())
+ {
+ bot->GetPetStable()->UnslottedPets.clear();
+ bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT);
+ bot->RemovePet(nullptr, PET_SAVE_NOT_IN_SLOT);
+ }
+
+ // Create the new tamed pet from the specified creature entry
+ Pet* pet = bot->CreateTamedPetFrom(creatureEntry, 0);
+ if (!pet)
+ {
+ botAI->TellError("Failed to create pet.");
+ return false;
+ }
+
+ // Set the pet's level to one below the bot's current level, then add to the map and set to full level
+ pet->SetUInt32Value(UNIT_FIELD_LEVEL, bot->GetLevel() - 1);
+ pet->GetMap()->AddToMap(pet->ToCreature());
+ pet->SetUInt32Value(UNIT_FIELD_LEVEL, bot->GetLevel());
+ // Set the pet as the bot's active minion
+ bot->SetMinion(pet, true);
+ // Initialize talents appropriate for the pet's level
+ pet->InitTalentForLevel();
+ // Save pet to the database as the current pet
+ pet->SavePetToDB(PET_SAVE_AS_CURRENT);
+ // Initialize available pet spells
+ bot->PetSpellInitialize();
+
+ // Further initialize pet stats to match the bot's level
+ pet->InitStatsForLevel(bot->GetLevel());
+ pet->SetLevel(bot->GetLevel());
+ // Set happiness and health of the pet to maximum values
+ pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS)));
+ pet->SetHealth(pet->GetMaxHealth());
+
+ // Enable autocast for all active (not removed) non-passive spells the pet knows
+ for (PetSpellMap::const_iterator itr = pet->m_spells.begin(); itr != pet->m_spells.end(); ++itr)
+ {
+ if (itr->second.state == PETSPELL_REMOVED)
+ continue;
+
+ SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first);
+ if (!spellInfo)
+ continue;
+
+ if (spellInfo->IsPassive())
+ continue;
+
+ pet->ToggleAutocast(spellInfo, true);
+ }
+
+ return true;
+}
+
+bool TameAction::AbandonPet()
+{
+ // Get the bot player and its current pet (if any)
+ Player* bot = botAI->GetBot();
+ Pet* pet = bot->GetPet();
+
+ // Check if the bot has a pet and that it is a hunter pet
+ if (pet && pet->getPetType() == HUNTER_PET)
+ {
+ // Remove the pet from the bot and mark it as deleted in the database
+ bot->RemovePet(pet, PET_SAVE_AS_DELETED);
+ // Inform the bot's master/player that the pet was abandoned
+ botAI->TellMaster("Your pet has been abandoned.");
+ return true;
+ }
+ else
+ {
+ // If there is no hunter pet, show an error message
+ botAI->TellError("You have no hunter pet to abandon.");
+ return false;
+ }
+}
diff --git a/src/strategy/actions/TameAction.h b/src/strategy/actions/TameAction.h
new file mode 100644
index 00000000..018ba5a0
--- /dev/null
+++ b/src/strategy/actions/TameAction.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license, you may redistribute it
+ * and/or modify it under version 2 of the License, or (at your option), any later version.
+ */
+
+#ifndef _PLAYERBOT_TAMEACTION_H
+#define _PLAYERBOT_TAMEACTION_H
+
+#include
+#include "Action.h"
+#include "PlayerbotFactory.h"
+
+class PlayerbotAI;
+
+class TameAction : public Action
+{
+public:
+ TameAction(PlayerbotAI* botAI) : Action(botAI, "tame") {}
+
+ bool Execute(Event event) override;
+
+private:
+ bool SetPetByName(const std::string& name);
+ bool SetPetById(uint32 id);
+ bool SetPetByFamily(const std::string& family);
+ bool RenamePet(const std::string& newName);
+ bool CreateAndSetPet(uint32 creatureEntry);
+ bool AbandonPet();
+
+ std::string lastPetName;
+ uint32 lastPetId = 0;
+};
+
+#endif
diff --git a/src/strategy/actions/WorldPacketActionContext.h b/src/strategy/actions/WorldPacketActionContext.h
index 88540cfd..0df4bd06 100644
--- a/src/strategy/actions/WorldPacketActionContext.h
+++ b/src/strategy/actions/WorldPacketActionContext.h
@@ -41,6 +41,7 @@
#include "UseMeetingStoneAction.h"
#include "NamedObjectContext.h"
#include "ReleaseSpiritAction.h"
+#include "PetAction.h"
class PlayerbotAI;
@@ -70,6 +71,7 @@ public:
creators["trade status extended"] = &WorldPacketActionContext::trade_status_extended;
creators["store loot"] = &WorldPacketActionContext::store_loot;
creators["self resurrect"] = &WorldPacketActionContext::self_resurrect;
+ creators["pet"] = &WorldPacketActionContext::pet;
// quest
creators["talk to quest giver"] = &WorldPacketActionContext::turn_in_quest;
@@ -139,6 +141,7 @@ private:
static Action* tell_not_enough_reputation(PlayerbotAI* botAI) { return new TellMasterAction(botAI, "Not enough reputation"); }
static Action* tell_cannot_equip(PlayerbotAI* botAI) { return new InventoryChangeFailureAction(botAI); }
static Action* self_resurrect(PlayerbotAI* botAI) { return new SelfResurrectAction(botAI); }
+ static Action* pet(PlayerbotAI* botAI) { return new PetAction(botAI); }
// quest
static Action* quest_update_add_kill(PlayerbotAI* ai) { return new QuestUpdateAddKillAction(ai); }
diff --git a/src/strategy/deathknight/GenericDKNonCombatStrategy.cpp b/src/strategy/deathknight/GenericDKNonCombatStrategy.cpp
index 2a696322..99163441 100644
--- a/src/strategy/deathknight/GenericDKNonCombatStrategy.cpp
+++ b/src/strategy/deathknight/GenericDKNonCombatStrategy.cpp
@@ -50,7 +50,9 @@ void GenericDKNonCombatStrategy::InitTriggers(std::vector& trigger
triggers.push_back(
new TriggerNode("bone shield", NextAction::array(0, new NextAction("bone shield", 21.0f), nullptr)));
triggers.push_back(
- new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 11.0f), NULL)));
+ new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), NULL)));
+ triggers.push_back(
+ new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), NULL)));
}
void DKBuffDpsStrategy::InitTriggers(std::vector& triggers)
diff --git a/src/strategy/deathknight/GenericDKStrategy.cpp b/src/strategy/deathknight/GenericDKStrategy.cpp
index 55146a56..a1810a8c 100644
--- a/src/strategy/deathknight/GenericDKStrategy.cpp
+++ b/src/strategy/deathknight/GenericDKStrategy.cpp
@@ -171,6 +171,10 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers)
// NextAction::array(0, new NextAction("anti magic zone", ACTION_EMERGENCY + 1), nullptr)));
triggers.push_back(
new TriggerNode("no pet", NextAction::array(0, new NextAction("raise dead", ACTION_NORMAL + 5), nullptr)));
+ triggers.push_back(
+ new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), nullptr)));
+ triggers.push_back(
+ new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
triggers.push_back(
new TriggerNode("mind freeze", NextAction::array(0, new NextAction("mind freeze", ACTION_HIGH + 1), nullptr)));
triggers.push_back(
diff --git a/src/strategy/druid/GenericDruidNonCombatStrategy.cpp b/src/strategy/druid/GenericDruidNonCombatStrategy.cpp
index 02d975f0..10f2314e 100644
--- a/src/strategy/druid/GenericDruidNonCombatStrategy.cpp
+++ b/src/strategy/druid/GenericDruidNonCombatStrategy.cpp
@@ -119,6 +119,42 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector& trig
// triggers.push_back(new TriggerNode("low mana", NextAction::array(0, new NextAction("innervate", ACTION_EMERGENCY
// + 5), nullptr))); triggers.push_back(new TriggerNode("swimming", NextAction::array(0, new NextAction("aquatic
// form", 1.0f), nullptr)));
+
+ triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("apply oil", 1.0f), nullptr)));
+
+ triggers.push_back(
+ new TriggerNode("party member critical health",
+ NextAction::array(0,
+ new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 7),
+ new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 6),
+ new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 5),
+ nullptr)));
+
+ triggers.push_back(
+ new TriggerNode("party member low health",
+ NextAction::array(0,
+ new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 5),
+ new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 4),
+ new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 3),
+ nullptr)));
+
+ triggers.push_back(
+ new TriggerNode("party member medium health",
+ NextAction::array(0, new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 3),
+ new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 2),
+ new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 1),
+ nullptr)));
+
+ triggers.push_back(
+ new TriggerNode("party member almost full health",
+ NextAction::array(0, new NextAction("wild growth on party", ACTION_LIGHT_HEAL + 3), new NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 2), NULL)));
+
+ triggers.push_back(
+ new TriggerNode("party member remove curse",
+ NextAction::array(0, new NextAction("remove curse on party", ACTION_DISPEL + 7), nullptr)));
+ triggers.push_back(
+ new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
+
triggers.push_back(new TriggerNode("party member critical health", NextAction::array(0,
new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 7),
new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 6),
@@ -147,6 +183,7 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector& trig
triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("apply oil", 1.0f), nullptr)));
if (specTab == 1) // Feral
triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("apply stone", 1.0f), nullptr)));
+
}
GenericDruidBuffStrategy::GenericDruidBuffStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI)
diff --git a/src/strategy/druid/GenericDruidStrategy.cpp b/src/strategy/druid/GenericDruidStrategy.cpp
index ef0ac2ea..da9427a8 100644
--- a/src/strategy/druid/GenericDruidStrategy.cpp
+++ b/src/strategy/druid/GenericDruidStrategy.cpp
@@ -122,7 +122,8 @@ void GenericDruidStrategy::InitTriggers(std::vector& triggers)
triggers.push_back(new TriggerNode("combat party member dead",
NextAction::array(0, new NextAction("rebirth", ACTION_HIGH + 9), NULL)));
triggers.push_back(new TriggerNode("being attacked",
- NextAction::array(0, new NextAction("nature's grasp", ACTION_HIGH + 1), nullptr)));
+ NextAction::array(0, new NextAction("nature's grasp", ACTION_HIGH + 1), nullptr)));
+ triggers.push_back(new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
}
void DruidCureStrategy::InitTriggers(std::vector& triggers)
diff --git a/src/strategy/generic/ChatCommandHandlerStrategy.cpp b/src/strategy/generic/ChatCommandHandlerStrategy.cpp
index 8c19d31a..c4fce31e 100644
--- a/src/strategy/generic/ChatCommandHandlerStrategy.cpp
+++ b/src/strategy/generic/ChatCommandHandlerStrategy.cpp
@@ -102,9 +102,11 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger
new TriggerNode("unlock traded item", NextAction::array(0, new NextAction("unlock traded item", relevance), nullptr)));
triggers.push_back(
new TriggerNode("wipe", NextAction::array(0, new NextAction("wipe", relevance), nullptr)));
- triggers.push_back(new TriggerNode("pet", NextAction::array(0, new NextAction("pet", relevance), nullptr)));
+ triggers.push_back(new TriggerNode("tame", NextAction::array(0, new NextAction("tame", relevance), nullptr)));
triggers.push_back(new TriggerNode("glyphs", NextAction::array(0, new NextAction("glyphs", relevance), nullptr))); // Added for custom Glyphs
triggers.push_back(new TriggerNode("glyph equip", NextAction::array(0, new NextAction("glyph equip", relevance), nullptr))); // Added for custom Glyphs
+ triggers.push_back(new TriggerNode("pet", NextAction::array(0, new NextAction("pet", relevance), nullptr)));
+ triggers.push_back(new TriggerNode("pet attack", NextAction::array(0, new NextAction("pet attack", relevance), nullptr)));
triggers.push_back(new TriggerNode("roll", NextAction::array(0, new NextAction("roll", relevance), nullptr)));
}
@@ -185,7 +187,9 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("qi");
supported.push_back("unlock items");
supported.push_back("unlock traded item");
- supported.push_back("pet");
+ supported.push_back("tame");
supported.push_back("glyphs"); // Added for custom Glyphs
supported.push_back("glyph equip"); // Added for custom Glyphs
+ supported.push_back("pet");
+ supported.push_back("pet attack");
}
diff --git a/src/strategy/generic/CombatStrategy.cpp b/src/strategy/generic/CombatStrategy.cpp
index 5026d3b3..77982143 100644
--- a/src/strategy/generic/CombatStrategy.cpp
+++ b/src/strategy/generic/CombatStrategy.cpp
@@ -22,8 +22,10 @@ void CombatStrategy::InitTriggers(std::vector& triggers)
triggers.push_back(new TriggerNode("combat stuck", NextAction::array(0, new NextAction("reset", 1.0f), nullptr)));
triggers.push_back(new TriggerNode("not facing target",
NextAction::array(0, new NextAction("set facing", ACTION_MOVE + 7), nullptr)));
- triggers.push_back(
- new TriggerNode("pet attack", NextAction::array(0, new NextAction("pet attack", 40.0f), nullptr)));
+ // triggers.push_back(new TriggerNode("pet attack", NextAction::array(0, new NextAction("pet attack", 40.0f), nullptr)));
+ // The pet-attack trigger is commented out because it was forcing the bot's pet to attack, overriding stay and follow commands.
+ // Pets will automatically attack the bot's enemy if they are in "defensive" or "aggressive"
+ // stance, or if the master issues an attack command.
// triggers.push_back(new TriggerNode("combat long stuck", NextAction::array(0, new NextAction("hearthstone", 0.9f),
// new NextAction("repop", 0.8f), nullptr)));
}
diff --git a/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp b/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp
index 382b7db0..f4084922 100644
--- a/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp
+++ b/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp
@@ -61,6 +61,7 @@ void HunterPetStrategy::InitTriggers(std::vector& triggers)
{
triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("call pet", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), nullptr)));
+ triggers.push_back(new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("pet not happy", NextAction::array(0, new NextAction("feed pet", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("hunters pet medium health", NextAction::array(0, new NextAction("mend pet", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("hunters pet dead", NextAction::array(0, new NextAction("revive pet", 60.0f), nullptr)));
diff --git a/src/strategy/mage/FrostMageStrategy.cpp b/src/strategy/mage/FrostMageStrategy.cpp
index 37a4551b..a532679c 100644
--- a/src/strategy/mage/FrostMageStrategy.cpp
+++ b/src/strategy/mage/FrostMageStrategy.cpp
@@ -61,7 +61,8 @@ void FrostMageStrategy::InitTriggers(std::vector& triggers)
// Pet/Defensive triggers
triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon water elemental", 30.0f), nullptr)));
- triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 29.5f), nullptr)));
+ triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), nullptr)));
+ triggers.push_back(new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("medium health", NextAction::array(0, new NextAction("ice barrier", 29.0f), nullptr)));
triggers.push_back(new TriggerNode("being attacked", NextAction::array(0, new NextAction("ice barrier", 29.0f), nullptr)));
diff --git a/src/strategy/priest/GenericPriestStrategy.cpp b/src/strategy/priest/GenericPriestStrategy.cpp
index ce924b5a..0c14de9d 100644
--- a/src/strategy/priest/GenericPriestStrategy.cpp
+++ b/src/strategy/priest/GenericPriestStrategy.cpp
@@ -58,6 +58,7 @@ void GenericPriestStrategy::InitTriggers(std::vector& triggers)
triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("apply oil", 1.0f), nullptr)));
triggers.push_back(new TriggerNode("being attacked",
NextAction::array(0, new NextAction("power word: shield", ACTION_HIGH + 1), nullptr)));
+ triggers.push_back(new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
}
PriestCureStrategy::PriestCureStrategy(PlayerbotAI* botAI) : Strategy(botAI)
diff --git a/src/strategy/priest/PriestNonCombatStrategy.cpp b/src/strategy/priest/PriestNonCombatStrategy.cpp
index be596054..0db6e22e 100644
--- a/src/strategy/priest/PriestNonCombatStrategy.cpp
+++ b/src/strategy/priest/PriestNonCombatStrategy.cpp
@@ -56,6 +56,8 @@ void PriestNonCombatStrategy::InitTriggers(std::vector& triggers)
triggers.push_back(
new TriggerNode("group heal setting", NextAction::array(0, new NextAction("circle of healing on party", 27.0f), NULL)));
+ triggers.push_back(new TriggerNode("new pet",
+ NextAction::array(0, new NextAction("set pet stance", 10.0f), nullptr)));
}
void PriestBuffStrategy::InitTriggers(std::vector& triggers)
diff --git a/src/strategy/shaman/GenericShamanStrategy.cpp b/src/strategy/shaman/GenericShamanStrategy.cpp
index b04063e0..8f8aabe7 100644
--- a/src/strategy/shaman/GenericShamanStrategy.cpp
+++ b/src/strategy/shaman/GenericShamanStrategy.cpp
@@ -133,6 +133,8 @@ void GenericShamanStrategy::InitTriggers(std::vector& triggers)
// NextAction("riptide", 26.0f), nullptr)));
triggers.push_back(new TriggerNode("heroism", NextAction::array(0, new NextAction("heroism", 31.0f), nullptr)));
triggers.push_back(new TriggerNode("bloodlust", NextAction::array(0, new NextAction("bloodlust", 30.0f), nullptr)));
+ triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), nullptr)));
+ triggers.push_back(new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 65.0f), nullptr)));
}
void ShamanBuffDpsStrategy::InitTriggers(std::vector& triggers)
@@ -187,4 +189,4 @@ void ShamanHealerDpsStrategy::InitTriggers(std::vector& triggers)
new TriggerNode("medium aoe and healer should attack",
NextAction::array(0,
new NextAction("chain lightning", ACTION_DEFAULT + 0.3f), nullptr)));
-}
\ No newline at end of file
+}
diff --git a/src/strategy/shaman/ShamanNonCombatStrategy.cpp b/src/strategy/shaman/ShamanNonCombatStrategy.cpp
index ad4dcb82..7e6df52c 100644
--- a/src/strategy/shaman/ShamanNonCombatStrategy.cpp
+++ b/src/strategy/shaman/ShamanNonCombatStrategy.cpp
@@ -49,6 +49,10 @@ void ShamanNonCombatStrategy::InitTriggers(std::vector& triggers)
new TriggerNode("cure disease", NextAction::array(0, new NextAction("cure disease", 31.0f), nullptr)));
triggers.push_back(new TriggerNode("party member cure disease",
NextAction::array(0, new NextAction("cure disease on party", 30.0f), nullptr)));
+ triggers.push_back(
+ new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), nullptr)));
+ triggers.push_back(
+ new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 65.0f), nullptr)));
}
void ShamanNonCombatStrategy::InitMultipliers(std::vector& multipliers)
diff --git a/src/strategy/triggers/ChatTriggerContext.h b/src/strategy/triggers/ChatTriggerContext.h
index 7f294ce0..a06a94a6 100644
--- a/src/strategy/triggers/ChatTriggerContext.h
+++ b/src/strategy/triggers/ChatTriggerContext.h
@@ -133,9 +133,11 @@ public:
creators["calc"] = &ChatTriggerContext::calc;
creators["qi"] = &ChatTriggerContext::qi;
creators["wipe"] = &ChatTriggerContext::wipe;
- creators["pet"] = &ChatTriggerContext::pet;
+ creators["tame"] = &ChatTriggerContext::tame;
creators["glyphs"] = &ChatTriggerContext::glyphs; // Added for custom Glyphs
creators["glyph equip"] = &ChatTriggerContext::glyph_equip; // Added for custom Glyphs
+ creators["pet"] = &ChatTriggerContext::pet;
+ creators["pet attack"] = &ChatTriggerContext::pet_attack;
creators["roll"] = &ChatTriggerContext::roll_action;
}
@@ -249,9 +251,11 @@ private:
static Trigger* calc(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "calc"); }
static Trigger* qi(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "qi"); }
static Trigger* wipe(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "wipe"); }
- static Trigger* pet(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet"); }
+ static Trigger* tame(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "tame"); }
static Trigger* glyphs(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "glyphs"); } // Added for custom Glyphs
static Trigger* glyph_equip(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "glyph equip"); } // Added for custom Glyphs
+ static Trigger* pet(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet"); }
+ static Trigger* pet_attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet attack"); }
static Trigger* roll_action(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "roll"); }
};
diff --git a/src/strategy/triggers/GenericTriggers.cpp b/src/strategy/triggers/GenericTriggers.cpp
index c8131bd4..5f1935a9 100644
--- a/src/strategy/triggers/GenericTriggers.cpp
+++ b/src/strategy/triggers/GenericTriggers.cpp
@@ -20,6 +20,8 @@
#include "TemporarySummon.h"
#include "ThreatMgr.h"
#include "Timer.h"
+#include "PlayerbotAI.h"
+#include "Player.h"
bool LowManaTrigger::IsActive()
{
@@ -685,3 +687,46 @@ bool AmmoCountTrigger::IsActive()
return ItemCountTrigger::IsActive();
}
+
+bool NewPetTrigger::IsActive()
+{
+ // Get the bot player object from the AI
+ Player* bot = botAI->GetBot();
+ if (!bot)
+ return false;
+
+ // Try to get the current pet; initialize guardian and GUID to null/empty
+ Pet* pet = bot->GetPet();
+ Guardian* guardian = nullptr;
+ ObjectGuid currentPetGuid = ObjectGuid::Empty;
+
+ // If bot has a pet, get its GUID
+ if (pet)
+ {
+ currentPetGuid = pet->GetGUID();
+ }
+ else
+ {
+ // If no pet, try to get a guardian pet and its GUID
+ guardian = bot->GetGuardianPet();
+ if (guardian)
+ currentPetGuid = guardian->GetGUID();
+ }
+
+ // If the current pet or guardian GUID has changed (including becoming empty), reset the trigger state
+ if (currentPetGuid != lastPetGuid)
+ {
+ triggered = false;
+ lastPetGuid = currentPetGuid;
+ }
+
+ // If there's a valid current pet/guardian (non-empty GUID) and we haven't triggered yet, activate trigger
+ if (currentPetGuid != ObjectGuid::Empty && !triggered)
+ {
+ triggered = true;
+ return true;
+ }
+
+ // Otherwise, do not activate
+ return false;
+}
diff --git a/src/strategy/triggers/GenericTriggers.h b/src/strategy/triggers/GenericTriggers.h
index 96691224..d6664122 100644
--- a/src/strategy/triggers/GenericTriggers.h
+++ b/src/strategy/triggers/GenericTriggers.h
@@ -943,4 +943,16 @@ public:
bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); }
};
+class NewPetTrigger : public Trigger
+{
+public:
+ NewPetTrigger(PlayerbotAI* ai) : Trigger(ai, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {}
+
+ bool IsActive() override;
+
+private:
+ ObjectGuid lastPetGuid;
+ bool triggered;
+};
+
#endif
diff --git a/src/strategy/triggers/TriggerContext.h b/src/strategy/triggers/TriggerContext.h
index 8fcf60bc..984c1dea 100644
--- a/src/strategy/triggers/TriggerContext.h
+++ b/src/strategy/triggers/TriggerContext.h
@@ -227,6 +227,7 @@ public:
creators["do quest status"] = &TriggerContext::do_quest_status;
creators["travel flight status"] = &TriggerContext::travel_flight_status;
creators["can self resurrect"] = &TriggerContext::can_self_resurrect;
+ creators["new pet"] = &TriggerContext::new_pet;
}
private:
@@ -425,6 +426,7 @@ private:
static Trigger* do_quest_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_DO_QUEST); }
static Trigger* travel_flight_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_TRAVEL_FLIGHT); }
static Trigger* can_self_resurrect(PlayerbotAI* ai) { return new SelfResurrectTrigger(ai); }
+ static Trigger* new_pet(PlayerbotAI* ai) { return new NewPetTrigger(ai); }
};
#endif
diff --git a/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp b/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp
index 2fd3c78c..49707e14 100644
--- a/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp
+++ b/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp
@@ -79,6 +79,7 @@ void GenericWarlockNonCombatStrategy::InitTriggers(std::vector& tr
{
NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode("has pet", NextAction::array(0, new NextAction("toggle pet spell", 60.0f), nullptr)));
+ triggers.push_back(new TriggerNode("new pet", NextAction::array(0, new NextAction("set pet stance", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("fel domination", 30.0f), nullptr)));
triggers.push_back(new TriggerNode("no soul shard", NextAction::array(0, new NextAction("create soul shard", 60.0f), nullptr)));
triggers.push_back(new TriggerNode("too many soul shards", NextAction::array(0, new NextAction("destroy soul shard", 60.0f), nullptr)));