From 76dd91c4fafdb6a690cf2db7da0036299c0a9d09 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 3 Apr 2026 22:25:29 +0200 Subject: [PATCH] Hand of Freedom support (#2233) ## Pull Request Description Added Hand of Freedom action for paladin. Related with: #2002 ## How to Test the Changes - invite paladin bot to party - start fight (can use dummy target) - apply some snare effect to bot or yourself (for example `.aura 1715`) - bot should use hand of freedom ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Yes, paladin bots start using Hand of Freedom - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate - Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance - Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) OpenCode, as helper to create and review code ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers obraz --- .../Value/PartyMemberSnaredTargetValue.cpp | 64 +++++++++++++++++++ .../Base/Value/PartyMemberSnaredTargetValue.h | 24 +++++++ src/Ai/Base/ValueContext.h | 3 + .../Class/Paladin/Action/PaladinActions.cpp | 34 ++++++++++ src/Ai/Class/Paladin/Action/PaladinActions.h | 13 ++++ .../Class/Paladin/PaladinAiObjectContext.cpp | 4 ++ .../Strategy/GenericPaladinStrategy.cpp | 3 + .../Class/Paladin/Trigger/PaladinTriggers.cpp | 35 ++++++++++ .../Class/Paladin/Trigger/PaladinTriggers.h | 10 +++ src/Ai/Class/Paladin/Util/PaladinHelper.h | 43 +++++++++++++ src/Bot/PlayerbotAI.cpp | 5 ++ src/Bot/PlayerbotAI.h | 1 + 12 files changed, 239 insertions(+) create mode 100644 src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp create mode 100644 src/Ai/Base/Value/PartyMemberSnaredTargetValue.h create mode 100644 src/Ai/Class/Paladin/Util/PaladinHelper.h diff --git a/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp new file mode 100644 index 000000000..02f726b0f --- /dev/null +++ b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "PartyMemberSnaredTargetValue.h" + +#include + +#include "PlayerbotAIAware.h" +#include "Playerbots.h" + +class PartyMemberSnaredTargetPredicate : public FindPlayerPredicate, public PlayerbotAIAware +{ +public: + PartyMemberSnaredTargetPredicate(PlayerbotAI* botAI) + : PlayerbotAIAware(botAI) + { + } + + bool Check(Unit* unit) override + { + if (!unit || !unit->IsAlive() || !unit->IsInWorld() || unit == botAI->GetBot()) + return false; + + if (unit->GetMapId() != botAI->GetBot()->GetMapId()) + return false; + + if (!botAI->GetBot()->IsWithinLOSInMap(unit)) + return false; + + return botAI->IsMovementImpaired(unit); + } +}; + +Unit* PartyMemberSnaredTargetValue::Calculate() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + PartyMemberSnaredTargetPredicate predicate(botAI); + Player* bestTarget = nullptr; + float closestDistanceSq = std::numeric_limits::max(); + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member) + continue; + + if (!predicate.Check(member)) + continue; + + float const distanceSq = bot->GetExactDist2dSq(member->GetPositionX(), member->GetPositionY()); + if (distanceSq < closestDistanceSq) + { + closestDistanceSq = distanceSq; + bestTarget = member; + } + } + + return bestTarget; +} diff --git a/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h new file mode 100644 index 000000000..d7b517e38 --- /dev/null +++ b/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H +#define _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H + +#include "NamedObjectContext.h" +#include "PartyMemberValue.h" + +class PartyMemberSnaredTargetValue : public PartyMemberValue +{ +public: + PartyMemberSnaredTargetValue(PlayerbotAI* botAI, std::string const name = "party member snared target") + : PartyMemberValue(botAI, name) + { + } + +protected: + Unit* Calculate() override; +}; + +#endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index d1b07b88c..14cbd185c 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -66,6 +66,7 @@ #include "PartyMemberToDispel.h" #include "PartyMemberToHeal.h" #include "PartyMemberToResurrect.h" +#include "PartyMemberSnaredTargetValue.h" #include "PartyMemberWithoutAuraValue.h" #include "PartyMemberWithoutItemValue.h" #include "PetTargetValue.h" @@ -152,6 +153,7 @@ public: creators["duel target"] = &ValueContext::duel_target; creators["party member to dispel"] = &ValueContext::party_member_to_dispel; creators["party member to protect"] = &ValueContext::party_member_to_protect; + creators["party member snared target"] = &ValueContext::party_member_snared_target; creators["health"] = &ValueContext::health; creators["rage"] = &ValueContext::rage; creators["energy"] = &ValueContext::energy; @@ -450,6 +452,7 @@ private: static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); } static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); } static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); } + static UntypedValue* party_member_snared_target(PlayerbotAI* botAI) { return new PartyMemberSnaredTargetValue(botAI); } static UntypedValue* current_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* old_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* self_target(PlayerbotAI* botAI) { return new SelfTargetValue(botAI); } diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index 944b6e686..38e17af1e 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -7,6 +7,7 @@ #include "AiFactory.h" #include "Event.h" +#include "PaladinHelper.h" #include "PlayerbotAI.h" #include "Playerbots.h" #include "SharedDefines.h" @@ -468,6 +469,39 @@ bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self ta Value* CastTurnUndeadAction::GetTargetValue() { return context->GetValue("cc target", getName()); } +Unit* CastHandOfFreedomOnPartyAction::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return CastBuffSpellAction::GetTarget(); +} + +Value* CastHandOfFreedomOnPartyAction::GetTargetValue() +{ + return context->GetValue("party member snared target"); +} + +bool CastHandOfFreedomOnPartyAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); +} + Unit* CastRighteousDefenseAction::GetTarget() { Unit* current_target = AI_VALUE(Unit*, "current target"); diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.h b/src/Ai/Class/Paladin/Action/PaladinActions.h index c58c3209d..75b0637a4 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/src/Ai/Class/Paladin/Action/PaladinActions.h @@ -371,6 +371,19 @@ public: Value* GetTargetValue() override; }; +class CastHandOfFreedomOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport +{ +public: + CastHandOfFreedomOnPartyAction(PlayerbotAI* botAI) + : CastBuffSpellAction(botAI, "hand of freedom"), PartyMemberActionNameSupport("hand of freedom") {} + + Unit* GetTarget() override; + Value* GetTargetValue() override; + std::string const GetTargetName() override { return "party member snared target"; } + std::string const getName() override { return PartyMemberActionNameSupport::getName(); } + bool isUseful() override; +}; + PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection"); class CastDivinePleaAction : public CastBuffSpellAction diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index ed8f4931b..3c92e57c3 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -142,6 +142,7 @@ public: creators["repentance interrupt"] = &PaladinTriggerFactoryInternal::repentance_interrupt; creators["beacon of light on main tank"] = &PaladinTriggerFactoryInternal::beacon_of_light_on_main_tank; creators["sacred shield on main tank"] = &PaladinTriggerFactoryInternal::sacred_shield_on_main_tank; + creators["hand of freedom on party"] = &PaladinTriggerFactoryInternal::hand_of_freedom_on_party; creators["blessing of kings on party"] = &PaladinTriggerFactoryInternal::blessing_of_kings_on_party; creators["blessing of wisdom on party"] = &PaladinTriggerFactoryInternal::blessing_of_wisdom_on_party; @@ -207,6 +208,7 @@ private: static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); } static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); } static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); } + static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); } static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); } static Trigger* blessing_of_wisdom_on_party(PlayerbotAI* botAI) @@ -308,6 +310,7 @@ public: creators["divine illumination"] = &PaladinAiObjectContextInternal::divine_illumination; creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice; creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice; + creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party; } private: @@ -414,6 +417,7 @@ private: static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); } static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); } static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); } + static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); } }; SharedNamedObjectContextList PaladinAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index 49143f9ae..f03207216 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -35,6 +35,9 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "protect party member", { NextAction("blessing of protection on party", ACTION_EMERGENCY + 2) })); + triggers.push_back(new TriggerNode( + "hand of freedom on party", + { NextAction("hand of freedom on party", ACTION_HIGH + 4) })); triggers.push_back( new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); } diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index 71328c4dc..e3367aaef 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -8,6 +8,7 @@ #include "PaladinActions.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "PaladinHelper.h" bool SealTrigger::IsActive() { @@ -31,6 +32,40 @@ bool BlessingTrigger::IsActive() "blessing of kings", "blessing of sanctuary", nullptr); } +Unit* HandOfFreedomOnPartyTrigger::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return Trigger::GetTarget(); +} + +bool HandOfFreedomOnPartyTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) + return false; + + if (!botAI->CanCastSpell("hand of freedom", target)) + return false; + + return !ai::paladin::HasAnyPaladinHandFromCaster(target, bot) && botAI->IsMovementImpaired(target); +} + bool NotSensingUndeadTrigger::IsActive() { return !botAI->HasAura("sense undead", bot); diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index f33b66890..cc6ceddcf 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -242,6 +242,16 @@ public: : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} }; +class HandOfFreedomOnPartyTrigger : public Trigger +{ +public: + HandOfFreedomOnPartyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "hand of freedom on party", 1) {} + + Unit* GetTarget() override; + std::string const GetTargetName() override { return "party member snared target"; } + bool IsActive() override; +}; + class AvengingWrathTrigger : public BoostTrigger { public: diff --git a/src/Ai/Class/Paladin/Util/PaladinHelper.h b/src/Ai/Class/Paladin/Util/PaladinHelper.h new file mode 100644 index 000000000..64b88b731 --- /dev/null +++ b/src/Ai/Class/Paladin/Util/PaladinHelper.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PALADINHELPER_H +#define _PLAYERBOT_PALADINHELPER_H + +#include + +#include "Unit.h" + +class Player; + +namespace ai::paladin +{ +static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022; +static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038; +static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044; +static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940; + +inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list spellIds) +{ + if (!target || !caster) + return false; + + for (uint32 spellId : spellIds) + { + if (target->HasAura(spellId, caster->GetGUID())) + return true; + } + + return false; +} + +inline bool HasAnyPaladinHandFromCaster(Unit* target, Player* caster) +{ + return HasHandFromCaster(target, caster, + { SPELL_HAND_OF_PROTECTION, SPELL_HAND_OF_SALVATION, SPELL_HAND_OF_FREEDOM, SPELL_HAND_OF_SACRIFICE }); +} +} + +#endif diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 4a746009a..446ca8c40 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1962,6 +1962,11 @@ bool PlayerbotAI::HasAggro(Unit* unit) return false; } +bool PlayerbotAI::IsMovementImpaired(Unit* unit) +{ + return unit && (unit->HasAuraType(SPELL_AURA_MOD_ROOT) || unit->IsRooted() || unit->GetSpeedRate(MOVE_RUN) < 1.0f); +} + int32 PlayerbotAI::GetAssistTankIndex(Player* player) { Group* group = player->GetGroup(); diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 5d64bf159..9c417561f 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -430,6 +430,7 @@ public: static bool IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); static bool IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); bool HasAggro(Unit* unit); + bool IsMovementImpaired(Unit* unit); static int32 GetAssistTankIndex(Player* player); int32 GetGroupSlotIndex(Player* player); int32 GetRangedIndex(Player* player);