From 551c698e37b4c8b4f7e0f2715ffd735f70910ef0 Mon Sep 17 00:00:00 2001 From: ThePenguinMan96 Date: Thu, 17 Jul 2025 04:51:06 -0700 Subject: [PATCH] Hunter Pet Chat Command (#1448) Hello everyone, I was working with hunter bots and I feel like I didn't have enough control over their pets. So I started working on a chat command for the hunter pets, and it's been a blast. Below is a description of what the commands do: pet name Summons a tameable pet by its creature name (case-insensitive). The bot checks if the pet is exotic and requires the Beast Mastery talent if so. If successful, the bot announces the pet's new name and creature ID. If not found or not tameable, an error message is given. pet id Summons a tameable pet by its database creature ID. The same exotic pet checks apply. Success is announced with the pet's name and ID. Errors are given for invalid IDs or untameable pets. pet family Randomly selects and summons a tameable pet from the specified family (e.g., "cat", "wolf"). Only families with tameable pets are considered. Exotic pet checks and talent requirements apply. Success is announced with the pet's name and ID. Errors are given if no suitable pet is found. pet rename Renames the hunter's current summoned pet to a new name. The name must be 1-12 alphabetic characters (A-Z, a-z) and is automatically formatted (first letter capitalized, rest lowercased). Forbidden or reserved names are disallowed. After renaming, the bot sets the new name, saves the pet, and updates the client. The bot dismisses and attempts to recall the pet using "Call Pet" (if the hunter knows it) to update the UI. If the new name isn't visible immediately, the bot instructs the player to dismiss and recall the pet manually. Confirmation and guidance messages are provided. Additional Details: All commands display errors if requirements are not met (wrong name/id/family, forbidden name, lack of Beast Mastery, or hunter below level 10). After changing or summoning a pet (except renaming), the bot initializes the pet and its talents, then announces the result. TLDR: pet name Summon a tameable pet by name pet id Summon a tameable pet by database creature ID pet family Randomly summon a tameable pet of the given family pet rename Rename the current pet and refresh its name in the client UI Description of files changed: src\strategy\actions\ChatActionContext.h: Added the "pet" action for the whisper command. src\strategy\actions\PetAction.cpp: New chat actions for all things related to hunter pets. src\strategy\actions\PetAction.h: New header for the PetAction.cpp. src\strategy\generic\ChatCommandHandlerStrategy.cpp: Linked the trigger and action in the chatcommandhandlerstrategy, for the bot to take action when whispered "pet" (trigger). src\strategy\triggers\ChatTriggerContext.h: Added the "pet" trigger. --- src/strategy/actions/ChatActionContext.h | 3 + src/strategy/actions/PetAction.cpp | 376 ++++++++++++++++++ src/strategy/actions/PetAction.h | 34 ++ .../generic/ChatCommandHandlerStrategy.cpp | 2 + src/strategy/triggers/ChatTriggerContext.h | 2 + 5 files changed, 417 insertions(+) create mode 100644 src/strategy/actions/PetAction.cpp create mode 100644 src/strategy/actions/PetAction.h diff --git a/src/strategy/actions/ChatActionContext.h b/src/strategy/actions/ChatActionContext.h index 6b5c6c44..f0a8d3f7 100644 --- a/src/strategy/actions/ChatActionContext.h +++ b/src/strategy/actions/ChatActionContext.h @@ -78,6 +78,7 @@ #include "OpenItemAction.h" #include "UnlockItemAction.h" #include "UnlockTradedItemAction.h" +#include "PetAction.h" class ChatActionContext : public NamedObjectContext { @@ -187,6 +188,7 @@ public: creators["lfg"] = &ChatActionContext::lfg; creators["calc"] = &ChatActionContext::calc; creators["wipe"] = &ChatActionContext::wipe; + creators["pet"] = &ChatActionContext::pet; } private: @@ -293,6 +295,7 @@ 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); } }; #endif diff --git a/src/strategy/actions/PetAction.cpp b/src/strategy/actions/PetAction.cpp new file mode 100644 index 00000000..dae760a7 --- /dev/null +++ b/src/strategy/actions/PetAction.cpp @@ -0,0 +1,376 @@ +/* + * 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 "PetAction.h" +#include +#include +#include +#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); +} + +bool PetAction::Execute(Event event) +{ + 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 + + bool found = false; + + // Reset lastPetName/Id each time + lastPetName = ""; + lastPetId = 0; + + if (mode == "name" && !value.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 "); + 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); + + // Only match if names match (case-insensitive) + if (creatureName == lowerName) + { + // Check if the pet is tameable at all + if (!creature.IsTameable(true)) + continue; + + // 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())) + continue; + + lastPetName = creature.Name; + lastPetId = creature.Entry; + return CreateAndSetPet(creature.Entry); + } + } + + 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) + { + // 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); + 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) + { + botAI->TellError("You have no pet to rename."); + return false; + } + + // 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); + } + + return true; +} diff --git a/src/strategy/actions/PetAction.h b/src/strategy/actions/PetAction.h new file mode 100644 index 00000000..74009e86 --- /dev/null +++ b/src/strategy/actions/PetAction.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_PETACTION_H +#define _PLAYERBOT_PETACTION_H + +#include + +#include "Action.h" +#include "PlayerbotFactory.h" + +class PlayerbotAI; + +class PetAction : public Action +{ +public: + PetAction(PlayerbotAI* botAI) : Action(botAI, "pet") {} + + 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; +}; +#endif diff --git a/src/strategy/generic/ChatCommandHandlerStrategy.cpp b/src/strategy/generic/ChatCommandHandlerStrategy.cpp index b3367213..20ed07b8 100644 --- a/src/strategy/generic/ChatCommandHandlerStrategy.cpp +++ b/src/strategy/generic/ChatCommandHandlerStrategy.cpp @@ -102,6 +102,7 @@ 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))); } ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) @@ -181,4 +182,5 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("qi"); supported.push_back("unlock items"); supported.push_back("unlock traded item"); + supported.push_back("pet"); } diff --git a/src/strategy/triggers/ChatTriggerContext.h b/src/strategy/triggers/ChatTriggerContext.h index 9ea6770e..6979e81f 100644 --- a/src/strategy/triggers/ChatTriggerContext.h +++ b/src/strategy/triggers/ChatTriggerContext.h @@ -133,6 +133,7 @@ public: creators["calc"] = &ChatTriggerContext::calc; creators["qi"] = &ChatTriggerContext::qi; creators["wipe"] = &ChatTriggerContext::wipe; + creators["pet"] = &ChatTriggerContext::pet; } private: @@ -245,6 +246,7 @@ 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"); } }; #endif