diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 3b4a676f..7b624fe8 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -699,6 +699,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 + # # # diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 8db503ac..fd6fc7e8 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -575,6 +575,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 620288a3..0c7a3f62 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -333,6 +333,8 @@ public: bool autoPickTalents; bool autoUpgradeEquip; int32 hunterWolfPet; + int32 defaultPetStance; + int32 petChatCommandDebug; bool autoLearnTrainerSpells; bool autoDoQuests; bool enableNewRpgStrategy; diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h index 06ee6461..2beee322 100644 --- a/src/strategy/actions/ActionContext.h +++ b/src/strategy/actions/ActionContext.h @@ -244,7 +244,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; @@ -431,6 +432,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 bc111ac6..9de7c511 100644 --- a/src/strategy/actions/ChatActionContext.h +++ b/src/strategy/actions/ChatActionContext.h @@ -78,9 +78,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 { @@ -190,9 +191,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; } private: @@ -299,9 +302,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"); } }; #endif 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..277175e7 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 { @@ -100,6 +110,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 +122,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 +152,79 @@ bool PetAttackAction::Execute(Event event) pet->ToCreature()->AI()->AttackStart(target); return true; } + +bool SetPetStanceAction::Execute(Event /*event*/) +{ + // Get the bot player object from the AI + Player* bot = botAI->GetBot(); + + // 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/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..2a3c36db --- /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..daac1755 --- /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 bc897948..4ca6c15a 100644 --- a/src/strategy/druid/GenericDruidNonCombatStrategy.cpp +++ b/src/strategy/druid/GenericDruidNonCombatStrategy.cpp @@ -153,6 +153,8 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector& trig 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))); } 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 89237eed..681cfacc 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))); } ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) @@ -184,7 +186,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 203c2fea..7167cd17 100644 --- a/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp +++ b/src/strategy/hunter/GenericHunterNonCombatStrategy.cpp @@ -63,6 +63,8 @@ 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( 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 aabbf529..cd04375b 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; } private: @@ -248,9 +250,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"); } }; #endif diff --git a/src/strategy/triggers/GenericTriggers.cpp b/src/strategy/triggers/GenericTriggers.cpp index c8131bd4..a907085b 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,44 @@ bool AmmoCountTrigger::IsActive() return ItemCountTrigger::IsActive(); } + +bool NewPetTrigger::IsActive() +{ + // Get the bot player object from the AI + Player* bot = botAI->GetBot(); + + // Try to get the current pet; initialize guardian and GUID to null/empty + Pet* pet = bot ? bot->GetPet() : nullptr; + 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 ? bot->GetGuardianPet() : nullptr; + 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)));