diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 6ea92b2d6..81f1b47b2 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -22,7 +22,6 @@ # THRESHOLDS # QUESTS # COMBAT -# GREATER BUFFS STRATEGIES # CHEATS # SPELLS # FLIGHTPATH @@ -478,6 +477,23 @@ AiPlayerbot.AutoSaveMana = 1 # Default: 60 (60%) AiPlayerbot.SaveManaThreshold = 60 +# Enable Paladin bots to use greater blessings, with the blessing used being based on the +# number of Paladins in the raid/group and the spec of the recipient. Priorities for each +# spec are hardcoded in GreaterBlessingActions.h. +# 0 = disabled +# 1 = enabled in raid groups only +# 2 = enabled in all groups +# Default: 1 (raid only) +AiPlayerbot.AutoGreaterBlessings = 1 + +# Enable bots to use group reagent buffs: Gift of the Wild, Arcane Brilliance, +# Prayer of Fortitude, Prayer of Spirit, and Prayer of Shadow Protection. +# 0 = disabled +# 1 = enabled in raid groups only +# 2 = enabled in all groups +# Default: 2 (all groups) +AiPlayerbot.AutoPartyBuffs = 2 + # Bots can flee from enemies AiPlayerbot.FleeingEnabled = 1 @@ -486,24 +502,6 @@ AiPlayerbot.FleeingEnabled = 1 # #################################################################################################### -#################################################################################################### -# GREATER 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/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 587862a29..d4b54f16f 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -24,9 +24,7 @@ using ai::buff::MakeAuraQualifierForBuff; using ai::spell::HasSpellOrCategoryCooldown; CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) - : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) -{ -} + : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) {} bool CastSpellAction::Execute(Event /*event*/) { @@ -53,18 +51,12 @@ bool CastSpellAction::Execute(Event /*event*/) wstrToLower(wnamepart); - if (!Utf8FitTo(spell, wnamepart)) - continue; - - if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM) + if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM) continue; uint32 itemId = spellInfo->Effects[0].ItemType; ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (!proto) - continue; - - if (bot->CanUseItem(proto) != EQUIP_ERR_OK) + if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK) continue; if (spellInfo->Id > castId) @@ -92,10 +84,7 @@ bool CastSpellAction::isUseful() } Unit* spellTarget = GetTarget(); - if (!spellTarget) - return false; - - if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId()) + if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId()) return false; // float combatReach = bot->GetCombatReach() + target->GetCombatReach(); @@ -143,10 +132,7 @@ CastMeleeSpellAction::CastMeleeSpellAction( bool CastMeleeSpellAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - - if (!bot->IsWithinMeleeRange(target)) + if (!target || !bot->IsWithinMeleeRange(target)) return false; return CastSpellAction::isUseful(); @@ -162,10 +148,7 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction( bool CastMeleeDebuffSpellAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - - if (!bot->IsWithinMeleeRange(target)) + if (!target || !bot->IsWithinMeleeRange(target)) return false; return CastDebuffSpellAction::isUseful(); @@ -175,14 +158,55 @@ bool CastAuraSpellAction::isUseful() { if (!GetTarget() || !CastSpellAction::isUseful()) return false; + Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration); - if (!aura) - return true; - if (beforeDuration && aura->GetDuration() < beforeDuration) + if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration)) return true; + return false; } +bool CastBuffSpellAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target || !CastSpellAction::isUseful()) + return false; + + Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration); + return !aura || (beforeDuration && aura->GetDuration() < beforeDuration); +} + +bool CastBuffSpellAction::Execute(Event /*event*/) +{ + return botAI->CastSpell(spell, GetTarget()); +} + +bool GroupBuffSpellAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target || !CastSpellAction::isUseful()) + return false; + + if (ai::buff::IsGroupVariantEnabled(bot, spell)) + { + std::string const groupVariant = ai::buff::GroupVariantFor(spell); + if (!groupVariant.empty() && botAI->HasAura(groupVariant, target, false, isOwner, -1, checkDuration)) + return false; + } + + Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration); + if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration)) + return true; + + return false; +} + +bool GroupBuffSpellAction::Execute(Event /*event*/) +{ + std::string const castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, spell); + return botAI->CastSpell(castName, GetTarget()); +} + CastEnchantItemMainHandAction::CastEnchantItemMainHandAction( PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} @@ -248,25 +272,16 @@ 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); +} + +Value* GroupBuffOnPartyAction::GetTargetValue() { 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"), shootSpellId(0) { @@ -365,50 +380,32 @@ bool CastVehicleSpellAction::Execute(Event /*event*/) bool CastEveryManForHimselfAction::isPossible() { uint32 spellId = AI_VALUE2(uint32, "spell id", spell); - if (!spellId) - return false; - - if (!bot->HasSpell(spellId)) - return false; - - if (HasSpellOrCategoryCooldown(bot, spellId)) - return false; - - return true; + return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId); } bool CastEveryManForHimselfAction::isUseful() { return (bot->HasAuraType(SPELL_AURA_MOD_STUN) || - bot->HasAuraType(SPELL_AURA_MOD_FEAR) || - bot->HasAuraType(SPELL_AURA_MOD_ROOT) || - bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || - bot->HasAuraType(SPELL_AURA_MOD_CHARM)) - && CastSpellAction::isUseful(); + bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_ROOT) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM)) + && CastSpellAction::isUseful(); } bool CastWillOfTheForsakenAction::isPossible() { uint32 spellId = AI_VALUE2(uint32, "spell id", spell); - if (!spellId) - return false; - - if (!bot->HasSpell(spellId)) - return false; - - if (HasSpellOrCategoryCooldown(bot, spellId)) - return false; - - return true; + return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId); } bool CastWillOfTheForsakenAction::isUseful() { return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) || - bot->HasAuraType(SPELL_AURA_MOD_CHARM) || - bot->HasAuraType(SPELL_AURA_AOE_CHARM) || - bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP)) - && CastSpellAction::isUseful(); + bot->HasAuraType(SPELL_AURA_MOD_CHARM) || + bot->HasAuraType(SPELL_AURA_AOE_CHARM) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP)) + && CastSpellAction::isUseful(); } bool UseTrinketAction::Execute(Event /*event*/) @@ -427,10 +424,7 @@ bool UseTrinketAction::Execute(Event /*event*/) bool UseTrinketAction::UseTrinket(Item* item) { - if (bot->CanUseItem(item) != EQUIP_ERR_OK) - return false; - - if (bot->IsNonMeleeSpellCast(true)) + if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true)) return false; uint8 bagIndex = item->GetBagSlot(); @@ -477,14 +471,13 @@ bool UseTrinketAction::UseTrinket(Item* item) if (spellProcFlag != 0) return false; if (!botAI->CanCastSpell(spellId, bot, false)) - { return false; - } break; } } if (!spellId) return false; + WorldPacket packet(CMSG_USE_ITEM); packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; @@ -500,9 +493,8 @@ bool CastDebuffSpellAction::isUseful() { Unit* target = GetTarget(); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } + return CastAuraSpellAction::isUseful() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; } diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index b87bd0a1f..5c52b7fd9 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -69,9 +69,7 @@ class CastDebuffSpellAction : public CastAuraSpellAction { public: CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f) - : CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) - { - } + : CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) {} bool isUseful() override; private: @@ -90,9 +88,7 @@ class CastDebuffSpellOnAttackerAction : public CastDebuffSpellAction public: CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true, float needLifeTime = 8.0f) - : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) - { - } + : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -104,9 +100,7 @@ class CastDebuffSpellOnMeleeAttackerAction : public CastDebuffSpellAction public: CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true, float needLifeTime = 8.0f) - : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) - { - } + : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -119,6 +113,19 @@ public: CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0); std::string const GetTargetName() override { return "self target"; } + bool isUseful() override; + bool Execute(Event event) override; +}; + +class GroupBuffSpellAction : public CastBuffSpellAction +{ +public: + GroupBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, + uint32 beforeDuration = 0) + : CastBuffSpellAction(botAI, spell, checkIsOwner, beforeDuration) {} + + bool isUseful() override; + bool Execute(Event event) override; }; class CastEnchantItemMainHandAction : public CastSpellAction @@ -151,8 +158,6 @@ public: // Yunfan: Mana efficiency tell the bot how to save mana. The higher the better. HealingManaEfficiency manaEfficiency; uint8 estAmount; - - // protected: }; class CastAoeHealSpellAction : public CastHealingSpellAction @@ -192,9 +197,7 @@ class HealPartyMemberAction : public CastHealingSpellAction, public PartyMemberA public: HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f, HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true) - : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) - { - } + : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) {} std::string const GetTargetName() override { return "party member to heal"; } std::string const getName() override { return PartyMemberActionNameSupport::getName(); } @@ -219,9 +222,7 @@ class CurePartyMemberAction : public CastSpellAction, public PartyMemberActionNa { public: CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType) - : CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) - { - } + : CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) {} Value* GetTargetValue() override; std::string const getName() override { return PartyMemberActionNameSupport::getName(); } @@ -230,18 +231,25 @@ 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; + std::string const getName() override { return PartyMemberActionNameSupport::getName(); } +}; + +class GroupBuffOnPartyAction : public GroupBuffSpellAction, public PartyMemberActionNameSupport +{ +public: + GroupBuffOnPartyAction(PlayerbotAI* botAI, std::string const spell) + : GroupBuffSpellAction(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 { @@ -323,6 +331,7 @@ class UseTrinketAction : public Action public: UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {} bool Execute(Event event) override; + protected: bool UseTrinket(Item* trinket); }; @@ -461,12 +470,11 @@ class BuffOnMainTankAction : public CastBuffSpellAction, public MainTankActionNa { public: BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false) - : CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) - { - } + : CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) {} public: virtual Value* GetTargetValue(); virtual std::string const getName() { return MainTankActionNameSupport::getName(); } }; + #endif diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 2035e1c29..aac920b5b 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -7,6 +7,7 @@ #include +#include "GenericBuffUtils.h" #include "CreatureAI.h" #include "ItemVisitors.h" #include "LastSpellCastValue.h" @@ -41,52 +42,50 @@ bool LowEnergyTrigger::IsActive() bool NoPetTrigger::IsActive() { - return (bot->GetMinionGUID().IsEmpty()) && (!AI_VALUE(Unit*, "pet target")) && (!bot->GetGuardianPet()) && - (!bot->GetFirstControlled()) && (!AI_VALUE2(bool, "mounted", "self target")); + return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() && + !bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target"); } bool HasPetTrigger::IsActive() { - return (AI_VALUE(Unit*, "pet target")) && !AI_VALUE2(bool, "mounted", "self target"); - ; + return AI_VALUE(Unit*, "pet target") && !AI_VALUE2(bool, "mounted", "self target"); } bool PetAttackTrigger::IsActive() { Guardian* pet = bot->GetGuardianPet(); if (!pet) - { return false; - } + Unit* target = AI_VALUE(Unit*, "current target"); if (!target) - { return false; - } + if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack()) - { return false; - } + if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat()) - { return false; - } + return true; } bool HighManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana; + return AI_VALUE2(bool, "has mana", "self target") && + AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana; } bool AlmostFullManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > 85; + return AI_VALUE2(bool, "has mana", "self target") && + AI_VALUE2(uint8, "mana", "self target") > 85; } bool EnoughManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana; + return AI_VALUE2(bool, "has mana", "self target") && + AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana; } bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; } @@ -101,9 +100,8 @@ bool TargetWithComboPointsLowerHealTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } + return ComboPointsAvailableTrigger::IsActive() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime; } @@ -164,19 +162,27 @@ bool BuffTrigger::IsActive() Unit* target = GetTarget(); if (!target) return false; - if (!SpellTrigger::IsActive()) - return false; + Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration); - if (!aura) - return true; - if (beforeDuration && aura->GetDuration() < beforeDuration) + if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration)) return true; + return false; } Value* BuffOnPartyTrigger::GetTargetValue() { - return context->GetValue("party member without aura", spell); + return context->GetValue( + "party member without aura", ai::buff::MakeAuraQualifierForBuff(spell)); +} + +bool BuffOnPartyTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (ai::buff::ShouldDeferPartyBuffEvaluationForRecentLogin(bot, target, spell)) + return false; + + return BuffTrigger::IsActive(); } bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); } @@ -209,13 +215,14 @@ bool MediumThreatTrigger::IsActive() { if (!AI_VALUE(Unit*, "main tank")) return false; + return MyAttackerCountTrigger::IsActive(); } bool LowTankThreatTrigger::IsActive() { - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) return false; Unit* current_target = AI_VALUE(Unit*, "current target"); @@ -224,7 +231,7 @@ bool LowTankThreatTrigger::IsActive() ThreatManager& mgr = current_target->GetThreatMgr(); float threat = mgr.GetThreat(bot); - float tankThreat = mgr.GetThreat(mt); + float tankThreat = mgr.GetThreat(mainTank); return tankThreat == 0.0f || threat > tankThreat * 0.5f; } @@ -232,9 +239,8 @@ bool AoeTrigger::IsActive() { Unit* current_target = AI_VALUE(Unit*, "current target"); if (!current_target) - { return false; - } + GuidVector attackers = context->GetValue("attackers")->Get(); int attackers_count = 0; for (ObjectGuid const guid : attackers) @@ -242,10 +248,9 @@ bool AoeTrigger::IsActive() Unit* unit = botAI->GetUnit(guid); if (!unit || !unit->IsAlive()) continue; + if (unit->GetDistance(current_target->GetPosition()) <= range) - { attackers_count++; - } } return attackers_count >= amount; } @@ -274,20 +279,19 @@ bool DebuffTrigger::IsActive() { Unit* target = GetTarget(); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } - return BuffTrigger::IsActive() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; + + return BuffTrigger::IsActive() && + (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; } bool DebuffOnBossTrigger::IsActive() { if (!DebuffTrigger::IsActive()) - { return false; - } - Creature* c = GetTarget()->ToCreature(); - return c && ((c->IsDungeonBoss()) || (c->isWorldBoss())); + + Creature* creature = GetTarget()->ToCreature(); + return creature && (creature->IsDungeonBoss() || creature->isWorldBoss()); } bool SpellTrigger::IsActive() { return GetTarget(); } @@ -317,9 +321,7 @@ bool SpellCooldownTrigger::IsActive() } RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability) - : Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) -{ -} + : Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) {} bool RandomTrigger::IsActive() { @@ -330,6 +332,7 @@ bool RandomTrigger::IsActive() int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier); if (k < 1) k = 1; + return (rand() % k) == 0; } @@ -368,9 +371,11 @@ bool BoostTrigger::IsActive() { if (!BuffTrigger::IsActive()) return false; + Unit* target = AI_VALUE(Unit*, "current target"); if (target && target->ToPlayer()) return true; + return AI_VALUE(uint8, "balance") <= balance; } @@ -379,20 +384,19 @@ bool GenericBoostTrigger::IsActive() Unit* target = AI_VALUE(Unit*, "current target"); if (target && target->ToPlayer()) return true; + return AI_VALUE(uint8, "balance") <= balance; } bool HealerShouldAttackTrigger::IsActive() { - // nobody can help me if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1) return true; if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth) return false; - // special check for resto druid (dont remove tree of life frequently) - if (bot->GetAura(33891)) + if (bot->GetAura(33891)) // Tree of Life { LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue("last spell cast")->Get(); if (lastSpell.timer + 5 > time(nullptr)) @@ -401,7 +405,6 @@ bool HealerShouldAttackTrigger::IsActive() int manaThreshold; int balance = AI_VALUE(uint8, "balance"); - // higher threshold in higher pressure if (balance <= 50) manaThreshold = 85; else if (balance <= 100) @@ -425,13 +428,7 @@ bool InterruptSpellTrigger::IsActive() bool DeflectSpellTrigger::IsActive() { Unit* target = GetTarget(); - if (!target) - return false; - - if (!target->IsNonMeleeSpellCast(true)) - return false; - - if (target->GetTarget() != bot->GetGUID()) + if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID()) return false; uint32 spellid = context->GetValue("spell id", spell)->Get(); @@ -462,6 +459,7 @@ bool DeflectSpellTrigger::IsActive() return true; } } + return false; } @@ -495,17 +493,16 @@ bool FearSleepSapTrigger::IsActive() bool HasAuraStackTrigger::IsActive() { - Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); - // sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "HasAuraStackTrigger::IsActive %s %d", getName(), aura ? - // aura->GetStackAmount() : -1); - return aura; + return botAI->GetAura(getName(), GetTarget(), false, true, stack); } bool TimerTrigger::IsActive() { - if (time(nullptr) != lastCheck) + time_t now = time(nullptr); + + if (now != lastCheck) { - lastCheck = time(nullptr); + lastCheck = now; return true; } @@ -552,9 +549,8 @@ bool IsBehindTargetTrigger::IsActive() bool IsNotBehindTargetTrigger::IsActive() { if (botAI->HasStrategy("stay", botAI->GetState())) - { return false; - } + Unit* target = AI_VALUE(Unit*, "current target"); return target && !AI_VALUE2(bool, "behind", "current target"); } @@ -562,9 +558,8 @@ bool IsNotBehindTargetTrigger::IsActive() bool IsNotFacingTargetTrigger::IsActive() { if (botAI->HasStrategy("stay", botAI->GetState())) - { return false; - } + return !AI_VALUE2(bool, "facing", "current target"); } @@ -581,12 +576,14 @@ bool NoPossibleTargetsTrigger::IsActive() return !targets.size(); } -bool PossibleAddsTrigger::IsActive() { return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); } +bool PossibleAddsTrigger::IsActive() +{ + return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); +} bool NotDpsTargetActiveTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); - // do not switch if enemy target if (target && target->IsAlive()) { Unit* enemy = AI_VALUE(Unit*, "enemy player target"); @@ -604,7 +601,6 @@ bool NotDpsAoeTargetActiveTrigger::IsActive() Unit* target = AI_VALUE(Unit*, "current target"); Unit* enemy = AI_VALUE(Unit*, "enemy player target"); - // do not switch if enemy target if (target && target == enemy && target->IsAlive()) return false; @@ -638,7 +634,10 @@ Value* InterruptEnemyHealerTrigger::GetTargetValue() return context->GetValue("enemy healer target", spell); } -bool RandomBotUpdateTrigger::IsActive() { return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); } +bool RandomBotUpdateTrigger::IsActive() +{ + return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); +} bool NoNonBotPlayersAroundTrigger::IsActive() { @@ -718,43 +717,24 @@ bool AmmoCountTrigger::IsActive() bool NewPetTrigger::IsActive() { - // Get the bot player object from the AI - Player* bot = botAI->GetBot(); - if (!bot) - return false; - - // Try to get the current pet; initialize guardian and GUID to null/empty - Pet* pet = bot->GetPet(); - Guardian* guardian = nullptr; ObjectGuid currentPetGuid = ObjectGuid::Empty; - // If bot has a pet, get its GUID - if (pet) - { + if (Pet* pet = bot->GetPet()) currentPetGuid = pet->GetGUID(); - } - else - { - // If no pet, try to get a guardian pet and its GUID - guardian = bot->GetGuardianPet(); - if (guardian) - currentPetGuid = guardian->GetGUID(); - } + else if (Guardian* guardian = bot->GetGuardianPet()) + 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/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index a3931c246..3e662eb3b 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -20,9 +20,7 @@ class StatAvailable : public Trigger { public: StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available") - : Trigger(botAI, name), amount(amount) - { - } + : Trigger(botAI, name), amount(amount) {} protected: int32 amount; @@ -118,8 +116,8 @@ public: class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger { public: - TargetWithComboPointsLowerHealTrigger(PlayerbotAI* ai, int32 combo_point = 5, float lifeTime = 8.0f) - : ComboPointsAvailableTrigger(ai, combo_point), lifeTime(lifeTime) + TargetWithComboPointsLowerHealTrigger(PlayerbotAI* botAI, int32 combo_point = 5, float lifeTime = 8.0f) + : ComboPointsAvailableTrigger(botAI, combo_point), lifeTime(lifeTime) { } bool IsActive() override; @@ -196,7 +194,6 @@ public: bool IsActive() override; }; -// TODO: check other targets class InterruptSpellTrigger : public SpellTrigger { public: @@ -217,9 +214,7 @@ class AttackerCountTrigger : public Trigger { public: AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance) - : Trigger(botAI), amount(amount), distance(distance) - { - } + : Trigger(botAI), amount(amount), distance(distance) {} bool IsActive() override; std::string const getName() override { return "attacker count"; } @@ -269,9 +264,7 @@ class AoeTrigger : public AttackerCountTrigger { public: AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f) - : AttackerCountTrigger(botAI, amount), range(range) - { - } + : AttackerCountTrigger(botAI, amount), range(range) {} bool IsActive() override; std::string const getName() override { return "aoe"; } @@ -317,7 +310,8 @@ public: class BuffTrigger : public SpellTrigger { public: - BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0) + BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, + bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0) : SpellTrigger(botAI, spell, checkInterval) { this->checkIsOwner = checkIsOwner; @@ -339,11 +333,10 @@ class BuffOnPartyTrigger : public BuffTrigger { public: BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1) - : BuffTrigger(botAI, spell, checkInterval) - { - } + : BuffTrigger(botAI, spell, checkInterval) {} Value* GetTargetValue() override; + bool IsActive() override; std::string const getName() override { return spell + " on party"; } }; @@ -393,9 +386,7 @@ class DebuffTrigger : public BuffTrigger public: DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, float needLifeTime = 8.0f, uint32 beforeDuration = 0) - : BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) - { - } + : BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) {} std::string const GetTargetName() override { return "current target"; } bool IsActive() override; @@ -408,9 +399,7 @@ class DebuffOnBossTrigger : public DebuffTrigger { public: DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false) - : DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) - { - } + : DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) {} bool IsActive() override; }; @@ -419,9 +408,7 @@ class DebuffOnAttackerTrigger : public DebuffTrigger public: DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true, float needLifeTime = 8.0f) - : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) - { - } + : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -432,9 +419,7 @@ class DebuffOnMeleeAttackerTrigger : public DebuffTrigger public: DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true, float needLifeTime = 8.0f) - : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) - { - } + : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -444,9 +429,7 @@ class BoostTrigger : public BuffTrigger { public: BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f) - : BuffTrigger(botAI, spell, 1), balance(balance) - { - } + : BuffTrigger(botAI, spell, 1), balance(balance) {} bool IsActive() override; @@ -458,9 +441,7 @@ class GenericBoostTrigger : public Trigger { public: GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f) - : Trigger(botAI, "generic boost", 1), balance(balance) - { - } + : Trigger(botAI, "generic boost", 1), balance(balance) {} bool IsActive() override; @@ -472,9 +453,7 @@ class HealerShouldAttackTrigger : public Trigger { public: HealerShouldAttackTrigger(PlayerbotAI* botAI) - : Trigger(botAI, "healer should attack", 1) - { - } + : Trigger(botAI, "healer should attack", 1) {} bool IsActive() override; }; @@ -580,7 +559,7 @@ public: class HasPetTrigger : public Trigger { public: - HasPetTrigger(PlayerbotAI* ai) : Trigger(ai, "has pet", 5 * 1000) {} + HasPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "has pet", 5 * 1000) {} virtual bool IsActive() override; }; @@ -588,7 +567,7 @@ public: class PetAttackTrigger : public Trigger { public: - PetAttackTrigger(PlayerbotAI* ai) : Trigger(ai, "pet attack") {} + PetAttackTrigger(PlayerbotAI* botAI) : Trigger(botAI, "pet attack") {} virtual bool IsActive() override; }; @@ -597,9 +576,7 @@ class ItemCountTrigger : public Trigger { public: ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000) - : Trigger(botAI, item, interval), item(item), count(count) - { - } + : Trigger(botAI, item, interval), item(item), count(count) {} bool IsActive() override; std::string const getName() override { return "item count"; } @@ -613,9 +590,7 @@ class AmmoCountTrigger : public ItemCountTrigger { public: AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000) - : ItemCountTrigger(botAI, item, count, interval) - { - } + : ItemCountTrigger(botAI, item, count, interval) {} bool IsActive() override; }; @@ -623,9 +598,7 @@ class HasAuraTrigger : public Trigger { public: HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1) - : Trigger(botAI, spell, checkInterval) - { - } + : Trigger(botAI, spell, checkInterval) {} std::string const GetTargetName() override { return "self target"; } bool IsActive() override; @@ -634,10 +607,8 @@ public: class HasAuraStackTrigger : public Trigger { public: - HasAuraStackTrigger(PlayerbotAI* ai, std::string spell, int stack, int checkInterval = 1) - : Trigger(ai, spell, checkInterval), stack(stack) - { - } + HasAuraStackTrigger(PlayerbotAI* botAI, std::string spell, int stack, int checkInterval = 1) + : Trigger(botAI, spell, checkInterval), stack(stack) {} std::string const GetTargetName() override { return "self target"; } bool IsActive() override; @@ -858,9 +829,7 @@ class StayTimeTrigger : public Trigger { public: StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name) - : Trigger(botAI, name, 5 * 1000), delay(delay) - { - } + : Trigger(botAI, name, 5 * 1000), delay(delay) {} bool IsActive() override; @@ -877,7 +846,7 @@ public: class ReturnToStayPositionTrigger : public Trigger { public: - ReturnToStayPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "return to stay position", 2) {} + ReturnToStayPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to stay position", 2) {} virtual bool IsActive() override; }; @@ -892,9 +861,7 @@ class GiveItemTrigger : public Trigger { public: GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item) - : Trigger(botAI, name, 2 * 1000), item(item) - { - } + : Trigger(botAI, name, 2 * 1000), item(item) {} bool IsActive() override; @@ -962,9 +929,7 @@ class BuffOnMainTankTrigger : public BuffTrigger { public: BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1) - : BuffTrigger(botAI, spell, checkInterval, checkIsOwner) - { - } + : BuffTrigger(botAI, spell, checkInterval, checkIsOwner) {} public: virtual Value* GetTargetValue(); @@ -973,7 +938,7 @@ public: class SelfResurrectTrigger : public Trigger { public: - SelfResurrectTrigger(PlayerbotAI* ai) : Trigger(ai, "can self resurrect") {} + SelfResurrectTrigger(PlayerbotAI* botAI) : Trigger(botAI, "can self resurrect") {} bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); } }; @@ -981,7 +946,7 @@ public: class NewPetTrigger : public Trigger { public: - NewPetTrigger(PlayerbotAI* ai) : Trigger(ai, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {} + NewPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {} bool IsActive() override; diff --git a/src/Ai/Base/Util/GenericBuffUtils.cpp b/src/Ai/Base/Util/GenericBuffUtils.cpp index 22d56c0ad..233d98f43 100644 --- a/src/Ai/Base/Util/GenericBuffUtils.cpp +++ b/src/Ai/Base/Util/GenericBuffUtils.cpp @@ -4,23 +4,89 @@ */ #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 "GameTime.h" +#include "Group.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" +#include "SpellMgr.h" +#include "Unit.h" #include "Value.h" -#include "Config.h" -#include "PlayerbotTextMgr.h" namespace ai::buff { + namespace + { + // Prevents bots from immediately casting already-present buffs upon logging in + constexpr uint32 POST_LOGIN_BUFF_GRACE_MS = 5 * IN_MILLISECONDS; + + bool IsWithinPostLoginBuffGrace(Player* player) + { + if (!player) + return false; + + return getMSTimeDiff( + player->GetInGameTime(), GameTime::GetGameTimeMS().count()) < POST_LOGIN_BUFF_GRACE_MS; + } + } + + static bool HasEnoughSameMapMissingPlayersForGroupVariant( + Player* bot, PlayerbotAI* botAI, std::string const& baseName, + std::string const& groupName, uint32 requiredCount = 3) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + uint32 missingCount = 0; + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member || !member->IsInWorld() || !member->IsAlive() || + member->GetMap() != bot->GetMap()) + { + continue; + } + + if (botAI->HasAura(baseName, member) || botAI->HasAura(groupName, member)) + continue; + + if (++missingCount >= requiredCount) + return true; + } + + return false; + } + + static bool IsEligibleGroupForPartyBuffs(Group const* group) + { + if (!group) + return false; + + switch (sPlayerbotAIConfig.autoPartyBuffs) + { + case AutoPartyBuffMode::RAID_ONLY: + return group->isRaidGroup(); + case AutoPartyBuffMode::GROUP_OR_RAID: + return true; + case AutoPartyBuffMode::DISABLED: + return false; + } + + return false; + } + + bool IsGroupVariantEnabled(Player* bot, std::string const& name) + { + if (!IsEligibleGroupForPartyBuffs(bot->GetGroup())) + return false; + + return !GroupVariantFor(name).empty(); + } + std::string MakeAuraQualifierForBuff(std::string const& name) { // Paladin @@ -34,27 +100,89 @@ namespace ai::buff if (name == "arcane intellect") return "arcane intellect,arcane brilliance"; // Priest if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude"; + if (name == "divine spirit") return "divine spirit,prayer of spirit"; + if (name == "shadow protection") return "shadow protection,prayer of shadow protection"; 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"; + if (name == "divine spirit") return "prayer of spirit"; + if (name == "shadow protection") return "prayer of shadow protection"; + // Paladin blessings are intentionally not included here because they are + // coordinated by the auto greater blessing system instead. return std::string(); } + bool NeedsPostLoginBuffGrace(std::string const& name) + { + static char const* const trackedBuffs[] = { + "mark of the wild", + "arcane intellect", + "power word: fortitude", + "prayer of fortitude", + "divine spirit", + "prayer of spirit", + "shadow protection", + "prayer of shadow protection", + "blessing of kings", + "blessing of might", + "blessing of wisdom", + "blessing of sanctuary" + }; + + for (char const* trackedBuff : trackedBuffs) + { + if (name.find(trackedBuff) != std::string::npos) + return true; + } + + return false; + } + + bool ShouldDeferPartyBuffEvaluationForRecentLogin( + Player* bot, Unit* target, std::string const& spell) + { + if (!NeedsPostLoginBuffGrace(spell)) + return false; + + if (IsWithinPostLoginBuffGrace(bot)) + return true; + + Player* playerTarget = target ? target->ToPlayer() : nullptr; + return IsWithinPostLoginBuffGrace(playerTarget); + } + + bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot) + { + if (IsWithinPostLoginBuffGrace(bot)) + return true; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member || !member->IsInWorld()) + continue; + + if (IsWithinPostLoginBuffGrace(member)) + return true; + } + + return false; + } + bool HasRequiredReagents(Player* bot, uint32 spellId) { if (!spellId) @@ -72,75 +200,33 @@ namespace ai::buff 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) + Player* bot, PlayerbotAI* botAI, std::string const& baseName) { - 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 (!IsGroupVariantEnabled(bot, baseName)) + return baseName; - if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty()) - { - uint32 const groupVariantSpellId = botAI->GetAiObjectContext() + std::string const groupName = GroupVariantFor(baseName); + if (groupName.empty()) + return baseName; + + // Prefer singles until at least three living, in-world group members on the bot's map + // are missing both the single-target buff and its group variant. + if (!HasEnoughSameMapMissingPlayersForGroupVariant(bot, botAI, baseName, groupName)) + return baseName; + + uint32 const groupSpellId = 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 (groupSpellId && HasRequiredReagents(bot, groupSpellId)) + return groupName; - 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 placeholders; - placeholders["%group_spell"] = groupName; - placeholders["%base_spell"] = baseName; - - std::string announceText = PlayerbotTextMgr::instance().GetBotTextOrDefault(key, - "Out of components for %group_spell. Using %base_spell!", placeholders); - - announce(announceText); - last = now; - } - } - } - return castName; + return baseName; } } diff --git a/src/Ai/Base/Util/GenericBuffUtils.h b/src/Ai/Base/Util/GenericBuffUtils.h index 37bad58a6..9f93bf108 100644 --- a/src/Ai/Base/Util/GenericBuffUtils.h +++ b/src/Ai/Base/Util/GenericBuffUtils.h @@ -6,63 +6,40 @@ #pragma once #include -#include #include "Common.h" -#include "Group.h" -#include "Chat.h" -#include "Language.h" class Player; class PlayerbotAI; +class Unit; namespace ai::buff { -// Build an aura qualifier "single + greater" to avoid double-buffing +bool IsGroupVariantEnabled(Player* bot, std::string const& name); + 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 NeedsPostLoginBuffGrace(std::string const& name); + +bool ShouldDeferPartyBuffEvaluationForRecentLogin( + Player* bot, + Unit* target, + std::string const& spell); + +bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot); + 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 = {} - ); + std::string const& baseName); + } namespace ai::spell { bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId); } - -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/Ai/Class/Druid/Action/DruidActions.h b/src/Ai/Class/Druid/Action/DruidActions.h index e386ff05d..bfe5cfc7f 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.h +++ b/src/Ai/Class/Druid/Action/DruidActions.h @@ -87,16 +87,16 @@ public: bool isUseful() override; }; -class CastMarkOfTheWildAction : public CastBuffSpellAction +class CastMarkOfTheWildAction : public GroupBuffSpellAction { public: - CastMarkOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mark of the wild") {} + CastMarkOfTheWildAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "mark of the wild") {} }; -class CastMarkOfTheWildOnPartyAction : public BuffOnPartyAction +class CastMarkOfTheWildOnPartyAction : public GroupBuffOnPartyAction { public: - CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "mark of the wild") {} + CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "mark of the wild") {} }; class CastSurvivalInstinctsAction : public CastBuffSpellAction diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp b/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp index 5a8dbdfa4..03b5accc9 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp @@ -9,11 +9,6 @@ #include "Playerbots.h" #include "ServerFacade.h" -bool MarkOfTheWildOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget()); -} - bool MarkOfTheWildTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget()); diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.h b/src/Ai/Class/Druid/Trigger/DruidTriggers.h index 1f389c947..990024685 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.h +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.h @@ -23,15 +23,13 @@ class PlayerbotAI; class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger { public: - MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 2 * 2000) {} - - bool IsActive() override; + MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 4 * 2000) {} }; class MarkOfTheWildTrigger : public BuffTrigger { public: - MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 2 * 2000) {} + MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 4 * 2000) {} bool IsActive() override; }; diff --git a/src/Ai/Class/Mage/Action/MageActions.h b/src/Ai/Class/Mage/Action/MageActions.h index 4c7f76a68..c394a379d 100644 --- a/src/Ai/Class/Mage/Action/MageActions.h +++ b/src/Ai/Class/Mage/Action/MageActions.h @@ -40,16 +40,16 @@ public: CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {} }; -class CastArcaneIntellectAction : public CastBuffSpellAction +class CastArcaneIntellectAction : public GroupBuffSpellAction { public: - CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {} + CastArcaneIntellectAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "arcane intellect") {} }; -class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction +class CastArcaneIntellectOnPartyAction : public GroupBuffOnPartyAction { public: - CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {} + CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "arcane intellect") {} }; class CastFocusMagicOnPartyAction : public CastSpellAction diff --git a/src/Ai/Class/Mage/Trigger/MageTriggers.cpp b/src/Ai/Class/Mage/Trigger/MageTriggers.cpp index 7e7ba9ab7..34babc81e 100644 --- a/src/Ai/Class/Mage/Trigger/MageTriggers.cpp +++ b/src/Ai/Class/Mage/Trigger/MageTriggers.cpp @@ -31,11 +31,6 @@ bool NoManaGemTrigger::IsActive() return true; } -bool ArcaneIntellectOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget()); -} - bool ArcaneIntellectTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget()); diff --git a/src/Ai/Class/Mage/Trigger/MageTriggers.h b/src/Ai/Class/Mage/Trigger/MageTriggers.h index 566b6b61e..58afab0f8 100644 --- a/src/Ai/Class/Mage/Trigger/MageTriggers.h +++ b/src/Ai/Class/Mage/Trigger/MageTriggers.h @@ -19,14 +19,13 @@ class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger { public: ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "arcane intellect", 2 * 2000) {} - bool IsActive() override; + : BuffOnPartyTrigger(botAI, "arcane intellect", 4 * 2000) {} }; class ArcaneIntellectTrigger : public BuffTrigger { public: - ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 2 * 2000) {} + ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 4 * 2000) {} bool IsActive() override; }; diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index 38e17af1e..b7044ee3b 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -7,24 +7,100 @@ #include "AiFactory.h" #include "Event.h" +#include "GenericBuffUtils.h" +#include "PaladinGreaterBlessingAction.h" #include "PaladinHelper.h" -#include "PlayerbotAI.h" #include "Playerbots.h" #include "SharedDefines.h" -#include "../../../../../src/server/scripts/Spells/spell_generic.cpp" -#include "Ai/Base/Util/GenericBuffUtils.h" -#include "Group.h" -#include "ObjectAccessor.h" -using ai::buff::MakeAuraQualifierForBuff; - -// 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) +static bool IsBlessingTargetCandidate(Player* bot, Player* player) { - if (!p) return false; - if (p->HasTankSpec()) + if (!player || !player->IsAlive() || player->GetMapId() != bot->GetMapId()) + return false; + + if (player->IsGameMaster()) + return false; + + return bot->GetDistance(player) < sPlayerbotAIConfig.spellDistance * 2 && + bot->IsWithinLOS(player->GetPositionX(), player->GetPositionY(), + player->GetPositionZ()); +} + +static bool HasBlessingAura( + PlayerbotAI* botAI, Unit* target, std::initializer_list auraNames) +{ + for (char const* auraName : auraNames) + { + if (botAI->HasAura(auraName, target)) + return true; + } + + return false; +} + +static bool IsGreaterBlessingMode(Player* bot) +{ + return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()); +} + +template +static Unit* FindBlessingTarget( + Player* bot, PlayerbotAI* botAI, Predicate&& predicate) +{ + std::vector masters; + std::vector healers; + std::vector tanks; + std::vector others; + + Player* master = botAI->GetMaster(); + auto addPlayer = [&](Player* player) + { + if (!IsBlessingTargetCandidate(bot, player)) + return; + + if (player == master) + masters.push_back(player); + else if (botAI->IsHeal(player)) + healers.push_back(player); + else if (botAI->IsTank(player)) + tanks.push_back(player); + else + others.push_back(player); + }; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + addPlayer(ref->GetSource()); + } + else + { + addPlayer(bot); + } + + std::vector*> orderedLists = { + &masters, &healers, &tanks, &others }; + for (std::vector* players : orderedLists) + { + for (Player* player : *players) + { + if (predicate(player)) + return player; + } + } + + return nullptr; +} + +static inline bool IsTankRole(Player* player) +{ + if (!player) + return false; + + if (player->HasTankSpec()) return true; - if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p)) + + if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(player)) { if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) || otherAI->HasStrategy("tank", BOT_STATE_COMBAT) || @@ -34,33 +110,36 @@ static inline bool IsTankRole(Player* p) 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()) + if (!bot) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return true; + + uint32 paladins = 0u; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - Player* p = r->GetSource(); - if (!p || !p->IsInWorld()) continue; - if (p->getClass() == CLASS_PALADIN) ++pals; + Player* player = ref->GetSource(); + if (!player || !player->IsInWorld()) continue; + if (player->getClass() == CLASS_PALADIN) ++paladins; } - return pals == 1u; + + return paladins == 1u; } inline std::string const GetActualBlessingOfMight(Unit* target) { if (!target->ToPlayer()) - { return "blessing of might"; - } - int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); + uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); switch (target->getClass()) { case CLASS_MAGE: @@ -70,21 +149,15 @@ inline std::string const GetActualBlessingOfMight(Unit* target) break; case CLASS_SHAMAN: if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION) - { return "blessing of wisdom"; - } break; case CLASS_DRUID: if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE) - { return "blessing of wisdom"; - } break; case CLASS_PALADIN: if (tab == PALADIN_TAB_HOLY) - { return "blessing of wisdom"; - } break; } @@ -94,10 +167,9 @@ inline std::string const GetActualBlessingOfMight(Unit* target) inline std::string const GetActualBlessingOfWisdom(Unit* target) { if (!target->ToPlayer()) - { return "blessing of might"; - } - int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); + + uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); switch (target->getClass()) { case CLASS_WARRIOR: @@ -108,21 +180,15 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target) break; case CLASS_SHAMAN: if (tab == SHAMAN_TAB_ENHANCEMENT) - { return "blessing of might"; - } break; case CLASS_DRUID: if (tab == DRUID_TAB_FERAL) - { return "blessing of might"; - } break; case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION) - { return "blessing of might"; - } break; } @@ -131,32 +197,41 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target) inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot) { - if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) + if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) return ""; - Player* tp = target->ToPlayer(); - if (!tp) + Player* targetPlayer = target->ToPlayer(); + if (!targetPlayer) return ""; - if (auto* ai = GET_PLAYERBOT_AI(bot)) + if (auto* botAI = GET_PLAYERBOT_AI(bot)) { - if (Unit* mt = ai->GetAiObjectContext()->GetValue("main tank")->Get()) + if (Unit* mainTank = + botAI->GetAiObjectContext()->GetValue("main tank")->Get()) { - if (mt == target) + if (mainTank == target) return "blessing of sanctuary"; } } - if (tp->HasTankSpec()) + if (targetPlayer->HasTankSpec()) return "blessing of sanctuary"; return ""; } -Value* CastBlessingOnPartyAction::GetTargetValue() +Unit* CastBlessingOfMightOnPartyAction::GetTarget() { + if (IsGreaterBlessingMode(bot)) + return nullptr; - return context->GetValue("party member without aura", MakeAuraQualifierForBuff(spell)); + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + return !HasBlessingAura(botAI, player, + { "blessing of might", "greater blessing of might", + "blessing of wisdom", "greater blessing of wisdom", + "blessing of sanctuary", "greater blessing of sanctuary" }); + }); } bool CastBlessingOfMightAction::Execute(Event /*event*/) @@ -166,9 +241,6 @@ bool CastBlessingOfMightAction::Execute(Event /*event*/) return false; 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); } @@ -176,20 +248,22 @@ Value* CastBlessingOfMightOnPartyAction::GetTargetValue() { 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" + "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*/) { + if (IsGreaterBlessingMode(bot)) + return false; + Unit* target = GetTarget(); if (!target) return false; 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); } @@ -200,45 +274,58 @@ bool CastBlessingOfWisdomAction::Execute(Event /*event*/) return false; 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); } +Unit* CastBlessingOfWisdomOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && IsTankRole(player)) + return false; + + return !HasBlessingAura(botAI, player, + { "blessing of might", "greater blessing of might", + "blessing of wisdom", "greater blessing of wisdom", + "blessing of sanctuary", "greater blessing of sanctuary" }); + }); +} + Value* CastBlessingOfWisdomOnPartyAction::GetTargetValue() { 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" + "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*/) { + if (IsGreaterBlessingMode(bot)) + return false; + Unit* target = GetTarget(); if (!target) return false; Player* targetPlayer = target->ToPlayer(); - if (Group* g = bot->GetGroup()) - if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) + if (Group* group = bot->GetGroup()) + if (targetPlayer && !group->IsMember(targetPlayer->GetGUID())) return false; - if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) && + if (botAI->HasStrategy("bwisdom", 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); } @@ -252,32 +339,31 @@ Value* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue() bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) { - if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) + if (IsGreaterBlessingMode(bot)) + return false; + + if (!bot->HasSpell(ai::paladin::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 HasKingsAura = [&](Unit* unit) -> bool { + return botAI->HasAura("blessing of kings", unit) || + botAI->HasAura("greater blessing of kings", unit); }; - const auto HasSanctAura = [&](Unit* u) -> bool { - return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u); + const auto HasSanctAura = [&](Unit* unit) -> bool { + return botAI->HasAura("blessing of sanctuary", unit) || + botAI->HasAura("greater blessing of sanctuary", unit); }; - if (Group* g = bot->GetGroup()) + if (Group* group = bot->GetGroup()) { - if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) + if (targetPlayer && !group->IsMember(targetPlayer->GetGUID())) { - LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring"); target = bot; targetPlayer = bot->ToPlayer(); } @@ -288,9 +374,6 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) 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; @@ -298,7 +381,6 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) } } - // Try to re-target a valid tank in group if needed bool targetOk = false; if (targetPlayer) { @@ -308,20 +390,20 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) if (!targetOk) { - if (Group* g = bot->GetGroup()) + if (Group* group = bot->GetGroup()) { - for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next()) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - Player* p = gref->GetSource(); - if (!p) continue; - if (!p->IsInWorld() || !p->IsAlive()) continue; - if (!IsTankRole(p)) continue; + Player* player = ref->GetSource(); + if (!player) continue; + if (!player->IsInWorld() || !player->IsAlive()) continue; + if (!IsTankRole(player)) continue; - bool hasSanct = HasSanctAura(p); + bool hasSanct = HasSanctAura(player); if (!hasSanct) { - target = p; // prioritize this tank - targetPlayer = p; + target = player; + targetPlayer = player; targetOk = true; break; } @@ -329,150 +411,147 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) } } - { - 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 (GetActualBlessingOfSanctuary(target, bot).empty()) { if (targetPlayer) { if (IsTankRole(targetPlayer)) - castName = "blessing of sanctuary"; // force single-target + return botAI->CastSpell("blessing of sanctuary", 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; + return botAI->CastSpell("blessing of sanctuary", target); +} + +Unit* CastBlessingOfSanctuaryOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + return nullptr; + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + return IsTankRole(player) && + !HasBlessingAura(botAI, player, + { "blessing of sanctuary", "greater blessing of sanctuary" }); + }); } Value* CastBlessingOfKingsOnPartyAction::GetTargetValue() { return context->GetValue( "party member without aura", - "blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary" + "blessing of kings,greater blessing of kings," + "blessing of sanctuary,greater blessing of sanctuary" ); } +Unit* CastBlessingOfKingsOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT); + const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT); + const bool onlyPaladinInGroup = IsOnlyPaladinInGroup(bot); + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + const bool isTank = IsTankRole(player); + const bool hasKingsOrSanct = HasBlessingAura(botAI, player, + { "blessing of kings", "greater blessing of kings", + "blessing of sanctuary", "greater blessing of sanctuary" }); + + if (hasKingsOrSanct) + return false; + + if (hasBwisdom) + return isTank; + + if (hasBkings) + { + if (isTank) + return false; + + if (onlyPaladinInGroup && player == bot) + return false; + } + + return true; + }); +} + bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/) { + if (IsGreaterBlessingMode(bot)) + return false; + Unit* target = GetTarget(); if (!target) return false; - Group* g = bot->GetGroup(); - if (!g) + Group* group = bot->GetGroup(); + if (!group) 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 (botAI->HasStrategy("bkings", 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())) + if (targetPlayer && !group->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); + const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT); + const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT); - if (hasBmana) - { - if (!targetPlayer || !IsTankRole(targetPlayer)) - { - LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName()); - return false; - } - } + if (hasBwisdom && (!targetPlayer || !IsTankRole(targetPlayer))) + 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()); + target->HasAura(ai::paladin::SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) || + target->HasAura(ai::paladin::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()); + if (hasBkings && isTank && hasSanctAny) 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); + return botAI->CastSpell("blessing of kings", target); } -bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); } +bool CastSealSpellAction::isUseful() +{ + return AI_VALUE2(bool, "combat", "self target"); +} -Value* CastTurnUndeadAction::GetTargetValue() { return context->GetValue("cc target", getName()); } +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); + bool const hasSelfHand = + selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); if (!bot->GetGroup()) { @@ -499,7 +578,8 @@ bool CastHandOfFreedomOnPartyAction::isUseful() if (!target) return false; - return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); + return CastBuffSpellAction::isUseful() && + !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); } Unit* CastRighteousDefenseAction::GetTarget() diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.h b/src/Ai/Class/Paladin/Action/PaladinActions.h index 75b0637a4..ef59225e6 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/src/Ai/Class/Paladin/Action/PaladinActions.h @@ -8,10 +8,6 @@ #include "AiObject.h" #include "GenericSpellActions.h" -#include "SharedDefines.h" - -class PlayerbotAI; -class Unit; // seals BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness"); @@ -88,24 +84,13 @@ public: bool Execute(Event event) override; }; -class CastBlessingOnPartyAction : public BuffOnPartyAction -{ -public: - CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) - : BuffOnPartyAction(botAI, name), name(name) {} - - Value* GetTargetValue() override; - -private: - std::string name; -}; - class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction { public: CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {} std::string const getName() override { return "blessing of might on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -124,6 +109,7 @@ public: CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {} std::string const getName() override { return "blessing of wisdom on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -134,12 +120,13 @@ public: CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {} }; -class CastBlessingOfKingsOnPartyAction : public CastBlessingOnPartyAction +class CastBlessingOfKingsOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : CastBlessingOnPartyAction(botAI, "blessing of kings") {} + CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of kings") {} std::string const getName() override { return "blessing of kings on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; // added for Sanctuary priority bool Execute(Event event) override; // added for 2 paladins logic }; @@ -156,6 +143,7 @@ public: CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {} std::string const getName() override { return "blessing of sanctuary on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; bool Execute(Event event) override; }; diff --git a/src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.cpp b/src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.cpp new file mode 100644 index 000000000..a123b29fa --- /dev/null +++ b/src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.cpp @@ -0,0 +1,1116 @@ +/* + * 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 "PaladinGreaterBlessingAction.h" + +#include "AiObjectContext.h" +#include "AiFactory.h" +#include "Event.h" +#include "GenericBuffUtils.h" +#include "PaladinHelper.h" +#include "Playerbots.h" +#include "SharedDefines.h" +#include "SpellAuraEffects.h" +#include "Value.h" + +#include +#include + +namespace ai::gbless +{ +namespace +{ + constexpr uint32 GREATER_BLESSING_ASSIGNMENT_CACHE_MS = 4 * 1000; + constexpr uint32 GREATER_BLESSING_PENDING_ASSIGNMENT_CACHE_MS = 100; + constexpr uint8 MAX_BLESSING_SLOTS = 4; + constexpr uint8 MAX_CLASS_ID = 12; + + constexpr size_t BaseBlessingCategoryCount = MAX_BLESSING_SLOTS; + + constexpr size_t BaseBlessingIndex(BaseBlessingCategory category) + { + return static_cast(static_cast(category) - static_cast(BASE_MIGHT)); + } + + bool UsesRoleBucket(uint8 classId) + { + switch (classId) + { + case CLASS_WARRIOR: + case CLASS_DEATH_KNIGHT: + case CLASS_SHAMAN: + case CLASS_PALADIN: + case CLASS_DRUID: + return true; + default: + return false; + } + } + + bool IsSameBucket( + CachedBlessingBucketAssignment const& left, + CachedBlessingBucketAssignment const& right) + { + return left.classId == right.classId && + left.byRole == right.byRole && + (!left.byRole || left.role == right.role); + } + + int TalentScore(Player* player) + { + if (!player) + return 0; + + int score = 0; + if (player->HasAura(SPELL_IMPROVED_MIGHT_R1) || + player->HasAura(SPELL_IMPROVED_MIGHT_R2)) + { + score += 2; + } + if (player->HasAura(SPELL_IMPROVED_WISDOM_R1) || + player->HasAura(SPELL_IMPROVED_WISDOM_R2)) + { + score += 1; + } + + return score; + } + + int TalentMatchScore(Player* player, BaseBlessingCategory category) + { + if (!player) + return std::numeric_limits::min() / 4; + + if (category == BASE_SANCTUARY) + { + if (!player->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + return std::numeric_limits::min() / 4; + + return 2; + } + + if (category == BASE_MIGHT && + (player->HasAura(SPELL_IMPROVED_MIGHT_R1) || + player->HasAura(SPELL_IMPROVED_MIGHT_R2))) + { + return 1; + } + if (category == BASE_WISDOM && + (player->HasAura(SPELL_IMPROVED_WISDOM_R1) || + player->HasAura(SPELL_IMPROVED_WISDOM_R2))) + { + return 1; + } + + return 0; + } + + struct DesiredBlessingSet + { + std::array ordered = {}; + std::array wants = {}; + uint8 count = 0; + }; + + struct PresentBucket + { + uint8 classId = 0; + RoleProfile role = ROLE_CASTER; + bool byRole = false; + uint8 memberCount = 0; + DesiredBlessingSet desired; + }; + + DesiredBlessingSet BuildDesiredBlessingSet( + RoleProfile role, uint8 paladinCount, bool anySanctuaryAvailable) + { + DesiredBlessingSet desired; + + auto const& priority = BASE_BLESSING_PRIORITIES[role]; + uint8 requestedCount = std::min(paladinCount, MAX_BLESSING_SLOTS); + + for (uint8 index = 0; + index < MAX_BLESSING_SLOTS && desired.count < requestedCount; + ++index) + { + BaseBlessingCategory category = priority.priorities[index]; + if (category == BASE_NONE) + continue; + + if (category == BASE_SANCTUARY && !anySanctuaryAvailable) + category = BASE_KINGS; + + if (category == BASE_NONE || desired.wants[BaseBlessingIndex(category)]) + continue; + + desired.ordered[desired.count++] = category; + desired.wants[BaseBlessingIndex(category)] = true; + } + + return desired; + } + + std::vector OrderedCommonBases( + std::vector const& classBuckets, + std::array const& commonBases) + { + std::vector ordered; + + for (PresentBucket const* bucket : classBuckets) + { + for (uint8 index = 0; index < bucket->desired.count; ++index) + { + BaseBlessingCategory category = bucket->desired.ordered[index]; + if (!commonBases[BaseBlessingIndex(category)]) + continue; + + if (std::find(ordered.begin(), ordered.end(), category) == ordered.end()) + ordered.push_back(category); + } + } + + return ordered; + } + + bool ComputeBestOwners( + std::vector const& categories, + std::vector const& botPaladins, + std::vector const& candidatePaladins, + std::vector& outOwners, + int* outScore = nullptr) + { + outOwners.clear(); + if (categories.empty()) + { + if (outScore) + *outScore = 0; + return true; + } + + std::vector currentOwners(categories.size(), -1); + std::vector bestOwners(categories.size(), -1); + std::vector used(candidatePaladins.size(), false); + int bestScore = std::numeric_limits::min(); + int bestTalentCost = std::numeric_limits::max(); + bool found = false; + + auto search = [&](auto&& self, size_t position, int score, int talentCost) -> void + { + if (position >= categories.size()) + { + if (!found || score > bestScore || + (score == bestScore && talentCost < bestTalentCost) || + (score == bestScore && + talentCost == bestTalentCost && + std::lexicographical_compare(currentOwners.begin(), currentOwners.end(), + bestOwners.begin(), bestOwners.end()))) + { + found = true; + bestScore = score; + bestTalentCost = talentCost; + bestOwners = currentOwners; + } + return; + } + + for (size_t candidateIndex = 0; + candidateIndex < candidatePaladins.size(); ++candidateIndex) + { + if (used[candidateIndex]) + continue; + + int paladinIndex = candidatePaladins[candidateIndex]; + int matchScore = TalentMatchScore(botPaladins[paladinIndex], categories[position]); + if (matchScore <= std::numeric_limits::min() / 8) + continue; + + used[candidateIndex] = true; + currentOwners[position] = paladinIndex; + self(self, position + 1, score + matchScore, + talentCost + TalentScore(botPaladins[paladinIndex])); + currentOwners[position] = -1; + used[candidateIndex] = false; + } + }; + + search(search, 0, 0, 0); + + if (!found) + return false; + + outOwners = std::move(bestOwners); + if (outScore) + *outScore = bestScore; + return true; + } + + struct ClassPlanPreference + { + std::array slotCoverage = {}; + std::array matchedSlotCoverage = {}; + uint8 commonCount = 0; + int ownerScore = std::numeric_limits::min(); + int commonTalentCost = std::numeric_limits::max(); + uint8 selectedMask = 0; + std::vector classWideOwners; + bool valid = false; + }; + + uint8 OrderedIndexForCategory(PresentBucket const* bucket, BaseBlessingCategory category) + { + for (uint8 index = 0; index < bucket->desired.count; ++index) + { + if (bucket->desired.ordered[index] == category) + return index; + } + + return MAX_BLESSING_SLOTS; + } + + bool IsBetterClassPlanPreference( + ClassPlanPreference const& candidate, ClassPlanPreference const& best) + { + if (!best.valid) + return true; + + for (uint8 index = 0; index < MAX_BLESSING_SLOTS; ++index) + { + if (candidate.slotCoverage[index] != best.slotCoverage[index]) + return candidate.slotCoverage[index] > best.slotCoverage[index]; + } + + for (uint8 index = 0; index < MAX_BLESSING_SLOTS; ++index) + { + if (candidate.matchedSlotCoverage[index] != best.matchedSlotCoverage[index]) + return candidate.matchedSlotCoverage[index] > best.matchedSlotCoverage[index]; + } + + if (candidate.commonCount != best.commonCount) + return candidate.commonCount > best.commonCount; + + if (candidate.ownerScore != best.ownerScore) + return candidate.ownerScore > best.ownerScore; + + if (candidate.commonTalentCost != best.commonTalentCost) + return candidate.commonTalentCost < best.commonTalentCost; + + if (std::lexicographical_compare(candidate.classWideOwners.begin(), + candidate.classWideOwners.end(), + best.classWideOwners.begin(), + best.classWideOwners.end())) + return true; + + return candidate.selectedMask < best.selectedMask; + } + + bool ComputeOwnersForClassPlan( + std::vector const& classWideBases, + std::vector> const& exclusiveBasesByBucket, + std::vector const& botPaladins, + std::vector const& allPaladins, + std::vector& outClassWideOwners, + std::vector>& outExclusiveOwnersByBucket, + int& outOwnerScore, + int& outCommonTalentCost) + { + outClassWideOwners.clear(); + outExclusiveOwnersByBucket.clear(); + outOwnerScore = std::numeric_limits::min(); + outCommonTalentCost = std::numeric_limits::max(); + + std::vector currentClassWideOwners(classWideBases.size(), -1); + std::vector bestClassWideOwners(classWideBases.size(), -1); + std::vector used(allPaladins.size(), false); + std::vector> bestExclusiveOwnersByBucket(exclusiveBasesByBucket.size()); + bool found = false; + + auto search = [&](auto&& self, size_t position, int commonScore, int commonTalentCost) -> void + { + if (position >= classWideBases.size()) + { + std::vector availablePaladins; + availablePaladins.reserve(allPaladins.size()); + for (size_t candidateIndex = 0; candidateIndex < allPaladins.size(); ++candidateIndex) + { + if (!used[candidateIndex]) + availablePaladins.push_back(allPaladins[candidateIndex]); + } + + int totalOwnerScore = commonScore; + std::vector> exclusiveOwnersByBucket(exclusiveBasesByBucket.size()); + for (size_t bucketIndex = 0; bucketIndex < exclusiveBasesByBucket.size(); ++bucketIndex) + { + int exclusiveScore = 0; + if (!ComputeBestOwners( + exclusiveBasesByBucket[bucketIndex], botPaladins, availablePaladins, + exclusiveOwnersByBucket[bucketIndex], &exclusiveScore)) + { + return; + } + + totalOwnerScore += exclusiveScore; + } + + if (!found || totalOwnerScore > outOwnerScore || + (totalOwnerScore == outOwnerScore && commonTalentCost < outCommonTalentCost) || + (totalOwnerScore == outOwnerScore && + commonTalentCost == outCommonTalentCost && + std::lexicographical_compare(currentClassWideOwners.begin(), currentClassWideOwners.end(), + bestClassWideOwners.begin(), bestClassWideOwners.end()))) + { + found = true; + outOwnerScore = totalOwnerScore; + outCommonTalentCost = commonTalentCost; + bestClassWideOwners = currentClassWideOwners; + bestExclusiveOwnersByBucket = std::move(exclusiveOwnersByBucket); + } + + return; + } + + for (size_t candidateIndex = 0; candidateIndex < allPaladins.size(); ++candidateIndex) + { + if (used[candidateIndex]) + continue; + + int const paladinIndex = allPaladins[candidateIndex]; + int const matchScore = TalentMatchScore(botPaladins[paladinIndex], classWideBases[position]); + if (matchScore <= std::numeric_limits::min() / 8) + continue; + + used[candidateIndex] = true; + currentClassWideOwners[position] = paladinIndex; + self(self, position + 1, commonScore + matchScore, + commonTalentCost + TalentScore(botPaladins[paladinIndex])); + currentClassWideOwners[position] = -1; + used[candidateIndex] = false; + } + }; + + search(search, 0, 0, 0); + + if (!found) + return false; + + outClassWideOwners = std::move(bestClassWideOwners); + outExclusiveOwnersByBucket = std::move(bestExclusiveOwnersByBucket); + return true; + } + + bool ComputeBestClassAssignments( + std::vector const& classBuckets, + std::vector const& botPaladins, + std::vector const& allPaladins, + std::vector& outClassWideOwners, + std::vector>& outExclusiveOwnersByBucket, + std::vector& outClassWideBases, + std::vector>& outExclusiveBasesByBucket) + { + outClassWideOwners.clear(); + outExclusiveOwnersByBucket.clear(); + outClassWideBases.clear(); + outExclusiveBasesByBucket.clear(); + + uint8 const categoryMaskLimit = + static_cast(1u << (static_cast(BASE_SANCTUARY) + 1u)); + uint8 commonUnionMask = 0; + ClassPlanPreference bestPreference; + std::vector bestClassWideOwners; + std::vector> bestExclusiveOwnersByBucket; + std::vector bestClassWideBases; + std::vector> bestExclusiveBasesByBucket; + + for (uint8 baseValue = BASE_MIGHT; baseValue <= BASE_SANCTUARY; ++baseValue) + { + BaseBlessingCategory category = static_cast(baseValue); + if (std::all_of(classBuckets.begin(), classBuckets.end(), + [&](PresentBucket const* bucket) + { + return bucket->desired.wants[BaseBlessingIndex(category)]; + })) + { + commonUnionMask |= static_cast(1u << static_cast(category)); + } + } + + for (uint8 promotedMask = 0; promotedMask < categoryMaskLimit; ++promotedMask) + { + if ((promotedMask & commonUnionMask) != promotedMask) + continue; + + ClassPlanPreference candidatePreference; + candidatePreference.selectedMask = promotedMask; + + std::vector> exclusiveBasesByBucket(classBuckets.size()); + + for (size_t bucketIndex = 0; bucketIndex < classBuckets.size(); ++bucketIndex) + { + PresentBucket const* bucket = classBuckets[bucketIndex]; + for (uint8 index = 0; index < bucket->desired.count; ++index) + { + BaseBlessingCategory category = bucket->desired.ordered[index]; + candidatePreference.slotCoverage[index] += bucket->memberCount; + + if (!(promotedMask & static_cast(1u << static_cast(category)))) + exclusiveBasesByBucket[bucketIndex].push_back(category); + } + } + + std::array promotedCommonBases = {}; + for (uint8 baseValue = BASE_MIGHT; baseValue <= BASE_SANCTUARY; ++baseValue) + { + BaseBlessingCategory category = static_cast(baseValue); + promotedCommonBases[BaseBlessingIndex(category)] = + (promotedMask & static_cast(1u << static_cast(category))) != 0; + } + + std::vector classWideBases = + OrderedCommonBases(classBuckets, promotedCommonBases); + + std::vector classWideOwners; + std::vector> exclusiveOwnersByBucket; + int ownerScore = 0; + int commonTalentCost = 0; + if (!ComputeOwnersForClassPlan( + classWideBases, exclusiveBasesByBucket, botPaladins, allPaladins, + classWideOwners, exclusiveOwnersByBucket, ownerScore, commonTalentCost)) + { + continue; + } + + candidatePreference.commonCount = static_cast(classWideBases.size()); + candidatePreference.ownerScore = ownerScore; + candidatePreference.commonTalentCost = commonTalentCost; + candidatePreference.classWideOwners = classWideOwners; + + for (size_t index = 0; index < classWideBases.size(); ++index) + { + Player* owner = botPaladins[classWideOwners[index]]; + BaseBlessingCategory category = classWideBases[index]; + if (TalentMatchScore(owner, category) <= 0) + continue; + + for (PresentBucket const* bucket : classBuckets) + { + uint8 orderedIndex = OrderedIndexForCategory(bucket, category); + if (orderedIndex >= MAX_BLESSING_SLOTS) + continue; + + candidatePreference.matchedSlotCoverage[orderedIndex] += bucket->memberCount; + } + } + + for (size_t bucketIndex = 0; bucketIndex < classBuckets.size(); ++bucketIndex) + { + PresentBucket const* bucket = classBuckets[bucketIndex]; + auto const& exclusiveBases = exclusiveBasesByBucket[bucketIndex]; + auto const& exclusiveOwners = exclusiveOwnersByBucket[bucketIndex]; + + for (size_t index = 0; index < exclusiveBases.size(); ++index) + { + Player* owner = botPaladins[exclusiveOwners[index]]; + BaseBlessingCategory category = exclusiveBases[index]; + if (TalentMatchScore(owner, category) <= 0) + continue; + + uint8 orderedIndex = OrderedIndexForCategory(bucket, category); + if (orderedIndex >= MAX_BLESSING_SLOTS) + continue; + + candidatePreference.matchedSlotCoverage[orderedIndex] += bucket->memberCount; + } + } + + candidatePreference.valid = true; + + if (!IsBetterClassPlanPreference(candidatePreference, bestPreference)) + continue; + + bestPreference = candidatePreference; + bestClassWideOwners = std::move(classWideOwners); + bestExclusiveOwnersByBucket = std::move(exclusiveOwnersByBucket); + bestClassWideBases = std::move(classWideBases); + bestExclusiveBasesByBucket = std::move(exclusiveBasesByBucket); + } + + if (!bestPreference.valid) + return false; + + outClassWideOwners = std::move(bestClassWideOwners); + outExclusiveOwnersByBucket = std::move(bestExclusiveOwnersByBucket); + outClassWideBases = std::move(bestClassWideBases); + outExclusiveBasesByBucket = std::move(bestExclusiveBasesByBucket); + return true; + } + + void AddUniqueAssignment( + std::vector& assignments, + CachedBlessingBucketAssignment const& assignment) + { + auto existing = std::find_if(assignments.begin(), assignments.end(), + [&](CachedBlessingBucketAssignment const& cachedAssignment) + { + return cachedAssignment.blessing == assignment.blessing && + IsSameBucket(cachedAssignment, assignment); + }); + + if (existing == assignments.end()) + assignments.push_back(assignment); + } + + bool ComputeGreaterBlessingAssignments( + PlayerbotAI* botAI, std::vector& outAssignments) + { + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + if (!IsEligibleGroupForAutoBlessings(group)) + return false; + + std::vector botPaladins; + struct RaidMember + { + Player* player; + RoleProfile role; + }; + std::vector raidMembers; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* player = ref->GetSource(); + if (!player || !player->IsInWorld()) + continue; + + if (player->getClass() == CLASS_PALADIN && GET_PLAYERBOT_AI(player)) + botPaladins.push_back(player); + + // Important: keep dead members in the assignment model so revive does + // not temporarily delete an entire blessing bucket from the cache. + raidMembers.push_back({player, ResolveRoleProfile(player)}); + } + + if (botPaladins.empty()) + return false; + + bool anySanctuaryAvailable = false; + for (Player* paladin : botPaladins) + { + if (paladin && paladin->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + { + anySanctuaryAvailable = true; + break; + } + } + + std::sort(botPaladins.begin(), botPaladins.end(), + [](Player* a, Player* b) + { + int sa = TalentScore(a); + int sb = TalentScore(b); + if (sa != sb) + return sa > sb; + return a->GetGUID() < b->GetGUID(); + }); + + uint8 activePaladinCount = + std::min(static_cast(botPaladins.size()), MAX_BLESSING_SLOTS); + + int mySlot = -1; + for (size_t i = 0; i < botPaladins.size(); ++i) + { + if (botPaladins[i]->GetGUID() == bot->GetGUID()) + { + mySlot = static_cast(i); + break; + } + } + if (mySlot < 0 || mySlot >= activePaladinCount) + return false; + + std::vector buckets; + buckets.reserve(raidMembers.size()); + + for (auto const& member : raidMembers) + { + uint8 classId = member.player->getClass(); + if (classId >= MAX_CLASS_ID) + continue; + + PresentBucket bucket; + bucket.classId = classId; + bucket.role = member.role; + bucket.byRole = UsesRoleBucket(classId); + bucket.memberCount = 1; + bucket.desired = BuildDesiredBlessingSet( + member.role, activePaladinCount, anySanctuaryAvailable); + + if (!bucket.byRole) + bucket.role = ROLE_CASTER; + + if (!bucket.desired.count) + continue; + + auto existing = std::find_if(buckets.begin(), buckets.end(), + [&](PresentBucket const& other) + { + return other.classId == bucket.classId && + other.byRole == bucket.byRole && + (!bucket.byRole || other.role == bucket.role); + }); + + if (existing == buckets.end()) + buckets.push_back(bucket); + else + ++existing->memberCount; + } + + if (buckets.empty()) + return false; + + std::vector allPaladins; + allPaladins.reserve(activePaladinCount); + for (uint8 paladinIndex = 0; paladinIndex < activePaladinCount; ++paladinIndex) + allPaladins.push_back(paladinIndex); + + outAssignments.clear(); + + for (uint8 classId = 0; classId < MAX_CLASS_ID; ++classId) + { + std::vector classBuckets; + for (PresentBucket const& bucket : buckets) + { + if (bucket.classId == classId) + classBuckets.push_back(&bucket); + } + + if (classBuckets.empty()) + continue; + + std::vector classWideOwners; + std::vector> exclusiveOwnersByBucket; + std::vector classWideBases; + std::vector> exclusiveBasesByBucket; + if (!ComputeBestClassAssignments( + classBuckets, botPaladins, allPaladins, + classWideOwners, exclusiveOwnersByBucket, classWideBases, + exclusiveBasesByBucket)) + return false; + + for (size_t index = 0; index < classWideBases.size(); ++index) + { + if (classWideOwners[index] != mySlot) + continue; + + CachedBlessingBucketAssignment assignment; + assignment.classId = classId; + assignment.role = classBuckets.front()->role; + assignment.byRole = false; + assignment.blessing = ToGreaterVariant(classWideBases[index]); + AddUniqueAssignment(outAssignments, assignment); + } + + for (size_t bucketIndex = 0; bucketIndex < classBuckets.size(); ++bucketIndex) + { + PresentBucket const* bucket = classBuckets[bucketIndex]; + auto const& exclusiveBases = exclusiveBasesByBucket[bucketIndex]; + auto const& exclusiveOwners = exclusiveOwnersByBucket[bucketIndex]; + + for (size_t index = 0; index < exclusiveBases.size(); ++index) + { + if (exclusiveOwners[index] != mySlot) + continue; + + CachedBlessingBucketAssignment assignment; + assignment.classId = bucket->classId; + assignment.role = bucket->role; + assignment.byRole = true; + assignment.blessing = ToSingleVariant(exclusiveBases[index]); + AddUniqueAssignment(outAssignments, assignment); + } + } + } + + return !outAssignments.empty(); + } + + class GreaterBlessingAssignmentsValue : public CalculatedValue + { + public: + GreaterBlessingAssignmentsValue(PlayerbotAI* botAI) + : CalculatedValue( + botAI, "greater blessing assignments", GREATER_BLESSING_ASSIGNMENT_CACHE_MS) {} + + protected: + CachedBlessingAssignments Calculate() override + { + CachedBlessingAssignments cached; + + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + cached.groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + std::vector assignments; + if (!ComputeGreaterBlessingAssignments(botAI, assignments)) + return cached; + + cached.valid = true; + cached.assignments = std::move(assignments); + + return cached; + } + }; + +} + +UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI) +{ + return new GreaterBlessingAssignmentsValue(botAI); +} + +bool IsEligibleGroupForAutoBlessings(Group const* group) +{ + if (!group) + return false; + + switch (sPlayerbotAIConfig.autoGreaterBlessings) + { + case AutoPartyBuffMode::RAID_ONLY: + return group->isRaidGroup(); + case AutoPartyBuffMode::GROUP_OR_RAID: + return true; + case AutoPartyBuffMode::DISABLED: + default: + return false; + } +} + +static bool HasMyExactBlessing(PlayerbotAI* botAI, Unit* target, BlessingType type) +{ + std::string name = BlessingSpellName(type); + if (name.empty()) + return false; + + return botAI->HasAura(name.c_str(), target, false, true); +} + +static int32 GetAuraStrength(Aura const* aura, AuraType auraType) +{ + if (!aura) + return 0; + + int32 amount = 0; + for (uint8 effect = 0; effect < MAX_SPELL_EFFECTS; ++effect) + { + AuraEffect* auraEffect = aura->GetEffect(effect); + if (!auraEffect || auraEffect->GetAuraType() != auraType) + continue; + + amount = std::max(amount, auraEffect->GetAmount()); + } + + return amount; +} + +static int32 GetExistingBlessingStrength( + PlayerbotAI* botAI, Unit* target, BaseBlessingCategory category) +{ + if (category != BASE_MIGHT && category != BASE_WISDOM) + return 0; + + AuraType auraType = + category == BASE_MIGHT ? SPELL_AURA_MOD_ATTACK_POWER : SPELL_AURA_MOD_POWER_REGEN; + int32 strongestAmount = 0; + + for (BlessingType type : {ToSingleVariant(category), ToGreaterVariant(category)}) + { + Aura* aura = botAI->GetAura(BlessingSpellName(type), target); + strongestAmount = std::max(strongestAmount, GetAuraStrength(aura, auraType)); + } + + return strongestAmount; +} + +static bool HasSameFamilyBlessing( + PlayerbotAI* botAI, Unit* target, BaseBlessingCategory category) +{ + for (BlessingType type : {ToSingleVariant(category), ToGreaterVariant(category)}) + { + if (botAI->HasAura(BlessingSpellName(type), target)) + return true; + } + + return false; +} + +static int32 GetBlessingCastStrength(Player* caster, BlessingType type, uint32 spellId) +{ + if (!caster || !spellId) + return 0; + + BaseBlessingCategory category = BaseBlessingOf(type); + if (category != BASE_MIGHT && category != BASE_WISDOM) + return 0; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return 0; + + AuraType auraType = + category == BASE_MIGHT ? SPELL_AURA_MOD_ATTACK_POWER : SPELL_AURA_MOD_POWER_REGEN; + int32 amount = 0; + for (uint8 effect = 0; effect < MAX_SPELL_EFFECTS; ++effect) + { + if (spellInfo->Effects[effect].ApplyAuraName != auraType) + continue; + + amount = std::max(amount, spellInfo->Effects[effect].BasePoints + 1); + } + + if (amount <= 0) + return 0; + + switch (category) + { + case BASE_MIGHT: + if (caster->HasAura(SPELL_IMPROVED_MIGHT_R2)) + return amount * 125 / 100; + if (caster->HasAura(SPELL_IMPROVED_MIGHT_R1)) + return amount * 112 / 100; + break; + case BASE_WISDOM: + if (caster->HasAura(SPELL_IMPROVED_WISDOM_R2)) + return amount * 120 / 100; + if (caster->HasAura(SPELL_IMPROVED_WISDOM_R1)) + return amount * 110 / 100; + break; + default: + break; + } + + return amount; +} + +static bool HasEquivalentOrStrongerSameFamilyBlessing( + PlayerbotAI* botAI, Unit* target, BlessingType castType, uint32 spellId) +{ + BaseBlessingCategory category = BaseBlessingOf(castType); + if (category != BASE_MIGHT && category != BASE_WISDOM) + return HasSameFamilyBlessing(botAI, target, category); + + int32 castStrength = GetBlessingCastStrength(botAI->GetBot(), castType, spellId); + if (castStrength <= 0) + return false; + + return GetExistingBlessingStrength(botAI, target, category) >= castStrength; +} + +static bool MatchesBucket(Player* player, CachedBlessingBucketAssignment const& assignment) +{ + if (!player || player->getClass() != assignment.classId) + return false; + + return !assignment.byRole || ResolveRoleProfile(player) == assignment.role; +} + +static Player* FindMissingBlessingTarget( + PlayerbotAI* botAI, CachedBlessingBucketAssignment const& assignment, + BlessingType castType, uint32 spellId, std::string const& spellName) +{ + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* player = ref->GetSource(); + if (!player || !player->IsInWorld() || !player->IsAlive()) + continue; + + if (!(player->IsInSameGroupWith(bot) || player->IsInSameRaidWith(bot))) + continue; + + if (!MatchesBucket(player, assignment) || + HasMyExactBlessing(botAI, player, castType) || + HasEquivalentOrStrongerSameFamilyBlessing(botAI, player, castType, spellId) || + !botAI->CanCastSpell(spellName, player)) + { + continue; + } + + return player; + } + + return nullptr; +} + +static bool GetCachedAssignments( + AiObjectContext* context, Group* group, + std::vector& outAssignments) +{ + uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + Value* cacheValue = + context->GetValue("greater blessing assignments"); + if (!cacheValue) + return false; + + CachedBlessingAssignments cachedAssignments = cacheValue->Get(); + if (cachedAssignments.groupKey != groupKey) + { + cacheValue->Reset(); + cachedAssignments = cacheValue->Get(); + } + + if (!cachedAssignments.valid || cachedAssignments.groupKey != groupKey) + return false; + + outAssignments = cachedAssignments.assignments; + return true; +} + +static bool FindPendingAssignmentFromAssignments( + PlayerbotAI* botAI, + std::vector const& assignments, + GreaterBlessingPlayerAssignment& outAssignment, + std::string& outSpellName) +{ + Player* bot = botAI->GetBot(); + AiObjectContext* aiContext = botAI->GetAiObjectContext(); + if (!aiContext) + return false; + + for (auto const& assigned : assignments) + { + if (assigned.blessing == BLESSING_NONE) + continue; + + BlessingType castType = assigned.blessing; + std::string spellName = BlessingSpellName(castType); + if (spellName.empty()) + continue; + + if (IsGreaterVariant(castType)) + { + uint32 spellId = aiContext->GetValue("spell id", spellName)->Get(); + if (!spellId || !ai::buff::HasRequiredReagents(bot, spellId)) + { + castType = ToSingleVariant(castType); + spellName = BlessingSpellName(castType); + if (spellName.empty()) + continue; + } + } + + uint32 spellId = aiContext->GetValue("spell id", spellName)->Get(); + if (!spellId) + continue; + + Player* target = FindMissingBlessingTarget( + botAI, assigned, castType, spellId, spellName); + if (!target) + continue; + + outAssignment = {target, assigned.blessing}; + outSpellName = spellName; + return true; + } + + return false; +} + +class GreaterBlessingPendingAssignmentValue : public CalculatedValue +{ +public: + GreaterBlessingPendingAssignmentValue(PlayerbotAI* botAI) + : CalculatedValue( + botAI, "greater blessing pending assignment", + GREATER_BLESSING_PENDING_ASSIGNMENT_CACHE_MS) {} + +protected: + CachedPendingBlessingAssignment Calculate() override + { + CachedPendingBlessingAssignment cached; + + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + cached.groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + std::vector assignments; + if (!GetCachedAssignments(this->context, group, assignments)) + return cached; + + if (!FindPendingAssignmentFromAssignments( + botAI, assignments, cached.assignment, cached.spellName)) + { + return cached; + } + + cached.valid = true; + return cached; + } +}; + +UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI) +{ + return new GreaterBlessingPendingAssignmentValue(botAI); +} + +} + +CastGreaterBlessingAssignmentAction::CastGreaterBlessingAssignmentAction( + PlayerbotAI* botAI) : Action(botAI, "cast greater blessing assignment") {} + +bool CastGreaterBlessingAssignmentAction::isUseful() +{ + return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()); +} + +bool CastGreaterBlessingAssignmentAction::HasPendingAssignment() +{ + ai::gbless::GreaterBlessingPlayerAssignment assignment; + std::string spellName; + + return FindPendingAssignment(assignment, spellName); +} + +bool CastGreaterBlessingAssignmentAction::Execute(Event /*event*/) +{ + ai::gbless::GreaterBlessingPlayerAssignment assignment; + std::string spellName; + if (!FindPendingAssignment(assignment, spellName)) + return false; + + uint32 finalId = AI_VALUE2(uint32, "spell id", spellName); + if (!finalId) + return false; + + return botAI->CastSpell(spellName, assignment.player); +} + +bool CastGreaterBlessingAssignmentAction::FindPendingAssignment( + ai::gbless::GreaterBlessingPlayerAssignment& outAssignment, std::string& outSpellName) +{ + Group* group = bot->GetGroup(); + uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + Value* pendingValue = + context->GetValue("greater blessing pending assignment"); + if (!pendingValue) + return false; + + ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get(); + if (pendingAssignment.groupKey != groupKey) + { + pendingValue->Reset(); + pendingAssignment = pendingValue->Get(); + } + + if (!pendingAssignment.valid || pendingAssignment.groupKey != groupKey) + return false; + + outAssignment = pendingAssignment.assignment; + outSpellName = pendingAssignment.spellName; + return true; +} diff --git a/src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.h b/src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.h new file mode 100644 index 000000000..fa8c66d4a --- /dev/null +++ b/src/Ai/Class/Paladin/Action/PaladinGreaterBlessingAction.h @@ -0,0 +1,267 @@ +/* + * 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_PALADINGREATERBLESSINGACTION_H +#define _PLAYERBOT_PALADINGREATERBLESSINGACTION_H + +#include +#include +#include + +#include "Action.h" +#include "AiFactory.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +class UntypedValue; + +namespace ai::gbless +{ + enum RoleProfile : uint8 + { + ROLE_CASTER = 0, + ROLE_PHYSICAL_DPS = 1, + ROLE_HYBRID_DPS = 2, + ROLE_DRUID_TANK = 3, + ROLE_WARRIOR_DK_TANK = 4, + ROLE_PALADIN_TANK = 5, + + ROLE_PROFILE_COUNT = 6 + }; + + enum BlessingType : uint8 + { + BLESSING_NONE = 0, + BLESSING_MIGHT_SINGLE = 1, + BLESSING_MIGHT_GREATER = 2, + BLESSING_WISDOM_SINGLE = 3, + BLESSING_WISDOM_GREATER = 4, + BLESSING_KINGS_SINGLE = 5, + BLESSING_KINGS_GREATER = 6, + BLESSING_SANCTUARY_SINGLE = 7, + BLESSING_SANCTUARY_GREATER = 8 + }; + + enum BaseBlessingCategory : uint8 + { + BASE_NONE = 0, + BASE_MIGHT = 1, + BASE_WISDOM = 2, + BASE_KINGS = 3, + BASE_SANCTUARY = 4 + }; + + inline constexpr BaseBlessingCategory BaseBlessingOf(BlessingType type) + { + switch (type) + { + case BLESSING_MIGHT_SINGLE: + case BLESSING_MIGHT_GREATER: return BASE_MIGHT; + case BLESSING_WISDOM_SINGLE: + case BLESSING_WISDOM_GREATER: return BASE_WISDOM; + case BLESSING_KINGS_SINGLE: + case BLESSING_KINGS_GREATER: return BASE_KINGS; + case BLESSING_SANCTUARY_SINGLE: + case BLESSING_SANCTUARY_GREATER: return BASE_SANCTUARY; + default: return BASE_NONE; + } + } + + inline constexpr bool IsSingleVariant(BlessingType type) + { + return type == BLESSING_MIGHT_SINGLE || type == BLESSING_WISDOM_SINGLE || + type == BLESSING_KINGS_SINGLE || type == BLESSING_SANCTUARY_SINGLE; + } + + inline constexpr bool IsGreaterVariant(BlessingType type) + { + return type == BLESSING_MIGHT_GREATER || type == BLESSING_WISDOM_GREATER || + type == BLESSING_KINGS_GREATER || type == BLESSING_SANCTUARY_GREATER; + } + + inline constexpr BlessingType ToSingleVariant(BaseBlessingCategory category) + { + switch (category) + { + case BASE_MIGHT: return BLESSING_MIGHT_SINGLE; + case BASE_WISDOM: return BLESSING_WISDOM_SINGLE; + case BASE_KINGS: return BLESSING_KINGS_SINGLE; + case BASE_SANCTUARY: return BLESSING_SANCTUARY_SINGLE; + default: return BLESSING_NONE; + } + } + + inline constexpr BlessingType ToSingleVariant(BlessingType type) + { + return ToSingleVariant(BaseBlessingOf(type)); + } + + inline constexpr BlessingType ToGreaterVariant(BaseBlessingCategory category) + { + switch (category) + { + case BASE_MIGHT: return BLESSING_MIGHT_GREATER; + case BASE_WISDOM: return BLESSING_WISDOM_GREATER; + case BASE_KINGS: return BLESSING_KINGS_GREATER; + case BASE_SANCTUARY: return BLESSING_SANCTUARY_GREATER; + default: return BLESSING_NONE; + } + } + + inline constexpr BlessingType ToGreaterVariant(BlessingType type) + { + return ToGreaterVariant(BaseBlessingOf(type)); + } + + inline std::string BlessingSpellName(BlessingType type) + { + switch (type) + { + case BLESSING_MIGHT_SINGLE: return "blessing of might"; + case BLESSING_MIGHT_GREATER: return "greater blessing of might"; + case BLESSING_WISDOM_SINGLE: return "blessing of wisdom"; + case BLESSING_WISDOM_GREATER: return "greater blessing of wisdom"; + case BLESSING_KINGS_SINGLE: return "blessing of kings"; + case BLESSING_KINGS_GREATER: return "greater blessing of kings"; + case BLESSING_SANCTUARY_SINGLE: return "blessing of sanctuary"; + case BLESSING_SANCTUARY_GREATER: return "greater blessing of sanctuary"; + default: return ""; + } + } + + struct BaseBlessingPriorityEntry + { + BaseBlessingCategory priorities[4]; + }; + + inline constexpr BaseBlessingPriorityEntry BASE_BLESSING_PRIORITIES[ROLE_PROFILE_COUNT] = + { + // All casters + {{ BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY, BASE_MIGHT }}, + // Physical DPS (no mana) + {{ BASE_MIGHT, BASE_KINGS, BASE_SANCTUARY, BASE_NONE }}, + // Hybrid DPS + {{ BASE_MIGHT, BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY }}, + // Druid tanks + {{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_WISDOM, }}, + // Warrior and DK tanks + {{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_NONE }}, + // Paladin tanks + {{ BASE_SANCTUARY, BASE_MIGHT, BASE_WISDOM, BASE_KINGS }}, + }; + + constexpr uint32 SPELL_IMPROVED_MIGHT_R1 = 20042; + constexpr uint32 SPELL_IMPROVED_MIGHT_R2 = 20045; + constexpr uint32 SPELL_IMPROVED_WISDOM_R1 = 20244; + constexpr uint32 SPELL_IMPROVED_WISDOM_R2 = 20245; + + inline RoleProfile ResolveRoleProfile(Player* player) + { + if (!player) + return ROLE_CASTER; + + uint8 cls = player->getClass(); + int tab = AiFactory::GetPlayerSpecTab(player); + bool isTank = PlayerbotAI::IsTank(player); + + switch (cls) + { + case CLASS_WARRIOR: + if (isTank) + return ROLE_WARRIOR_DK_TANK; + return ROLE_PHYSICAL_DPS; + + case CLASS_DEATH_KNIGHT: + if (isTank) + return ROLE_WARRIOR_DK_TANK; + return ROLE_PHYSICAL_DPS; + + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ENHANCEMENT) + return ROLE_HYBRID_DPS; + return ROLE_CASTER; + + case CLASS_PALADIN: + if (isTank) + return ROLE_PALADIN_TANK; + if (tab == PALADIN_TAB_HOLY) + return ROLE_CASTER; + return ROLE_HYBRID_DPS; + + case CLASS_DRUID: + if (tab == DRUID_TAB_FERAL) + return isTank ? ROLE_DRUID_TANK : ROLE_HYBRID_DPS; + return ROLE_CASTER; + + case CLASS_ROGUE: + return ROLE_PHYSICAL_DPS; + + case CLASS_HUNTER: + return ROLE_HYBRID_DPS; + + case CLASS_MAGE: + return ROLE_CASTER; + + case CLASS_WARLOCK: + return ROLE_CASTER; + + case CLASS_PRIEST: + return ROLE_CASTER; + + default: + return ROLE_CASTER; + } + } + + struct GreaterBlessingPlayerAssignment + { + Player* player = nullptr; + BlessingType blessing = BLESSING_NONE; + }; + + struct CachedBlessingBucketAssignment + { + uint8 classId = 0; + RoleProfile role = ROLE_CASTER; + bool byRole = false; + BlessingType blessing = BLESSING_NONE; + }; + + struct CachedBlessingAssignments + { + uint32 groupKey = 0; + bool valid = false; + std::vector assignments; + }; + + struct CachedPendingBlessingAssignment + { + uint32 groupKey = 0; + bool valid = false; + GreaterBlessingPlayerAssignment assignment; + std::string spellName; + }; + + UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI); + UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI); + bool IsEligibleGroupForAutoBlessings(Group const* group); +} + +class CastGreaterBlessingAssignmentAction : public Action +{ +public: + CastGreaterBlessingAssignmentAction(PlayerbotAI* botAI); + + bool Execute(Event event) override; + bool isUseful() override; + bool HasPendingAssignment(); + +private: + bool FindPendingAssignment( + ai::gbless::GreaterBlessingPlayerAssignment& outAssignment, + std::string& outSpellName); +}; + +#endif diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index a58ede3f8..d15a79453 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -7,6 +7,7 @@ #include "DpsPaladinStrategy.h" #include "GenericPaladinNonCombatStrategy.h" +#include "PaladinGreaterBlessingAction.h" #include "HealPaladinStrategy.h" #include "NamedObjectContext.h" #include "OffhealRetPaladinStrategy.h" @@ -70,17 +71,17 @@ class PaladinBuffStrategyFactoryInternal : public NamedObjectContext public: PaladinBuffStrategyFactoryInternal() : NamedObjectContext(false, true) { - creators["bhealth"] = &PaladinBuffStrategyFactoryInternal::bhealth; - creators["bmana"] = &PaladinBuffStrategyFactoryInternal::bmana; - creators["bdps"] = &PaladinBuffStrategyFactoryInternal::bdps; - creators["bstats"] = &PaladinBuffStrategyFactoryInternal::bstats; + creators["bsanc"] = &PaladinBuffStrategyFactoryInternal::bsanc; + creators["bwisdom"] = &PaladinBuffStrategyFactoryInternal::bwisdom; + creators["bmight"] = &PaladinBuffStrategyFactoryInternal::bmight; + creators["bkings"] = &PaladinBuffStrategyFactoryInternal::bkings; } private: - static Strategy* bhealth(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); } - static Strategy* bmana(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); } - static Strategy* bdps(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); } - static Strategy* bstats(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); } + static Strategy* bsanc(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); } + static Strategy* bwisdom(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); } + static Strategy* bmight(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); } + static Strategy* bkings(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); } }; class PaladinCombatStrategyFactoryInternal : public NamedObjectContext @@ -154,6 +155,7 @@ public: creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party; creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath; + creators["greater blessing needed"] = &PaladinTriggerFactoryInternal::greater_blessing_needed; } private: @@ -211,8 +213,8 @@ private: static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); } static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); } 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* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new BeaconOfLightOnMainTankTrigger(botAI); } + static Trigger* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new SacredShieldOnMainTankTrigger(botAI); } 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); } @@ -227,6 +229,10 @@ private: } static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); } + static Trigger* greater_blessing_needed(PlayerbotAI* botAI) + { + return new GreaterBlessingNeededTrigger(botAI); + } }; class PaladinAiObjectContextInternal : public NamedObjectContext @@ -316,6 +322,8 @@ public: 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; + creators["cast greater blessing assignment"] = + &PaladinAiObjectContextInternal::cast_greater_blessing_assignment; } private: @@ -414,15 +422,41 @@ private: static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); } static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); } static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); } - static Action* divine_plea(PlayerbotAI* ai) { return new CastDivinePleaAction(ai); } - static Action* shield_of_righteousness(PlayerbotAI* ai) { return new ShieldOfRighteousnessAction(ai); } - static Action* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new CastBeaconOfLightOnMainTankAction(ai); } - static Action* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new CastSacredShieldOnMainTankAction(ai); } - static Action* avenging_wrath(PlayerbotAI* ai) { return new CastAvengingWrathAction(ai); } - 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); } + static Action* divine_plea(PlayerbotAI* botAI) { return new CastDivinePleaAction(botAI); } + static Action* shield_of_righteousness(PlayerbotAI* botAI) { return new ShieldOfRighteousnessAction(botAI); } + static Action* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new CastBeaconOfLightOnMainTankAction(botAI); } + static Action* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new CastSacredShieldOnMainTankAction(botAI); } + static Action* avenging_wrath(PlayerbotAI* botAI) { return new CastAvengingWrathAction(botAI); } + static Action* divine_illumination(PlayerbotAI* botAI) { return new CastDivineIlluminationAction(botAI); } + static Action* divine_sacrifice(PlayerbotAI* botAI) { return new CastDivineSacrificeAction(botAI); } + static Action* cancel_divine_sacrifice(PlayerbotAI* botAI) { return new CastCancelDivineSacrificeAction(botAI); } + static Action* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new CastHandOfFreedomOnPartyAction(botAI); } + static Action* cast_greater_blessing_assignment(PlayerbotAI* botAI) + { + return new CastGreaterBlessingAssignmentAction(botAI); + } +}; + +class PaladinValueContextInternal : public NamedObjectContext +{ +public: + PaladinValueContextInternal() + { + creators["greater blessing assignments"] = &PaladinValueContextInternal::greater_blessing_assignments; + creators["greater blessing pending assignment"] = + &PaladinValueContextInternal::greater_blessing_pending_assignment; + } + +private: + static UntypedValue* greater_blessing_assignments(PlayerbotAI* botAI) + { + return ai::gbless::greater_blessing_assignments_value(botAI); + } + + static UntypedValue* greater_blessing_pending_assignment(PlayerbotAI* botAI) + { + return ai::gbless::greater_blessing_pending_assignment_value(botAI); + } }; SharedNamedObjectContextList PaladinAiObjectContext::sharedStrategyContexts; @@ -467,4 +501,5 @@ void PaladinAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContext void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) { AiObjectContext::BuildSharedValueContexts(valueContexts); + valueContexts.Add(new PaladinValueContextInternal()); } diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp index 84f449813..5b3e697d6 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp @@ -30,4 +30,7 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector& tr triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) })); if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION) triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) })); + + triggers.push_back(new TriggerNode("greater blessing needed", + { NextAction("cast greater blessing assignment", ACTION_NORMAL) })); } diff --git a/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h b/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h index 25f8886a3..e3866d6f6 100644 --- a/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h +++ b/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h @@ -16,7 +16,7 @@ public: PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bmana"; } + std::string const getName() override { return "bwisdom"; } }; class PaladinBuffHealthStrategy : public Strategy @@ -25,7 +25,7 @@ public: PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bhealth"; } + std::string const getName() override { return "bsanc"; } }; class PaladinBuffDpsStrategy : public Strategy @@ -34,7 +34,7 @@ public: PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bdps"; } + std::string const getName() override { return "bmight"; } }; class PaladinBuffArmorStrategy : public Strategy @@ -88,7 +88,7 @@ public: PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bstats"; } + std::string const getName() override { return "bkings"; } }; class PaladinShadowResistanceStrategy : public Strategy diff --git a/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp index b76116e04..39d67e14e 100644 --- a/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp @@ -95,13 +95,14 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) } ) ); - triggers.push_back(new TriggerNode( - "light aoe", - { - NextAction("avenger's shield", ACTION_HIGH + 5) - } - ) -); + triggers.push_back( + new TriggerNode( + "light aoe", + { + NextAction("avenger's shield", ACTION_HIGH + 5) + } + ) + ); triggers.push_back( new TriggerNode( "medium aoe", @@ -122,13 +123,6 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode( "medium health", - { NextAction("holy shield", ACTION_HIGH + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "low health", { NextAction("holy shield", ACTION_HIGH + 4) } @@ -136,20 +130,12 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "critical health", + "avenging wrath", { - NextAction("holy shield", ACTION_HIGH + 4) + NextAction("avenging wrath", ACTION_HIGH + 2) } ) ); - triggers.push_back( - new TriggerNode( - "avenging wrath", - { - NextAction("avenging wrath", ACTION_HIGH + 2) - } - ) -); triggers.push_back( new TriggerNode( "target critical health", diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index 46a2d8a94..79459b682 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -5,10 +5,11 @@ #include "PaladinTriggers.h" +#include "GenericBuffUtils.h" +#include "PaladinGreaterBlessingAction.h" #include "PaladinActions.h" -#include "PlayerbotAIConfig.h" -#include "Playerbots.h" #include "PaladinHelper.h" +#include "Playerbots.h" bool SealTrigger::IsActive() { @@ -28,8 +29,9 @@ bool CrusaderAuraTrigger::IsActive() bool BlessingTrigger::IsActive() { Unit* target = GetTarget(); - return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", - "blessing of kings", "blessing of sanctuary", nullptr); + return SpellTrigger::IsActive() && + !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", + "blessing of kings", "blessing of sanctuary", nullptr); } bool DivineShieldLowHealthTrigger::IsActive() @@ -62,7 +64,8 @@ bool HandOfFreedomOnPartyTrigger::IsActive() if (!target) return false; - if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) + if (target != bot && + bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) return false; if (!botAI->CanCastSpell("hand of freedom", target)) @@ -75,3 +78,29 @@ bool NotSensingUndeadTrigger::IsActive() { return !botAI->HasAura("sense undead", bot); } + +bool GreaterBlessingNeededTrigger::IsActive() +{ + if (!ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup())) + return false; + + if (ai::buff::ShouldDeferGreaterBlessingAssignmentForRecentLogin(bot)) + return false; + + Group* group = bot->GetGroup(); + uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + Value* pendingValue = + context->GetValue("greater blessing pending assignment"); + if (!pendingValue) + return false; + + ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get(); + if (pendingAssignment.groupKey != groupKey) + { + pendingValue->Reset(); + pendingAssignment = pendingValue->Get(); + } + + return pendingAssignment.valid && pendingAssignment.groupKey == groupKey; +} diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index d11c8024f..28f4d75cb 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -13,32 +13,6 @@ class PlayerbotAI; -inline std::string const GetActualBlessingOfMight(Unit* target) -{ - switch (target->getClass()) - { - case CLASS_MAGE: - case CLASS_PRIEST: - case CLASS_WARLOCK: - return "blessing of wisdom"; - } - - return "blessing of might"; -} - -inline std::string const GetActualBlessingOfWisdom(Unit* target) -{ - switch (target->getClass()) - { - case CLASS_WARRIOR: - case CLASS_ROGUE: - case CLASS_DEATH_KNIGHT: - return "blessing of might"; - } - - return "blessing of wisdom"; -} - BUFF_TRIGGER(HolyShieldTrigger, "holy shield"); BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury"); @@ -212,42 +186,55 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield"); class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger { public: - BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) - : BuffOnMainTankTrigger(ai, "beacon of light", true) {} + BeaconOfLightOnMainTankTrigger(PlayerbotAI* botAI) + : BuffOnMainTankTrigger(botAI, "beacon of light", true) {} }; class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger { public: - SacredShieldOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "sacred shield", false) {} + SacredShieldOnMainTankTrigger(PlayerbotAI* botAI) + : BuffOnMainTankTrigger(botAI, "sacred shield", false) {} }; -class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfKingsOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} + : BlessingOnPartyTrigger(botAI) + { + spell = "blessing of kings"; + } }; -class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfWisdomOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} + : BlessingOnPartyTrigger(botAI) + { + spell = "blessing of might,blessing of wisdom"; + } }; -class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfMightOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} + : BlessingOnPartyTrigger(botAI) + { + spell = "blessing of might,blessing of wisdom"; + } }; -class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfSanctuaryOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} + : BlessingOnPartyTrigger(botAI) + { + spell = "blessing of sanctuary"; + } }; class HandOfFreedomOnPartyTrigger : public Trigger @@ -266,4 +253,13 @@ public: AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} }; +class GreaterBlessingNeededTrigger : public Trigger +{ +public: + GreaterBlessingNeededTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "greater blessing needed", 4) {} + + bool IsActive() override; +}; + #endif diff --git a/src/Ai/Class/Paladin/Util/PaladinHelper.h b/src/Ai/Class/Paladin/Util/PaladinHelper.h index 64b88b731..8e9a155f7 100644 --- a/src/Ai/Class/Paladin/Util/PaladinHelper.h +++ b/src/Ai/Class/Paladin/Util/PaladinHelper.h @@ -18,6 +18,8 @@ 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; +static constexpr uint32 SPELL_BLESSING_OF_SANCTUARY = 20911; +static constexpr uint32 SPELL_GREATER_BLESSING_OF_SANCTUARY = 25899; inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list spellIds) { diff --git a/src/Ai/Class/Priest/Action/PriestActions.h b/src/Ai/Class/Priest/Action/PriestActions.h index 4e94f27cc..0fe0a832f 100644 --- a/src/Ai/Class/Priest/Action/PriestActions.h +++ b/src/Ai/Class/Priest/Action/PriestActions.h @@ -13,9 +13,19 @@ class PlayerbotAI; // disc -BUFF_ACTION(CastPowerWordFortitudeAction, "power word: fortitude"); -BUFF_PARTY_ACTION(CastPowerWordFortitudeOnPartyAction, "power word: fortitude"); -BUFF_PARTY_ACTION(CastPrayerOfFortitudeOnPartyAction, "prayer of fortitude"); +class CastPowerWordFortitudeAction : public GroupBuffSpellAction +{ +public: + CastPowerWordFortitudeAction(PlayerbotAI* botAI) + : GroupBuffSpellAction(botAI, "power word: fortitude") {} +}; + +class CastPowerWordFortitudeOnPartyAction : public GroupBuffOnPartyAction +{ +public: + CastPowerWordFortitudeOnPartyAction(PlayerbotAI* botAI) + : GroupBuffOnPartyAction(botAI, "power word: fortitude") {} +}; BUFF_ACTION(CastPowerWordShieldAction, "power word: shield"); BUFF_ACTION(CastInnerFireAction, "inner fire"); @@ -26,9 +36,19 @@ CC_ACTION(CastShackleUndeadAction, "shackle undead"); SPELL_ACTION_U(CastManaBurnAction, "mana burn", AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20); BUFF_ACTION(CastLevitateAction, "levitate"); -BUFF_ACTION(CastDivineSpiritAction, "divine spirit"); -BUFF_PARTY_ACTION(CastDivineSpiritOnPartyAction, "divine spirit"); -BUFF_PARTY_ACTION(CastPrayerOfSpiritOnPartyAction, "prayer of spirit"); +class CastDivineSpiritAction : public GroupBuffSpellAction +{ +public: + CastDivineSpiritAction(PlayerbotAI* botAI) + : GroupBuffSpellAction(botAI, "divine spirit") {} +}; + +class CastDivineSpiritOnPartyAction : public GroupBuffOnPartyAction +{ +public: + CastDivineSpiritOnPartyAction(PlayerbotAI* botAI) + : GroupBuffOnPartyAction(botAI, "divine spirit") {} +}; // disc 2.4.3 SPELL_ACTION(CastMassDispelAction, "mass dispel"); @@ -103,9 +123,19 @@ SPELL_ACTION(CastMindBlastAction, "mind blast"); SPELL_ACTION(CastPsychicScreamAction, "psychic scream"); DEBUFF_ACTION(CastMindSootheAction, "mind soothe"); BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup()); -BUFF_ACTION(CastShadowProtectionAction, "shadow protection"); -BUFF_PARTY_ACTION(CastShadowProtectionOnPartyAction, "shadow protection"); -BUFF_PARTY_ACTION(CastPrayerOfShadowProtectionAction, "prayer of shadow protection"); +class CastShadowProtectionAction : public GroupBuffSpellAction +{ +public: + CastShadowProtectionAction(PlayerbotAI* botAI) + : GroupBuffSpellAction(botAI, "shadow protection") {} +}; + +class CastShadowProtectionOnPartyAction : public GroupBuffOnPartyAction +{ +public: + CastShadowProtectionOnPartyAction(PlayerbotAI* botAI) + : GroupBuffOnPartyAction(botAI, "shadow protection") {} +}; // shadow talents SPELL_ACTION(CastMindFlayAction, "mind flay"); diff --git a/src/Ai/Class/Priest/PriestAiObjectContext.cpp b/src/Ai/Class/Priest/PriestAiObjectContext.cpp index 7ffac96cc..0187f8381 100644 --- a/src/Ai/Class/Priest/PriestAiObjectContext.cpp +++ b/src/Ai/Class/Priest/PriestAiObjectContext.cpp @@ -92,8 +92,6 @@ public: creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection; creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party; creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead; - creators["prayer of fortitude on party"] = &PriestTriggerFactoryInternal::prayer_of_fortitude_on_party; - creators["prayer of spirit on party"] = &PriestTriggerFactoryInternal::prayer_of_spirit_on_party; creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire; creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness; creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness; @@ -136,8 +134,6 @@ private: static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); } static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); } static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); } - static Trigger* prayer_of_fortitude_on_party(PlayerbotAI* botAI) { return new PrayerOfFortitudeTrigger(botAI); } - static Trigger* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new PrayerOfSpiritTrigger(botAI); } static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); } static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); } static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); } @@ -207,8 +203,6 @@ public: creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection; creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party; creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead; - creators["prayer of fortitude on party"] = &PriestAiObjectContextInternal::prayer_of_fortitude_on_party; - creators["prayer of spirit on party"] = &PriestAiObjectContextInternal::prayer_of_spirit_on_party; creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party; creators["silence"] = &PriestAiObjectContextInternal::silence; creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer; @@ -311,11 +305,6 @@ private: static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); } static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); } static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); } - static Action* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new CastPrayerOfSpiritOnPartyAction(botAI); } - static Action* prayer_of_fortitude_on_party(PlayerbotAI* botAI) - { - return new CastPrayerOfFortitudeOnPartyAction(botAI); - } static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); } static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); } static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); } diff --git a/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp b/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp index 6447d4519..1caa6f8e0 100644 --- a/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp +++ b/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp @@ -54,12 +54,6 @@ void PriestBuffStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("prayer of fortitude on party", - { NextAction("prayer of fortitude on party", 12.0f) })); - triggers.push_back( - new TriggerNode("prayer of spirit on party", - { NextAction("prayer of spirit on party", 14.0f) })); triggers.push_back( new TriggerNode("power word: fortitude on party", { NextAction("power word: fortitude on party", 11.0f) })); diff --git a/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h b/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h index c7d4aba82..66a6a44c5 100644 --- a/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h +++ b/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h @@ -30,8 +30,6 @@ public: creators["flash heal"] = &flash_heal; creators["flash heal on party"] = &flash_heal_on_party; creators["circle of healing on party"] = &circle_of_healing; - creators["prayer of fortitude on party"] = &prayer_of_fortitude_on_party; - creators["prayer of spirit on party"] = &prayer_of_spirit_on_party; } private: @@ -134,20 +132,6 @@ private: /*A*/ {}, /*C*/ {}); } - static ActionNode* prayer_of_fortitude_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("prayer of fortitude on party", - /*P*/ { NextAction("remove shadowform") }, - /*A*/ { NextAction("power word: fortitude on party") }, - /*C*/ {}); - } - static ActionNode* prayer_of_spirit_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("prayer of spirit on party", - /*P*/ { NextAction("remove shadowform") }, - /*A*/ { NextAction("divine spirit on party") }, - /*C*/ {}); - } }; #endif diff --git a/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp b/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp index 7f61ba6ee..ff108a9e1 100644 --- a/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp +++ b/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp @@ -8,10 +8,9 @@ #include "Player.h" #include "Playerbots.h" -bool PowerWordFortitudeOnPartyTrigger::IsActive() +bool ShadowProtectionTrigger::IsActive() { - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("power word : fortitude", GetTarget()) && - !botAI->HasAura("prayer of fortitude", GetTarget()); + return BuffTrigger::IsActive() && !botAI->HasAura("prayer of shadow protection", GetTarget()); } bool PowerWordFortitudeTrigger::IsActive() @@ -20,43 +19,12 @@ bool PowerWordFortitudeTrigger::IsActive() !botAI->HasAura("prayer of fortitude", GetTarget()); } -bool DivineSpiritOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) && - !botAI->HasAura("prayer of spirit", GetTarget()); -} - bool DivineSpiritTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) && !botAI->HasAura("prayer of spirit", GetTarget()); } -bool PrayerOfFortitudeTrigger::IsActive() -{ - Unit* target = GetTarget(); - if (!target || !target->IsPlayer()) - return false; - - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of fortitude", GetTarget()) && - botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) && - botAI->GetBuffedCount((Player*)GetTarget(), "prayer of fortitude") < 4 && - !botAI->GetBuffedCount((Player*)GetTarget(), "power word: fortitude"); -} - -bool PrayerOfSpiritTrigger::IsActive() -{ - Unit* target = GetTarget(); - if (!target || !target->IsPlayer()) - return false; - - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of spirit", GetTarget()) && - botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) && - // botAI->GetManaPercent() > 50 && - botAI->GetBuffedCount((Player*)GetTarget(), "prayer of spirit") < 4 && - !botAI->GetBuffedCount((Player*)GetTarget(), "divine spirit"); -} - bool InnerFireTrigger::IsActive() { Unit* target = GetTarget(); diff --git a/src/Ai/Class/Priest/Trigger/PriestTriggers.h b/src/Ai/Class/Priest/Trigger/PriestTriggers.h index 5ae91985d..fc5b1f8cc 100644 --- a/src/Ai/Class/Priest/Trigger/PriestTriggers.h +++ b/src/Ai/Class/Priest/Trigger/PriestTriggers.h @@ -27,8 +27,6 @@ BUFF_TRIGGER_A(InnerFireTrigger, "inner fire"); BUFF_TRIGGER_A(ShadowformTrigger, "shadowform"); BOOST_TRIGGER(PowerInfusionTrigger, "power infusion"); BUFF_TRIGGER(InnerFocusTrigger, "inner focus"); -BUFF_TRIGGER(ShadowProtectionTrigger, "shadow protection"); -BUFF_PARTY_TRIGGER(ShadowProtectionOnPartyTrigger, "shadow protection"); CC_TRIGGER(ShackleUndeadTrigger, "shackle undead"); INTERRUPT_TRIGGER(SilenceTrigger, "silence"); INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence"); @@ -44,20 +42,34 @@ SNARE_TRIGGER(ChastiseTrigger, "chastise"); BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend"); +class ShadowProtectionTrigger : public BuffTrigger +{ +public: + ShadowProtectionTrigger(PlayerbotAI* botAI) + : BuffTrigger(botAI, "shadow protection", 4 * 2000) {} + + bool IsActive() override; +}; + +class ShadowProtectionOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + ShadowProtectionOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "shadow protection", 4 * 2000) {} +}; + class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger { public: - PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) - { - } - - bool IsActive() override; + PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) {} }; class PowerWordFortitudeTrigger : public BuffTrigger { public: - PowerWordFortitudeTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {} + PowerWordFortitudeTrigger(PlayerbotAI* botAI) + : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {} bool IsActive() override; }; @@ -65,31 +77,15 @@ public: class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger { public: - DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {} - - bool IsActive() override; + DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {} }; class DivineSpiritTrigger : public BuffTrigger { public: - DivineSpiritTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine spirit", 4 * 2000) {} - - bool IsActive() override; -}; - -class PrayerOfFortitudeTrigger : public BuffOnPartyTrigger -{ -public: - PrayerOfFortitudeTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of fortitude", 3 * 2000) {} - - bool IsActive() override; -}; - -class PrayerOfSpiritTrigger : public BuffOnPartyTrigger -{ -public: - PrayerOfSpiritTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of spirit", 2 * 2000) {} + DivineSpiritTrigger(PlayerbotAI* botAI) + : BuffTrigger(botAI, "divine spirit", 4 * 2000) {} bool IsActive() override; }; @@ -106,9 +102,7 @@ class MindSearChannelCheckTrigger : public Trigger { public: MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2) - : Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) - { - } + : Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) {} bool IsActive() override; diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index f1fd2491f..b3abbd8ad 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -500,21 +500,21 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const switch (player->getClass()) { case CLASS_PRIEST: - nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr); + nonCombatEngine->addStrategiesNoInit("dps assist", "cure", "rshadow", nullptr); break; case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION) { nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr); if (player->GetLevel() >= 20) - nonCombatEngine->addStrategy("bhealth", false); + nonCombatEngine->addStrategy("bsanc", false); else - nonCombatEngine->addStrategy("bdps", false); + nonCombatEngine->addStrategy("bmight", false); } else if (tab == PALADIN_TAB_HOLY) - nonCombatEngine->addStrategiesNoInit("dps assist", "bmana", "bcast", nullptr); + nonCombatEngine->addStrategiesNoInit("dps assist", "bwisdom", "bcast", nullptr); else - nonCombatEngine->addStrategiesNoInit("dps assist", "bdps", "baoe", nullptr); + nonCombatEngine->addStrategiesNoInit("dps assist", "bmight", "baoe", nullptr); nonCombatEngine->addStrategiesNoInit("cure", nullptr); break; diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index e1096b0cc..8bd1f2c2f 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -5971,29 +5971,6 @@ void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot) LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str()); } -uint32 PlayerbotAI::GetBuffedCount(Player* player, std::string const spellname) -{ - uint32 bcount = 0; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) - { - Player* member = gref->GetSource(); - if (!member || !member->IsInWorld()) - continue; - - if (!member->IsInSameRaidWith(player)) - continue; - - if (HasAura(spellname, member, true)) - bcount++; - } - } - - return bcount; -} - int32 PlayerbotAI::GetNearGroupMemberCount(float dis) { int count = 1; // yourself diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 01924c46f..2e31c9a9e 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -493,7 +493,6 @@ public: void ImbueItem(Item* item, Unit* target); void ImbueItem(Item* item); void EnchantItemT(uint32 spellid, uint8 slot); - uint32 GetBuffedCount(Player* player, std::string const spellname); int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance); virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index af08edf64..fd230cb9b 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -84,8 +84,6 @@ bool PlayerbotAIConfig::Initialize() sitDelay = sConfigMgr->GetOption("AiPlayerbot.SitDelay", 20000); returnDelay = sConfigMgr->GetOption("AiPlayerbot.ReturnDelay", 2000); lootDelay = sConfigMgr->GetOption("AiPlayerbot.LootDelay", 1000); - minBotsForGreaterBuff = sConfigMgr->GetOption("AiPlayerbot.MinBotsForGreaterBuff", 3); - rpWarningCooldown = sConfigMgr->GetOption("AiPlayerbot.RPWarningCooldown", 30); disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30); disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300); @@ -116,6 +114,32 @@ bool PlayerbotAIConfig::Initialize() highMana = sConfigMgr->GetOption("AiPlayerbot.HighMana", 65); autoSaveMana = sConfigMgr->GetOption("AiPlayerbot.AutoSaveMana", true); saveManaThreshold = sConfigMgr->GetOption("AiPlayerbot.SaveManaThreshold", 60); + switch (sConfigMgr->GetOption("AiPlayerbot.AutoGreaterBlessings", 1)) + { + case 0: + autoGreaterBlessings = AutoPartyBuffMode::DISABLED; + break; + case 2: + autoGreaterBlessings = AutoPartyBuffMode::GROUP_OR_RAID; + break; + case 1: + default: + autoGreaterBlessings = AutoPartyBuffMode::RAID_ONLY; + break; + } + switch (sConfigMgr->GetOption("AiPlayerbot.AutoPartyBuffs", 2)) + { + case 0: + autoPartyBuffs = AutoPartyBuffMode::DISABLED; + break; + case 1: + autoPartyBuffs = AutoPartyBuffMode::RAID_ONLY; + break; + case 2: + default: + autoPartyBuffs = AutoPartyBuffMode::GROUP_OR_RAID; + break; + } autoAvoidAoe = sConfigMgr->GetOption("AiPlayerbot.AutoAvoidAoe", true); maxAoeAvoidRadius = sConfigMgr->GetOption("AiPlayerbot.MaxAoeAvoidRadius", 15.0f); LoadSet>(sConfigMgr->GetOption("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"), diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 715701231..267042271 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -40,6 +40,13 @@ enum class HealingManaEfficiency : uint8 SUPERIOR = 32 }; +enum class AutoPartyBuffMode : uint8 +{ + DISABLED = 0, + RAID_ONLY = 1, + GROUP_OR_RAID = 2 +}; + enum NewRpgStatus : int { //Initial Status @@ -94,6 +101,8 @@ public: uint32 lowMana, mediumMana, highMana; bool autoSaveMana; uint32 saveManaThreshold; + AutoPartyBuffMode autoGreaterBlessings; + AutoPartyBuffMode autoPartyBuffs; bool autoAvoidAoe; float maxAoeAvoidRadius; std::set aoeAvoidSpellWhitelist; @@ -146,12 +155,6 @@ 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; - // Professions bool enableFishingWithMaster; uint32 classMatchingProfessionChance;