From 57831ea133f45180a9219892ece6c2911a070f59 Mon Sep 17 00:00:00 2001 From: bo Date: Thu, 12 Mar 2026 21:23:56 -0500 Subject: [PATCH] Natural Hunterbot strategies --- src/modules/Bots/playerbot/PlayerbotFactory.h | 10 +-- .../hunter/GenericHunterNonCombatStrategy.cpp | 9 +++ .../strategy/hunter/GenericHunterStrategy.cpp | 15 ++++- .../strategy/hunter/HunterActions.cpp | 44 ++++++++++++ .../playerbot/strategy/hunter/HunterActions.h | 67 ++++++++++++++++++- .../strategy/hunter/HunterAiObjectContext.cpp | 22 ++++++ .../strategy/hunter/HunterTriggers.cpp | 10 ++- .../strategy/hunter/HunterTriggers.h | 7 ++ 8 files changed, 176 insertions(+), 8 deletions(-) diff --git a/src/modules/Bots/playerbot/PlayerbotFactory.h b/src/modules/Bots/playerbot/PlayerbotFactory.h index a3ee5ae48..f30d4f4eb 100644 --- a/src/modules/Bots/playerbot/PlayerbotFactory.h +++ b/src/modules/Bots/playerbot/PlayerbotFactory.h @@ -46,6 +46,11 @@ class PlayerbotFactory : public InventoryAction */ void Refresh(); + /** + * @brief Initializes the pet for the player bot. + */ + void InitPet(); + private: /** * @brief Randomizes the player bot with an option for incremental changes. @@ -148,11 +153,6 @@ class PlayerbotFactory : public InventoryAction */ void InitQuests(); - /** - * @brief Initializes the pet for the player bot. - */ - void InitPet(); - /** * @brief Clears the inventory of the player bot. */ diff --git a/src/modules/Bots/playerbot/strategy/hunter/GenericHunterNonCombatStrategy.cpp b/src/modules/Bots/playerbot/strategy/hunter/GenericHunterNonCombatStrategy.cpp index 0f5dc4485..1be1ee6b3 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/GenericHunterNonCombatStrategy.cpp +++ b/src/modules/Bots/playerbot/strategy/hunter/GenericHunterNonCombatStrategy.cpp @@ -13,6 +13,7 @@ class GenericHunterNonCombatStrategyActionNodeFactory : public NamedObjectFactor creators["rapid fire"] = &rapid_fire; creators["boost"] = &rapid_fire; creators["aspect of the pack"] = &aspect_of_the_pack; + creators["aspect of the viper"] = &aspect_of_the_viper; } private: static ActionNode* rapid_fire(PlayerbotAI* ai) @@ -29,6 +30,14 @@ class GenericHunterNonCombatStrategyActionNodeFactory : public NamedObjectFactor /*A*/ NextAction::array(0, new NextAction("aspect of the cheetah"), NULL), /*C*/ NULL); } + // aspect of the viper doesn't exist in Vanilla 1.12 - fall back to drinking + static ActionNode* aspect_of_the_viper(PlayerbotAI* ai) + { + return new ActionNode ("aspect of the viper", + /*P*/ NULL, + /*A*/ NextAction::array(0, new NextAction("drink"), NULL), + /*C*/ NULL); + } }; GenericHunterNonCombatStrategy::GenericHunterNonCombatStrategy(PlayerbotAI* ai) : NonCombatStrategy(ai) diff --git a/src/modules/Bots/playerbot/strategy/hunter/GenericHunterStrategy.cpp b/src/modules/Bots/playerbot/strategy/hunter/GenericHunterStrategy.cpp index 8c404fc3c..db89efc8a 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/GenericHunterStrategy.cpp +++ b/src/modules/Bots/playerbot/strategy/hunter/GenericHunterStrategy.cpp @@ -50,7 +50,15 @@ void GenericHunterStrategy::InitTriggers(std::list &triggers) triggers.push_back(new TriggerNode( "enemy too close for spell", - NextAction::array(0, new NextAction("wing clip", 50.0f), new NextAction("flee",49.0f), new NextAction("concussive shot", 48.0f), NULL))); + NextAction::array(0, + new NextAction("intimidation", 52.0f), + new NextAction("wing clip", 51.0f), + new NextAction("hunter ensure ranged position", 50.0f), + new NextAction("mongoose bite", 49.5f), + new NextAction("disengage", 49.0f), + new NextAction("hunter melee", 48.5f), + new NextAction("flee", 48.0f), + NULL))); triggers.push_back(new TriggerNode( "medium threat", @@ -63,4 +71,9 @@ void GenericHunterStrategy::InitTriggers(std::list &triggers) triggers.push_back(new TriggerNode( "rapid fire", NextAction::array(0, new NextAction("rapid fire", 55.0f), NULL))); + + triggers.push_back(new TriggerNode( + "bestial wrath", + NextAction::array(0, new NextAction("bestial wrath", 55.0f), NULL))); + } diff --git a/src/modules/Bots/playerbot/strategy/hunter/HunterActions.cpp b/src/modules/Bots/playerbot/strategy/hunter/HunterActions.cpp index 5b0e78edf..66205b119 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/HunterActions.cpp +++ b/src/modules/Bots/playerbot/strategy/hunter/HunterActions.cpp @@ -2,6 +2,7 @@ #include "../../playerbot.h" #include "../actions/GenericActions.h" #include "HunterActions.h" +#include "../../PlayerbotFactory.h" using namespace ai; @@ -24,3 +25,46 @@ Value* CastFreezingTrap::GetTargetValue() { return context->GetValue("cc target", "freezing trap"); } + +bool CastRevivePetAction::isPossible() +{ + if (bot->GetPet()) + return CastBuffSpellAction::isPossible(); + PetDatabaseStatus status = Pet::GetStatusFromDB(bot); + return status == PET_DB_DEAD || status == PET_DB_NO_PET; +} + +bool CastRevivePetAction::Execute(Event event) +{ + if (!bot->GetPet() && Pet::GetStatusFromDB(bot) == PET_DB_NO_PET) + { + PlayerbotFactory factory(bot, bot->getLevel()); + factory.InitPet(); + return true; + } + return CastBuffSpellAction::Execute(event); +} + +bool CastIntimidationAction::isUseful() +{ + return CastSpellAction::isUseful() && AI_VALUE(Unit*, "pet target") != NULL; +} + +bool HunterMeleeAction::isUseful() +{ + // Only swing if enemy is already in our face AND targeting us. + // Perhaps in the future a ranged/melee hunter strategy would be nice. + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || !target->IsAlive()) return false; + bool victim = target->getVictim() == bot; + float dist = AI_VALUE2(float, "distance", "current target"); + return victim && dist <= ATTACK_DISTANCE; +} + +bool HunterMeleeAction::Execute(Event event) +{ + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) return false; + bot->Attack(target, true); + return true; +} diff --git a/src/modules/Bots/playerbot/strategy/hunter/HunterActions.h b/src/modules/Bots/playerbot/strategy/hunter/HunterActions.h index ad4ed659f..263292b36 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/HunterActions.h +++ b/src/modules/Bots/playerbot/strategy/hunter/HunterActions.h @@ -87,6 +87,8 @@ namespace ai { public: CastRevivePetAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "revive pet") {} + virtual bool isPossible(); + virtual bool Execute(Event event); }; class CastTrueshotAuraAction : public CastBuffSpellAction @@ -120,7 +122,8 @@ namespace ai CastWingClipAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "wing clip") {} virtual bool isUseful() { - return CastMeleeSpellAction::isUseful() && !ai->HasAura(spell, GetTarget()); + Unit* target = GetTarget(); + return target && target->IsAlive() && CastMeleeSpellAction::isUseful() && !ai->HasAura(spell, target); } }; @@ -129,4 +132,66 @@ namespace ai public: CastSerpentStingOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "serpent sting") {} }; + + BEGIN_MELEE_SPELL_ACTION(CastDisengageAction, "disengage") + END_SPELL_ACTION() + + BEGIN_MELEE_SPELL_ACTION(CastImmolationTrapAction, "immolation trap") + END_SPELL_ACTION() + + BEGIN_MELEE_SPELL_ACTION(CastFrostTrapAction, "frost trap") + END_SPELL_ACTION() + + BEGIN_MELEE_SPELL_ACTION(CastExplosiveTrapAction, "explosive trap") + END_SPELL_ACTION() + + BEGIN_RANGED_SPELL_ACTION(CastScatterShotAction, "scatter shot") + END_SPELL_ACTION() + + class CastBestialWrathAction : public CastAuraSpellAction + { + public: + CastBestialWrathAction(PlayerbotAI* ai) : CastAuraSpellAction(ai, "bestial wrath") {} + virtual string GetTargetName() { return "pet target"; } + virtual bool isUseful() { return CastAuraSpellAction::isUseful() && AI_VALUE(Unit*, "pet target") != NULL; } + }; + + class CastMongooseBiteAction : public CastMeleeSpellAction + { + public: + CastMongooseBiteAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "mongoose bite") {} + virtual bool isPossible() { return bot->HasAuraState(AURA_STATE_DEFENSE) && CastMeleeSpellAction::isPossible(); } + }; + + class CastIntimidationAction : public CastSpellAction + { + public: + CastIntimidationAction(PlayerbotAI* ai) : CastSpellAction(ai, "intimidation") {} + virtual bool isUseful(); + }; + + class HunterMeleeAction : public Action + { + public: + HunterMeleeAction(PlayerbotAI* ai) : Action(ai, "hunter melee") {} + virtual bool Execute(Event event); + virtual bool isUseful(); + }; + + class HunterEnsureRangedPositionAction : public MovementAction + { + public: + HunterEnsureRangedPositionAction(PlayerbotAI* ai) : MovementAction(ai, "hunter ensure ranged position") {} + virtual bool Execute(Event event) + { + return MoveTo(AI_VALUE(Unit*, "current target"), sPlayerbotAIConfig.spellDistance); + } + virtual bool isUseful() + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || !target->IsAlive()) return false; + return target->getVictim() != bot && + bot->GetDistance(target) < sPlayerbotAIConfig.spellDistance; + } + }; } diff --git a/src/modules/Bots/playerbot/strategy/hunter/HunterAiObjectContext.cpp b/src/modules/Bots/playerbot/strategy/hunter/HunterAiObjectContext.cpp index 076d1ea10..d34405d47 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/HunterAiObjectContext.cpp +++ b/src/modules/Bots/playerbot/strategy/hunter/HunterAiObjectContext.cpp @@ -76,6 +76,7 @@ namespace ai creators["freezing trap"] = &TriggerFactoryInternal::freezing_trap; creators["aspect of the pack"] = &TriggerFactoryInternal::aspect_of_the_pack; creators["rapid fire"] = &TriggerFactoryInternal::rapid_fire; + creators["bestial wrath"] = &TriggerFactoryInternal::bestial_wrath; creators["aspect of the hawk"] = &TriggerFactoryInternal::aspect_of_the_hawk; creators["aspect of the wild"] = &TriggerFactoryInternal::aspect_of_the_wild; creators["aspect of the viper"] = &TriggerFactoryInternal::aspect_of_the_viper; @@ -95,6 +96,7 @@ namespace ai static Trigger* freezing_trap(PlayerbotAI* ai) { return new FreezingTrapTrigger(ai); } static Trigger* aspect_of_the_pack(PlayerbotAI* ai) { return new HunterAspectOfThePackTrigger(ai); } static Trigger* rapid_fire(PlayerbotAI* ai) { return new RapidFireTrigger(ai); } + static Trigger* bestial_wrath(PlayerbotAI* ai) { return new BestialWrathTrigger(ai); } static Trigger* aspect_of_the_hawk(PlayerbotAI* ai) { return new HunterAspectOfTheHawkTrigger(ai); } static Trigger* aspect_of_the_wild(PlayerbotAI* ai) { return new HunterAspectOfTheWildTrigger(ai); } }; @@ -141,6 +143,16 @@ namespace ai creators["trueshot aura"] = &AiObjectContextInternal::trueshot_aura; creators["feign death"] = &AiObjectContextInternal::feign_death; creators["wing clip"] = &AiObjectContextInternal::wing_clip; + creators["disengage"] = &AiObjectContextInternal::disengage; + creators["immolation trap"] = &AiObjectContextInternal::immolation_trap; + creators["frost trap"] = &AiObjectContextInternal::frost_trap; + creators["explosive trap"] = &AiObjectContextInternal::explosive_trap; + creators["scatter shot"] = &AiObjectContextInternal::scatter_shot; + creators["bestial wrath"] = &AiObjectContextInternal::bestial_wrath; + creators["mongoose bite"] = &AiObjectContextInternal::mongoose_bite; + creators["intimidation"] = &AiObjectContextInternal::intimidation; + creators["hunter melee"] = &AiObjectContextInternal::hunter_melee; + creators["hunter ensure ranged position"] = &AiObjectContextInternal::hunter_ensure_ranged_position; } private: @@ -167,6 +179,16 @@ namespace ai static Action* rapid_fire(PlayerbotAI* ai) { return new CastRapidFireAction(ai); } static Action* aspect_of_the_hawk(PlayerbotAI* ai) { return new CastAspectOfTheHawkAction(ai); } static Action* aspect_of_the_wild(PlayerbotAI* ai) { return new CastAspectOfTheWildAction(ai); } + static Action* disengage(PlayerbotAI* ai) { return new CastDisengageAction(ai); } + static Action* immolation_trap(PlayerbotAI* ai) { return new CastImmolationTrapAction(ai); } + static Action* frost_trap(PlayerbotAI* ai) { return new CastFrostTrapAction(ai); } + static Action* explosive_trap(PlayerbotAI* ai) { return new CastExplosiveTrapAction(ai); } + static Action* scatter_shot(PlayerbotAI* ai) { return new CastScatterShotAction(ai); } + static Action* bestial_wrath(PlayerbotAI* ai) { return new CastBestialWrathAction(ai); } + static Action* mongoose_bite(PlayerbotAI* ai) { return new CastMongooseBiteAction(ai); } + static Action* intimidation(PlayerbotAI* ai) { return new CastIntimidationAction(ai); } + static Action* hunter_melee(PlayerbotAI* ai) { return new HunterMeleeAction(ai); } + static Action* hunter_ensure_ranged_position(PlayerbotAI* ai) { return new HunterEnsureRangedPositionAction(ai); } static Action* aspect_of_the_pack(PlayerbotAI* ai) { return new CastAspectOfThePackAction(ai); } static Action* aspect_of_the_cheetah(PlayerbotAI* ai) { return new CastAspectOfTheCheetahAction(ai); } static Action* wing_clip(PlayerbotAI* ai) { return new CastWingClipAction(ai); } diff --git a/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.cpp b/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.cpp index 21df6ceed..2cb04cea9 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.cpp +++ b/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.cpp @@ -16,8 +16,16 @@ bool HunterNoStingsActiveTrigger::IsActive() bool HuntersPetDeadTrigger::IsActive() { + if (AI_VALUE2(bool, "mounted", "self target")) + return false; + Unit* pet = AI_VALUE(Unit*, "pet target"); - return pet && AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); + if (pet) + return AI_VALUE2(bool, "dead", "pet target"); + + // Pet not in world — check DB to catch the common case where the corpse timer has already expired + PetDatabaseStatus status = Pet::GetStatusFromDB(bot); + return status == PET_DB_DEAD || status == PET_DB_NO_PET; } diff --git a/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.h b/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.h index 947426c85..065092a11 100644 --- a/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.h +++ b/src/modules/Bots/playerbot/strategy/hunter/HunterTriggers.h @@ -72,6 +72,12 @@ namespace ai RapidFireTrigger(PlayerbotAI* ai) : BoostTrigger(ai, "rapid fire") {} }; + class BestialWrathTrigger : public BoostTrigger + { + public: + BestialWrathTrigger(PlayerbotAI* ai) : BoostTrigger(ai, "bestial wrath") {} + }; + class TrueshotAuraTrigger : public BuffTrigger { public: @@ -83,4 +89,5 @@ namespace ai public: SerpentStingOnAttackerTrigger(PlayerbotAI* ai) : DebuffOnAttackerTrigger(ai, "serpent sting") {} }; + }