diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 967c7152..0cee5ab5 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -21,6 +21,7 @@ # THRESHOLDS # QUESTS # COMBAT +# PALADIN BUFFS STRATEGIES # CHEATS # SPELLS # FLIGHTPATH @@ -457,6 +458,24 @@ AiPlayerbot.FleeingEnabled = 1 # #################################################################################################### +#################################################################################################### +# PALADIN BUFFS STRATEGIES +# +# + +# Min group size to use Greater buffs (Paladin, Mage, Druid) +# Default: 3 +AiPlayerbot.MinBotsForGreaterBuff = 3 + +# Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff +# Default: 30 +AiPlayerbot.RPWarningCooldown = 30 + +# +# +# +#################################################################################################### + #################################################################################################### # CHEATS # diff --git a/data/sql/playerbots/updates/2025_09_17_00_paladin_buff_reagent_texts.sql b/data/sql/playerbots/updates/2025_09_17_00_paladin_buff_reagent_texts.sql new file mode 100644 index 00000000..0daf2a0b --- /dev/null +++ b/data/sql/playerbots/updates/2025_09_17_00_paladin_buff_reagent_texts.sql @@ -0,0 +1,35 @@ +DELETE FROM ai_playerbot_texts +WHERE name IN ( + 'rp_missing_reagent_greater_blessing', + 'rp_missing_reagent_gift_of_the_wild', + 'rp_missing_reagent_arcane_brilliance', + 'rp_missing_reagent_generic' +); + +DELETE FROM ai_playerbot_texts_chance +WHERE name IN ( + 'rp_missing_reagent_greater_blessing', + 'rp_missing_reagent_gift_of_the_wild', + 'rp_missing_reagent_arcane_brilliance', + 'rp_missing_reagent_generic' +); + +INSERT INTO ai_playerbot_texts (name, text, say_type, reply_type, text_loc2) VALUES + ('rp_missing_reagent_greater_blessing', + 'By the Light... I forgot my Symbols of Kings. We’ll make do with %base_spell!', 0, 0, + 'Par la Lumière... J''ai oublié mes Symboles du roi. On se contentera de %base_spell !'), + ('rp_missing_reagent_gift_of_the_wild', + 'Nature is generous, my bags are not... out of herbs for %group_spell. Take %base_spell for now!', 0, 0, + 'La nature est généreuse, pas mes sacs... plus d''herbes pour %group_spell. Prenez %base_spell pour l''instant !'), + ('rp_missing_reagent_arcane_brilliance', + 'Out of Arcane Powder... %group_spell will have to wait. Casting %base_spell!', 0, 0, + 'Plus de poudre des arcanes... %group_spell attendra. Je lance %base_spell !'), + ('rp_missing_reagent_generic', + 'Oops, I’m out of components for %group_spell. We’ll go with %base_spell!', 0, 0, + 'Oups, je n''ai plus de composants pour %group_spell. On fera avec %base_spell !'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES + ('rp_missing_reagent_greater_blessing', 100), + ('rp_missing_reagent_gift_of_the_wild', 100), + ('rp_missing_reagent_arcane_brilliance', 100), + ('rp_missing_reagent_generic', 100); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index d8662ed1..4e1763e5 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -81,6 +81,9 @@ bool PlayerbotAIConfig::Initialize() sitDelay = sConfigMgr->GetOption("AiPlayerbot.SitDelay", 30000); returnDelay = sConfigMgr->GetOption("AiPlayerbot.ReturnDelay", 7000); lootDelay = sConfigMgr->GetOption("AiPlayerbot.LootDelay", 1000); + // Buff system + minBotsForGreaterBuff = sConfigMgr->GetOption("AiPlayerbot.MinBotsForGreaterBuff", 3); + rpWarningCooldown = sConfigMgr->GetOption("AiPlayerbot.RPWarningCooldown", 30); disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30); disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index db85e538..13427698 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -139,6 +139,12 @@ public: uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay; bool randomBotJoinLfg; + // Buff system + // Min group size to use Greater buffs (Paladin, Mage, Druid). Default: 3 + int32 minBotsForGreaterBuff; + // Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff. Default: 30 + int32 rpWarningCooldown; + // chat bool randomBotTalk; bool randomBotEmote; diff --git a/src/strategy/actions/GenericBuffUtils.cpp b/src/strategy/actions/GenericBuffUtils.cpp new file mode 100644 index 00000000..bbd0a853 --- /dev/null +++ b/src/strategy/actions/GenericBuffUtils.cpp @@ -0,0 +1,153 @@ +/* + * 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 "GenericBuffUtils.h" +#include "PlayerbotAIConfig.h" + +#include + +#include "Player.h" +#include "Group.h" +#include "SpellMgr.h" +#include "Chat.h" +#include "PlayerbotAI.h" +#include "ServerFacade.h" +#include "AiObjectContext.h" +#include "Value.h" +#include "Config.h" +#include "PlayerbotTextMgr.h" + +namespace ai::buff +{ + std::string MakeAuraQualifierForBuff(std::string const& name) + { + // Paladin + if (name == "blessing of kings") return "blessing of kings,greater blessing of kings"; + if (name == "blessing of might") return "blessing of might,greater blessing of might"; + if (name == "blessing of wisdom") return "blessing of wisdom,greater blessing of wisdom"; + if (name == "blessing of sanctuary") return "blessing of sanctuary,greater blessing of sanctuary"; + // Druid + if (name == "mark of the wild") return "mark of the wild,gift of the wild"; + // Mage + if (name == "arcane intellect") return "arcane intellect,arcane brilliance"; + // Priest + if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude"; + return name; + } + + std::string GroupVariantFor(std::string const& name) + { + // Paladin + if (name == "blessing of kings") return "greater blessing of kings"; + if (name == "blessing of might") return "greater blessing of might"; + if (name == "blessing of wisdom") return "greater blessing of wisdom"; + if (name == "blessing of sanctuary") return "greater blessing of sanctuary"; + // Druid + if (name == "mark of the wild") return "gift of the wild"; + // Mage + if (name == "arcane intellect") return "arcane brilliance"; + // Priest + if (name == "power word: fortitude") return "prayer of fortitude"; + + return std::string(); + } + + bool HasRequiredReagents(Player* bot, uint32 spellId) + { + if (!spellId) + return false; + + if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId)) + { for (int i = 0; i < MAX_SPELL_REAGENTS; ++i) + { + if (info->Reagent[i] > 0) + { + uint32 const itemId = info->Reagent[i]; + int32 const need = info->ReagentCount[i]; + if ((int32)bot->GetItemCount(itemId, false) < need) + return false; + } + } + // No reagent required + return true; + } + return false; + } + + std::string UpgradeToGroupIfAppropriate( + Player* bot, + PlayerbotAI* botAI, + std::string const& baseName, + bool announceOnMissing, + std::function announce) + { + std::string castName = baseName; Group* g = bot->GetGroup(); + if (!g || g->GetMembersCount() < static_cast(sPlayerbotAIConfig->minBotsForGreaterBuff)) + return castName; // Group too small: stay in solo mode + + if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty()) + { + uint32 const groupVariantSpellId = botAI->GetAiObjectContext() + ->GetValue("spell id", groupName)->Get(); + + // We check usefulness on the **basic** buff (not the greater version), + // because "spell cast useful" may return false for the greater variant. + bool const usefulBase = botAI->GetAiObjectContext() + ->GetValue("spell cast useful", baseName)->Get(); + + if (groupVariantSpellId && HasRequiredReagents(bot, groupVariantSpellId)) + { + // Learned + reagents OK -> switch to greater + return groupName; + } + + // Missing reagents -> announce if (a) greater is known, (b) base buff is useful, + // (c) announce was requested, (d) a callback is provided. + if (announceOnMissing && groupVariantSpellId && usefulBase && announce) + { + static std::map, time_t> s_lastWarn; // par bot & par buff + time_t now = std::time(nullptr); + uint32 botLow = static_cast(bot->GetGUID().GetCounter()); + time_t& last = s_lastWarn[ std::make_pair(botLow, groupName) ]; + if (!last || now - last >= sPlayerbotAIConfig->rpWarningCooldown) // Configurable anti-spam + { + // DB Key choice in regard of the buff + std::string key; + if (groupName.find("greater blessing") != std::string::npos) + key = "rp_missing_reagent_greater_blessing"; + else if (groupName == "gift of the wild") + key = "rp_missing_reagent_gift_of_the_wild"; + else if (groupName == "arcane brilliance") + key = "rp_missing_reagent_arcane_brilliance"; + else + key = "rp_missing_reagent_generic"; + + // Placeholders + std::map ph; + ph["%group_spell"] = groupName; + ph["%base_spell"] = baseName; + + // Respecte ai_playerbot_texts_chance if present + std::string rp; + bool got = sPlayerbotTextMgr->GetBotText(key, rp, ph); + if (got && !rp.empty()) + { + announce(rp); + last = now; + } + else + { + // Minimal Fallback + std::ostringstream oss; + oss << "Out of components for " << groupName << ". Using " << baseName << "!"; + announce(oss.str()); + last = now; + } + } + } + } + return castName; + } +} diff --git a/src/strategy/actions/GenericBuffUtils.h b/src/strategy/actions/GenericBuffUtils.h new file mode 100644 index 00000000..a3d95d7e --- /dev/null +++ b/src/strategy/actions/GenericBuffUtils.h @@ -0,0 +1,64 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include "Common.h" +#include "Group.h" +#include "Chat.h" +#include "Language.h" + +class Player; +class PlayerbotAI; + +namespace ai::buff +{ + + // Build an aura qualifier "single + greater" to avoid double-buffing + std::string MakeAuraQualifierForBuff(std::string const& name); + + // Returns the group spell name for a given single-target buff. + // If no group equivalent exists, returns "". + std::string GroupVariantFor(std::string const& name); + + // Checks if the bot has the required reagents to cast a spell (by its spellId). + // Returns false if the spellId is invalid. + bool HasRequiredReagents(Player* bot, uint32 spellId); + + + // Applies the "switch to group buff" policy if: the bot is in a group of size x+, + // the group variant is known/useful, and reagents are available. Otherwise, returns baseName. + // If announceOnMissing == true and reagents are missing, calls the 'announce' callback + // (if provided) to notify the party/raid. + std::string UpgradeToGroupIfAppropriate( + Player* bot, + PlayerbotAI* botAI, + std::string const& baseName, + bool announceOnMissing = false, + std::function announce = {} + ); +} + +namespace ai::chat { + inline std::function MakeGroupAnnouncer(Player* me) + { + return [me](std::string const& msg) + { + if (Group* g = me->GetGroup()) + { + WorldPacket data; + ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY; + ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str()); + g->BroadcastPacket(&data, true, -1, me->GetGUID()); + } + else + { + me->Say(msg, LANG_UNIVERSAL); + } + }; + } +} diff --git a/src/strategy/actions/GenericSpellActions.cpp b/src/strategy/actions/GenericSpellActions.cpp index f25fa408..691a3e89 100644 --- a/src/strategy/actions/GenericSpellActions.cpp +++ b/src/strategy/actions/GenericSpellActions.cpp @@ -5,6 +5,8 @@ #include "GenericSpellActions.h" +#include + #include "Event.h" #include "ItemTemplate.h" #include "ObjectDefines.h" @@ -13,6 +15,14 @@ #include "Playerbots.h" #include "ServerFacade.h" #include "WorldPacket.h" +#include "Group.h" +#include "Chat.h" +#include "Language.h" +#include "GenericBuffUtils.h" +#include "PlayerbotAI.h" + +using ai::buff::MakeAuraQualifierForBuff; +using ai::buff::UpgradeToGroupIfAppropriate; CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) @@ -216,11 +226,24 @@ Value* CurePartyMemberAction::GetTargetValue() return context->GetValue("party member to dispel", dispelType); } +// Make Bots Paladin, druid, mage use the greater buff rank spell +// TODO Priest doen't verify il he have components Value* BuffOnPartyAction::GetTargetValue() { - return context->GetValue("party member without aura", spell); + return context->GetValue("party member without aura", MakeAuraQualifierForBuff(spell)); } +bool BuffOnPartyAction::Execute(Event event) +{ + std::string castName = spell; // default = mono + + auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot); + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP); + + return botAI->CastSpell(castName, GetTarget()); +} +// End greater buff fix + CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot") { if (Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) diff --git a/src/strategy/actions/GenericSpellActions.h b/src/strategy/actions/GenericSpellActions.h index daf361b1..fbec0f02 100644 --- a/src/strategy/actions/GenericSpellActions.h +++ b/src/strategy/actions/GenericSpellActions.h @@ -215,17 +215,18 @@ protected: uint32 dispelType; }; +// Make Bots Paladin, druid, mage use the greater buff rank spell class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport { public: BuffOnPartyAction(PlayerbotAI* botAI, std::string const spell) - : CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) - { - } + : CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) { } Value* GetTargetValue() override; + bool Execute(Event event) override; std::string const getName() override { return PartyMemberActionNameSupport::getName(); } }; +// End Fix class CastShootAction : public CastSpellAction { diff --git a/src/strategy/paladin/PaladinActions.cpp b/src/strategy/paladin/PaladinActions.cpp index 8100148e..28b5db31 100644 --- a/src/strategy/paladin/PaladinActions.cpp +++ b/src/strategy/paladin/PaladinActions.cpp @@ -13,6 +13,53 @@ #include "Playerbots.h" #include "SharedDefines.h" #include "../../../../../src/server/scripts/Spells/spell_generic.cpp" +#include "GenericBuffUtils.h" +#include "Config.h" +#include "Group.h" +#include "ObjectAccessor.h" + +using ai::buff::MakeAuraQualifierForBuff; +using ai::buff::UpgradeToGroupIfAppropriate; + +// Helper : detect tank role on the target (player bot or not) return true if spec is tank or if the bot have tank strategies (bear/tank/tank face). +static inline bool IsTankRole(Player* p) +{ + if (!p) return false; + if (p->HasTankSpec()) + return true; + if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p)) + { + if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) || + otherAI->HasStrategy("tank", BOT_STATE_COMBAT) || + otherAI->HasStrategy("tank face", BOT_STATE_NON_COMBAT) || + otherAI->HasStrategy("tank face", BOT_STATE_COMBAT) || + otherAI->HasStrategy("bear", BOT_STATE_NON_COMBAT) || + otherAI->HasStrategy("bear", BOT_STATE_COMBAT)) + return true; + } + return false; +} + +// Added for solo paladin patch : determine if he's the only paladin on party +static inline bool IsOnlyPaladinInGroup(Player* bot) +{ + if (!bot) return false; + Group* g = bot->GetGroup(); + if (!g) return true; // solo + uint32 pals = 0u; + for (GroupReference* r = g->GetFirstMember(); r; r = r->next()) + { + Player* p = r->GetSource(); + if (!p || !p->IsInWorld()) continue; + if (p->getClass() == CLASS_PALADIN) ++pals; + } + return pals == 1u; +} + +static inline bool GroupHasTankOfClass(Group* g, uint8 classId) +{ + return GroupHasTankOfClass(g, static_cast(classId)); +} inline std::string const GetActualBlessingOfMight(Unit* target) { @@ -92,24 +139,32 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target) inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot) { - Player* targetPlayer = target->ToPlayer(); + if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) + return ""; - if (!targetPlayer) + Player* tp = target->ToPlayer(); + if (!tp) + return ""; + + if (auto* ai = GET_PLAYERBOT_AI(bot)) { - return "blessing of sanctuary"; + if (Unit* mt = ai->GetAiObjectContext()->GetValue("main tank")->Get()) + { + if (mt == target) + return "blessing of sanctuary"; + } } - if (targetPlayer->HasTankSpec() && bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) - { + if (tp->HasTankSpec()) return "blessing of sanctuary"; - } - return "blessing of kings"; + return ""; } Value* CastBlessingOnPartyAction::GetTargetValue() { - return context->GetValue("party member without aura", name); + + return context->GetValue("party member without aura", MakeAuraQualifierForBuff(spell)); } bool CastBlessingOfMightAction::Execute(Event event) @@ -118,12 +173,19 @@ bool CastBlessingOfMightAction::Execute(Event event) if (!target) return false; - return botAI->CastSpell(GetActualBlessingOfMight(target), target); + std::string castName = GetActualBlessingOfMight(target); + auto RP = ai::chat::MakeGroupAnnouncer(bot); + + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); + return botAI->CastSpell(castName, target); } Value* CastBlessingOfMightOnPartyAction::GetTargetValue() { - return context->GetValue("party member without aura", "blessing of might,blessing of wisdom"); + return context->GetValue( + "party member without aura", + "blessing of might,greater blessing of might,blessing of wisdom,greater blessing of wisdom,blessing of sanctuary,greater blessing of sanctuary" + ); } bool CastBlessingOfMightOnPartyAction::Execute(Event event) @@ -132,7 +194,11 @@ bool CastBlessingOfMightOnPartyAction::Execute(Event event) if (!target) return false; - return botAI->CastSpell(GetActualBlessingOfMight(target), target); + std::string castName = GetActualBlessingOfMight(target); + auto RP = ai::chat::MakeGroupAnnouncer(bot); + + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); + return botAI->CastSpell(castName, target); } bool CastBlessingOfWisdomAction::Execute(Event event) @@ -141,12 +207,19 @@ bool CastBlessingOfWisdomAction::Execute(Event event) if (!target) return false; - return botAI->CastSpell(GetActualBlessingOfWisdom(target), target); + std::string castName = GetActualBlessingOfWisdom(target); + auto RP = ai::chat::MakeGroupAnnouncer(bot); + + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); + return botAI->CastSpell(castName, target); } Value* CastBlessingOfWisdomOnPartyAction::GetTargetValue() { - return context->GetValue("party member without aura", "blessing of might,blessing of wisdom"); + return context->GetValue( + "party member without aura", + "blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,blessing of sanctuary,greater blessing of sanctuary" + ); } bool CastBlessingOfWisdomOnPartyAction::Execute(Event event) @@ -155,21 +228,250 @@ bool CastBlessingOfWisdomOnPartyAction::Execute(Event event) if (!target) return false; - return botAI->CastSpell(GetActualBlessingOfWisdom(target), target); + Player* targetPlayer = target->ToPlayer(); + + if (Group* g = bot->GetGroup()) + if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) + return false; + + if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) && + targetPlayer && IsTankRole(targetPlayer)) + { + LOG_DEBUG("playerbots", "[Wisdom/bmana] Skip tank {} (Kings only)", target->GetName()); + return false; + } + + std::string castName = GetActualBlessingOfWisdom(target); + if (castName.empty()) + return false; + + auto RP = ai::chat::MakeGroupAnnouncer(bot); + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); + return botAI->CastSpell(castName, target); } + Value* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue() { - return context->GetValue("party member without aura", "blessing of sanctuary,blessing of kings"); + return context->GetValue( + "party member without aura", + "blessing of sanctuary,greater blessing of sanctuary" + ); } bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event event) +{ + if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) + return false; + + Unit* target = GetTarget(); + if (!target) + { + // Fallback: GetTarget() can be null if no one needs a buff. + // Keep a valid pointer for the checks/logs that follow. + target = bot; + } + + Player* targetPlayer = target ? target->ToPlayer() : nullptr; + + // Small helpers to check relevant auras + const auto HasKingsAura = [&](Unit* u) -> bool { + return botAI->HasAura("blessing of kings", u) || botAI->HasAura("greater blessing of kings", u); + }; + const auto HasSanctAura = [&](Unit* u) -> bool { + return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u); + }; + + if (Group* g = bot->GetGroup()) + { + if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) + { + LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring"); + target = bot; + targetPlayer = bot->ToPlayer(); + } + } + + if (Player* self = bot->ToPlayer()) + { + bool selfHasSanct = HasSanctAura(self); + bool needSelf = IsTankRole(self) && !selfHasSanct; + + LOG_DEBUG("playerbots", "[Sanct] {} isTank={} selfHasSanct={} needSelf={}", + bot->GetName(), IsTankRole(self), selfHasSanct, needSelf); + + if (needSelf) + { + target = self; + targetPlayer = self; + } + } + + // Try to re-target a valid tank in group if needed + bool targetOk = false; + if (targetPlayer) + { + bool hasSanct = HasSanctAura(targetPlayer); + targetOk = IsTankRole(targetPlayer) && !hasSanct; + } + + if (!targetOk) + { + if (Group* g = bot->GetGroup()) + { + for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next()) + { + Player* p = gref->GetSource(); + if (!p) continue; + if (!p->IsInWorld() || !p->IsAlive()) continue; + if (!IsTankRole(p)) continue; + + bool hasSanct = HasSanctAura(p); + if (!hasSanct) + { + target = p; // prioritize this tank + targetPlayer = p; + targetOk = true; + break; + } + } + } + } + + { + bool hasKings = HasKingsAura(target); + bool hasSanct = HasSanctAura(target); + bool knowSanct = bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY); + LOG_DEBUG("playerbots", "[Sanct] Final target={} hasKings={} hasSanct={} knowSanct={}", + target->GetName(), hasKings, hasSanct, knowSanct); + } + + std::string castName = GetActualBlessingOfSanctuary(target, bot); + // If internal logic didn't recognize the tank (e.g., bear druid), force single-target Sanctuary + if (castName.empty()) + { + if (targetPlayer) + { + if (IsTankRole(targetPlayer)) + castName = "blessing of sanctuary"; // force single-target + else + return false; + } + else + return false; + } + if (targetPlayer && !IsTankRole(targetPlayer)) + { + auto RP = ai::chat::MakeGroupAnnouncer(bot); + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); + } + else + { + castName = "blessing of sanctuary"; + } + + bool ok = botAI->CastSpell(castName, target); + LOG_DEBUG("playerbots", "[Sanct] Cast {} on {} result={}", castName, target->GetName(), ok); + return ok; +} + +Value* CastBlessingOfKingsOnPartyAction::GetTargetValue() +{ + return context->GetValue( + "party member without aura", + "blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary" + ); +} + +bool CastBlessingOfKingsOnPartyAction::Execute(Event event) { Unit* target = GetTarget(); if (!target) return false; - return botAI->CastSpell(GetActualBlessingOfSanctuary(target, bot), target); + Group* g = bot->GetGroup(); + if (!g) + return false; + + // Added for patch solo paladin, never buff itself to not remove his sanctuary buff + if (botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT) && IsOnlyPaladinInGroup(bot)) + { + if (target->GetGUID() == bot->GetGUID()) + { + LOG_DEBUG("playerbots", "[Kings/bstats-solo] Skip self to keep Sanctuary on {}", bot->GetName()); + return false; + } + } + // End solo paladin patch + + Player* targetPlayer = target->ToPlayer(); + if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) + return false; + + const bool hasBmana = botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT); + const bool hasBstats = botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT); + + if (hasBmana) + { + if (!targetPlayer || !IsTankRole(targetPlayer)) + { + LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName()); + return false; + } + } + + if (targetPlayer) + { + const bool isTank = IsTankRole(targetPlayer); + const bool hasSanctFromMe = + target->HasAura(SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) || + target->HasAura(SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID()); + const bool hasSanctAny = + botAI->HasAura("blessing of sanctuary", target) || + botAI->HasAura("greater blessing of sanctuary", target); + + if (isTank && hasSanctFromMe) + { + LOG_DEBUG("playerbots", "[Kings] Skip: {} has my Sanctuary and is a tank", target->GetName()); + return false; + } + + if (hasBstats && isTank && hasSanctAny) + { + LOG_DEBUG("playerbots", "[Kings] Skip (bstats): {} already has Sanctuary and is a tank", target->GetName()); + return false; + } + } + + std::string castName = "blessing of kings"; + + bool allowGreater = true; + + if (hasBmana) + allowGreater = false; + + if (allowGreater && hasBstats && targetPlayer) + { + switch (targetPlayer->getClass()) + { + case CLASS_WARRIOR: + case CLASS_PALADIN: + case CLASS_DRUID: + case CLASS_DEATH_KNIGHT: + allowGreater = false; + break; + default: + break; + } + } + + if (allowGreater) + { + auto RP = ai::chat::MakeGroupAnnouncer(bot); + castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); + } + + return botAI->CastSpell(castName, target); } bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); } diff --git a/src/strategy/paladin/PaladinActions.h b/src/strategy/paladin/PaladinActions.h index 25159ceb..367a79f6 100644 --- a/src/strategy/paladin/PaladinActions.h +++ b/src/strategy/paladin/PaladinActions.h @@ -141,6 +141,8 @@ public: CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : CastBlessingOnPartyAction(botAI, "blessing of kings") {} std::string const getName() override { return "blessing of kings on party"; } + Value* GetTargetValue() override; // added for Sanctuary priority + bool Execute(Event event) override; // added for 2 paladins logic }; class CastBlessingOfSanctuaryAction : public CastBuffSpellAction diff --git a/src/strategy/paladin/PaladinBuffStrategies.cpp b/src/strategy/paladin/PaladinBuffStrategies.cpp index e18e38e5..c473e0df 100644 --- a/src/strategy/paladin/PaladinBuffStrategies.cpp +++ b/src/strategy/paladin/PaladinBuffStrategies.cpp @@ -9,11 +9,11 @@ void PaladinBuffManaStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back( - new TriggerNode("blessing of wisdom on party", - NextAction::array(0, new NextAction("blessing of wisdom on party", 11.0f), nullptr))); - // triggers.push_back(new TriggerNode("blessing", NextAction::array(0, new NextAction("blessing of wisdom", - // ACTION_HIGH + 8), nullptr))); + triggers.push_back(new TriggerNode("blessing of wisdom on party", + NextAction::array(0, new NextAction("blessing of wisdom on party", 11.0f), NULL))); + + triggers.push_back(new TriggerNode("blessing of kings on party", + NextAction::array(0, new NextAction("blessing of kings on party", 10.5f), NULL))); } void PaladinBuffHealthStrategy::InitTriggers(std::vector& triggers) @@ -85,10 +85,14 @@ void PaladinBuffThreatStrategy::InitTriggers(std::vector& triggers } void PaladinBuffStatsStrategy::InitTriggers(std::vector& triggers) -{ +{ + // First Sanctuary (prio > Kings) + triggers.push_back( + new TriggerNode("blessing of sanctuary on party", + NextAction::array(0, new NextAction("blessing of sanctuary on party", 12.0f), nullptr))); + + // After Kings triggers.push_back( new TriggerNode("blessing of kings on party", NextAction::array(0, new NextAction("blessing of kings on party", 11.0f), nullptr))); - // triggers.push_back(new TriggerNode("blessing", NextAction::array(0, new NextAction("blessing of kings", - // ACTION_HIGH + 8), nullptr))); } diff --git a/src/strategy/paladin/PaladinTriggers.h b/src/strategy/paladin/PaladinTriggers.h index b1409d36..a3c42d70 100644 --- a/src/strategy/paladin/PaladinTriggers.h +++ b/src/strategy/paladin/PaladinTriggers.h @@ -238,7 +238,7 @@ class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of sanctuary,blessing of kings", 2 * 2000) + : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) { } };