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)));