/* EQEmu: EQEmulator Copyright (C) 2001-2026 EQEmu Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "bot.h" #include "common/data_verification.h" #include "common/repositories/bot_spells_entries_repository.h" #include "common/repositories/npc_spells_repository.h" bool Bot::AICastSpell(Mob* tar, uint8 chance, uint16 spell_type, uint16 sub_target_type, uint16 sub_type) { if (!tar) { return false; } LogBotSpellChecksDetail("{} says, 'Attempting {} AICastSpell on {}.'", GetCleanName(), GetSpellTypeNameByID(spell_type), tar->GetCleanName()); if ( !AI_HasSpells() || (spell_type == BotSpellTypes::Pet && tar != this) || (IsPetBotSpellType(spell_type) && !tar->IsPet()) || (!IsPetBotSpellType(spell_type) && tar->IsPet()) || (!RuleB(Bots, AllowBuffingHealingFamiliars) && tar->IsFamiliar()) || (tar->IsPet() && tar->IsCharmed() && spell_type == BotSpellTypes::PetBuffs && !RuleB(Bots, AllowCharmedPetBuffs)) || (tar->IsPet() && tar->IsCharmed() && spell_type == BotSpellTypes::PetCures && !RuleB(Bots, AllowCharmedPetCures)) || (tar->IsPet() && tar->IsCharmed() && IsHealBotSpellType(spell_type) && !RuleB(Bots, AllowCharmedPetHeals)) ) { return false; } if ( !IsCommandedSpell() && zone->random.Int(0, 100) > chance ) { return false; } if ((spell_type != BotSpellTypes::Resurrect && spell_type != BotSpellTypes::SummonCorpse) && tar->GetAppearance() == eaDead) { if (!((tar->IsClient() && tar->CastToClient()->GetFeigned()) || tar->IsBot())) { return false; } } uint8 bot_class = GetClass(); SetCastedSpellType(UINT16_MAX); // this is for recast timers SetTempSpellType(spell_type); // this is for spell checks BotSpell bot_spell; bot_spell.SpellId = 0; bot_spell.SpellIndex = 0; bot_spell.ManaCost = 0; if (!BotSpellTypeRequiresLoS(spell_type) || tar == this) { SetHasLoS(true); } switch (spell_type) { case BotSpellTypes::Slow: if (tar->GetSpecialAbility(SpecialAbility::SlowImmunity)) { return false; } break; case BotSpellTypes::Snare: if (tar->GetSpecialAbility(SpecialAbility::SnareImmunity)) { return false; } break; case BotSpellTypes::AELull: case BotSpellTypes::Lull: if (tar->GetSpecialAbility(SpecialAbility::PacifyImmunity)) { return false; } break; case BotSpellTypes::Fear: if (tar->GetSpecialAbility(SpecialAbility::FearImmunity)) { return false; } if (!IsCommandedSpell() && (tar->IsRooted() || tar->GetSnaredAmount() == -1)) { return false; } break; case BotSpellTypes::Dispel: if (tar->GetSpecialAbility(SpecialAbility::DispellImmunity)) { return false; } if (!IsCommandedSpell() && tar->CountDispellableBuffs() <= 0) { return false; } break; case BotSpellTypes::HateRedux: if (!IsCommandedSpell() && !GetNeedsHateRedux(tar)) { return false; } break; case BotSpellTypes::InCombatBuff: if (!IsCommandedSpell() && GetClass() != Class::Shaman && spell_type == BotSpellTypes::InCombatBuff && IsCasterClass(GetClass()) && GetLevel() >= GetStopMeleeLevel()) { return false; } break; case BotSpellTypes::HateLine: if (!tar->IsNPC()) { return false; } break; case BotSpellTypes::InCombatBuffSong: case BotSpellTypes::Buff: case BotSpellTypes::PetBuffs: case BotSpellTypes::PreCombatBuff: case BotSpellTypes::DamageShields: case BotSpellTypes::PetDamageShields: case BotSpellTypes::ResistBuffs: case BotSpellTypes::PetResistBuffs: case BotSpellTypes::Teleport: case BotSpellTypes::Succor: case BotSpellTypes::BindAffinity: case BotSpellTypes::Identify: case BotSpellTypes::Levitate: case BotSpellTypes::Rune: case BotSpellTypes::WaterBreathing: case BotSpellTypes::Size: case BotSpellTypes::Invisibility: case BotSpellTypes::MovementSpeed: case BotSpellTypes::SendHome: if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { return false; } break; case BotSpellTypes::PreCombatBuffSong: case BotSpellTypes::OutOfCombatBuffSong: if (!IsCommandedSpell() && (IsEngaged() || tar->IsEngaged())) { // Out-of-Combat songs can not be cast in combat return false; } break; case BotSpellTypes::AEMez: case BotSpellTypes::Mez: return BotCastMez(tar, bot_class, bot_spell, spell_type); case BotSpellTypes::AENukes: case BotSpellTypes::AERains: case BotSpellTypes::PBAENuke: case BotSpellTypes::Nuke: case BotSpellTypes::AEStun: case BotSpellTypes::Stun: return BotCastNuke(tar, bot_class, bot_spell, spell_type); case BotSpellTypes::RegularHeal: case BotSpellTypes::GroupCompleteHeals: case BotSpellTypes::CompleteHeal: case BotSpellTypes::FastHeals: case BotSpellTypes::VeryFastHeals: case BotSpellTypes::GroupHeals: case BotSpellTypes::GroupHoTHeals: case BotSpellTypes::HoTHeals: case BotSpellTypes::PetRegularHeals: case BotSpellTypes::PetCompleteHeals: case BotSpellTypes::PetFastHeals: case BotSpellTypes::PetVeryFastHeals: case BotSpellTypes::PetHoTHeals: if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { return false; } return BotCastHeal(tar, bot_class, bot_spell, spell_type); case BotSpellTypes::GroupCures: case BotSpellTypes::Cure: case BotSpellTypes::PetCures: if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { return false; } return BotCastCure(tar, bot_class, bot_spell, spell_type); case BotSpellTypes::Pet: if (HasPet() || IsBotCharmer()) { return false; } return BotCastPet(tar, bot_class, bot_spell, spell_type); case BotSpellTypes::Resurrect: if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { return false; } break; case BotSpellTypes::Charm: if (HasPet() || tar->IsCharmed() || !tar->IsNPC() || tar->GetSpecialAbility(SpecialAbility::CharmImmunity)) { return false; } break; default: break; } std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, (IsAEBotSpellType(spell_type) || sub_target_type == CommandedSubTypes::AETarget), sub_target_type, sub_type); for (const auto& s : bot_spell_list) { if (!IsValidSpell(s.SpellId)) { continue; } if (BotRequiresLoSToCast(spell_type, s.SpellId) && !HasLoS()) { continue; } if (IsInvulnerabilitySpell(s.SpellId)) { tar = this; //target self for invul type spells } if (IsCommandedSpell() && IsCasting()) { RaidGroupSay( fmt::format( "Interrupting {}. I have been commanded to try to cast a [{}] spell, {} on {}.", CastingSpellID() ? spells[CastingSpellID()].name : "my spell", GetSpellTypeNameByID(spell_type), spells[s.SpellId].name, tar->GetCleanName() ).c_str() ); InterruptSpell(); } if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { if (BotSpellTypeUsesTargetSettings(spell_type)) { SetCastedSpellType(UINT16_MAX); if (!IsCommandedSpell()) { SetBotSpellRecastTimer(spell_type, tar, true); } } else { SetCastedSpellType(spell_type); } RaidGroupSay( fmt::format( "Casting {} [{}] on {}.", GetSpellName(s.SpellId), GetSpellTypeNameByID(spell_type), (tar == this ? "myself" : tar->GetCleanName()) ).c_str() ); return true; } } return false; } bool Bot::BotCastMez(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, IsAEBotSpellType(spell_type)); for (const auto& s : bot_spell_list) { if (!IsValidSpell(s.SpellId)) { continue; } if (BotRequiresLoSToCast(spell_type, s.SpellId) && !HasLoS()) { continue; } if (!IsCommandedSpell()) { Mob* add_mob = GetFirstIncomingMobToMez(this, s.SpellId, spell_type, IsAEBotSpellType(spell_type)); if (!add_mob) { continue; } tar = add_mob; } if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { if (BotSpellTypeUsesTargetSettings(spell_type)) { SetCastedSpellType(UINT16_MAX); if (!IsCommandedSpell()) { SetBotSpellRecastTimer(spell_type, tar, true); } } else { SetCastedSpellType(spell_type); } RaidGroupSay( fmt::format( "Casting {} [{}] on {}.", GetSpellName(s.SpellId), GetSpellTypeNameByID(spell_type), (tar == this ? "myself" : tar->GetCleanName()) ).c_str() ); return true; } } return false; } bool Bot::BotCastCure(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { uint32 current_time = Timer::GetCurrentTime(); uint32 next_cure_time = tar->DontCureMeBefore(); if (!IsCommandedSpell()) { if ((next_cure_time > current_time) || !GetNeedsCured(tar)) { return false; } } bot_spell = GetBestBotSpellForCure(this, tar, spell_type); if (!IsValidSpell(bot_spell.SpellId)) { return false; } if (BotRequiresLoSToCast(spell_type, bot_spell.SpellId) && !HasLoS()) { return false; } if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { if (IsGroupSpell(bot_spell.SpellId)) { if (!IsCommandedSpell()) { for (Mob* m : GatherSpellTargets(false, tar)) { SetBotSpellRecastTimer(spell_type, m, true); } } RaidGroupSay( fmt::format( "Curing the group with {}.", GetSpellName(bot_spell.SpellId) ).c_str() ); } else { if (!IsCommandedSpell()) { SetBotSpellRecastTimer(spell_type, tar, true); } RaidGroupSay( fmt::format( "Curing {} with {}.", (tar == this ? "myself" : tar->GetCleanName()), GetSpellName(bot_spell.SpellId) ).c_str() ); } return true; } return false; } bool Bot::BotCastPet(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { if (bot_class == Class::Wizard) { auto buffs_max = GetMaxBuffSlots(); auto my_buffs = GetBuffs(); int familiar_buff_slot = -1; if (buffs_max && my_buffs) { for (int index = 0; index < buffs_max; ++index) { if (IsEffectInSpell(my_buffs[index].spellid, SpellEffect::Familiar)) { MakePet(my_buffs[index].spellid, spells[my_buffs[index].spellid].teleport_zone); familiar_buff_slot = index; break; } } } if (GetPetID()) { return false; } if (familiar_buff_slot >= 0) { BuffFadeBySlot(familiar_buff_slot); return false; } bot_spell = GetFirstBotSpellBySpellType(this, spell_type); } else if (bot_class == Class::Magician) { bot_spell = GetBestBotMagicianPetSpell(this, spell_type); } else { bot_spell = GetFirstBotSpellBySpellType(this, spell_type); } if (!IsValidSpell(bot_spell.SpellId)) { return false; } if (BotRequiresLoSToCast(spell_type, bot_spell.SpellId) && !HasLoS()) { return false; } if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { SetCastedSpellType(spell_type); RaidGroupSay( fmt::format( "Summoning a pet [{}].", GetSpellName(bot_spell.SpellId) ).c_str() ); return true; } return false; } bool Bot::BotCastNuke(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { if (BotRequiresLoSToCast(spell_type, bot_spell.SpellId) && !HasLoS()) { return false; } if (spell_type == BotSpellTypes::Stun || spell_type == BotSpellTypes::AEStun) { uint8 stun_chance = (tar->IsCasting() ? RuleI(Bots, StunCastChanceIfCasting) : RuleI(Bots, StunCastChanceNormal)); if (bot_class == Class::Paladin) { stun_chance = RuleI(Bots, StunCastChancePaladins); } if ( !tar->GetSpecialAbility(SpecialAbility::StunImmunity) && ( IsCommandedSpell() || (!tar->IsStunned() && (zone->random.Int(1, 100) <= stun_chance)) ) ) { bot_spell = GetBestBotSpellForStunByTargetType(this, ST_TargetOptional, spell_type, IsAEBotSpellType(spell_type), tar); } if (!IsValidSpell(bot_spell.SpellId)) { return false; } } if (!IsValidSpell(bot_spell.SpellId)) { bot_spell = GetBestBotSpellForNukeByBodyType(this, tar->GetBodyType(), spell_type, IsAEBotSpellType(spell_type), tar); } if (spell_type == BotSpellTypes::Nuke && bot_class == Class::Wizard && !IsValidSpell(bot_spell.SpellId)) { bot_spell = GetBestBotWizardNukeSpellByTargetResists(this, tar, spell_type); } if (!IsValidSpell(bot_spell.SpellId)) { std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, IsAEBotSpellType(spell_type)); for (const auto& s : bot_spell_list) { if (!IsValidSpell(s.SpellId)) { continue; } if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { SetCastedSpellType(spell_type); RaidGroupSay( fmt::format( "Casting {} [{}] on {}.", GetSpellName(s.SpellId), GetSpellTypeNameByID(spell_type), tar->GetCleanName() ).c_str() ); return true; } } } else { if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { SetCastedSpellType(spell_type); RaidGroupSay( fmt::format( "Casting {} [{}] on {}.", GetSpellName(bot_spell.SpellId), GetSpellTypeNameByID(spell_type), tar->GetCleanName() ).c_str() ); return true; } } return false; } bool Bot::BotCastHeal(Mob* tar, uint8 bot_class, BotSpell& bot_spell, uint16 spell_type) { if (!TargetValidation(tar)) { return false; } bot_spell = GetSpellByHealType(spell_type, tar); if (!IsValidSpell(bot_spell.SpellId)) { return false; } if (AIDoSpellCast(bot_spell.SpellIndex, tar, bot_spell.ManaCost)) { if (IsGroupSpell(bot_spell.SpellId)) { if (bot_class != Class::Bard) { if (!IsCommandedSpell()) { for (Mob* m : GatherSpellTargets(false, tar)) { SetBotSpellRecastTimer(spell_type, m, true); } } } RaidGroupSay( fmt::format( "Healing the group with {} [{}].", GetSpellName(bot_spell.SpellId), GetSpellTypeNameByID(spell_type) ).c_str() ); } else { if (bot_class != Class::Bard) { if (!IsCommandedSpell()) { SetBotSpellRecastTimer(spell_type, tar, true); } } RaidGroupSay( fmt::format( "Healing {} with {} [{}].", (tar == this ? "myself" : tar->GetCleanName()), GetSpellName(bot_spell.SpellId), GetSpellTypeNameByID(spell_type) ).c_str() ); } return true; } return false; } bool Bot::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore) { bool result = false; // manacost has special values, -1 is no mana cost, -2 is instant cast (no mana) int32 manaCost = mana_cost; if (manaCost == -1) { manaCost = spells[AIBot_spells[i].spellid].mana; } else if (manaCost == -2) { manaCost = 0; } int64 hasMana = GetMana(); // Allow bots to cast buff spells even if they are out of mana if ( RuleB(Bots, FinishBuffing) && manaCost > hasMana && !IsEngaged() && IsBotBuffSpellType(AIBot_spells[i].type) ) { SetMana(manaCost); } float dist2 = 0; if (AIBot_spells[i].type == BotSpellTypes::Escape) { dist2 = 0; } else dist2 = DistanceSquared(m_Position, tar->GetPosition()); if (IsValidSpellRange(AIBot_spells[i].spellid, tar) && (mana_cost <= GetMana() || IsBotNonSpellFighter())) { casting_spell_AIindex = i; LogAI("spellid [{}] tar [{}] mana [{}] Name [{}]", AIBot_spells[i].spellid, tar->GetName(), mana_cost, spells[AIBot_spells[i].spellid].name); result = Mob::CastSpell(AIBot_spells[i].spellid, tar->GetID(), EQ::spells::CastingSlot::Gem2, spells[AIBot_spells[i].spellid].cast_time, AIBot_spells[i].manacost == -2 ? 0 : mana_cost, oDontDoAgainBefore, -1, -1, 0, &(AIBot_spells[i].resist_adjust)); if (IsCasting() && IsSitting()) Stand(); } // if the spell wasn't casted, then take back any extra mana that was given to the bot to cast that spell if (!result) { SetMana(hasMana); } return result; } bool Bot::AI_PursueCastCheck() { if (GetAppearance() == eaDead || delaytimer || spellend_timer.Enabled() || IsFeared() || IsSilenced() || IsAmnesiad() || GetHP() < 0) { return false; } bool result = false; if (GetTarget() && AIautocastspell_timer->Check(false)) { LogAIDetail("Bot Pursue autocast check triggered: [{}]", GetCleanName()); LogBotSpellChecksDetail("{} says, 'AI_PursueCastCheck started.'", GetCleanName()); AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. if (!IsAttackAllowed(GetTarget())) { return false; } auto cast_order = GetSpellTypesPrioritized(BotPriorityCategories::Pursue); Mob* tar = nullptr; for (auto& current_cast : cast_order) { if (current_cast.priority == 0) { SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeHeldDelay)); LogBotSpellChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(current_cast.spellType)); continue; } if (!RuleB(Bots, AllowAIMez) && (current_cast.spellType == BotSpellTypes::AEMez || current_cast.spellType == BotSpellTypes::Mez)) { continue; } if (IsCommandedBotSpellType(current_cast.spellType)) { // Unsupported by AI currently. continue; } if (AIBot_spells_by_type[current_cast.spellType].empty() && AIBot_spells_by_type[GetParentSpellType(current_cast.spellType)].empty()) { continue; } if (!SpellTypeAIDelayCheck(current_cast.spellType)) { continue; } result = AttemptAICastSpell(current_cast.spellType, nullptr); if (!result && IsBotSpellTypeBeneficial(current_cast.spellType)) { result = AttemptCloseBeneficialSpells(current_cast.spellType); } SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeDelay)); if (result) { break; } } if (!AIautocastspell_timer->Enabled()) { AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenInCombatCastAttempts), RuleI(Bots, MaxDelayBetweenInCombatCastAttempts)), false); } } return result; } bool Bot::AI_IdleCastCheck() { if (GetAppearance() == eaDead || delaytimer || spellend_timer.Enabled() || IsFeared() || IsSilenced() || IsAmnesiad() || GetHP() < 0) { return false; } bool result = false; if (AIautocastspell_timer->Check(false)) { LogAIDetail("Bot Non-Engaged autocast check triggered: [{}]", GetCleanName()); LogBotSpellChecksDetail("{} says, 'AI_IdleCastCheck started.'", GetCleanName()); AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. bool pre_combat = false; Client* test_against = nullptr; if (HasGroup() && GetGroup()->GetLeader() && GetGroup()->GetLeader()->IsClient()) { test_against = GetGroup()->GetLeader()->CastToClient(); } else if (GetOwner() && GetOwner()->IsClient()) { test_against = GetOwner()->CastToClient(); } if (test_against) { pre_combat = test_against->GetBotPrecombat(); } auto cast_order = GetSpellTypesPrioritized(BotPriorityCategories::Idle); Mob* tar = nullptr; for (auto& current_cast : cast_order) { if (current_cast.priority == 0) { SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeHeldDelay)); LogBotSpellChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(current_cast.spellType)); continue; } if (!pre_combat && (current_cast.spellType == BotSpellTypes::PreCombatBuff || current_cast.spellType == BotSpellTypes::PreCombatBuffSong)) { continue; } if (!RuleB(Bots, AllowAIMez) && (current_cast.spellType == BotSpellTypes::AEMez || current_cast.spellType == BotSpellTypes::Mez)) { continue; } if (IsCommandedBotSpellType(current_cast.spellType)) { // Unsupported by AI currently. continue; } if (!IsBotSpellTypeBeneficial(current_cast.spellType)) { continue; } if (AIBot_spells_by_type[current_cast.spellType].empty() && AIBot_spells_by_type[GetParentSpellType(current_cast.spellType)].empty()) { continue; } if (!SpellTypeAIDelayCheck(current_cast.spellType)) { continue; } result = AttemptAICastSpell(current_cast.spellType, nullptr); if (result) { break; } result = AttemptCloseBeneficialSpells(current_cast.spellType); SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeDelay)); if (result) { break; } } if (!AIautocastspell_timer->Enabled()) { AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenOutCombatCastAttempts), RuleI(Bots, MaxDelayBetweenOutCombatCastAttempts)), false); } } return result; } bool Bot::AI_EngagedCastCheck() { if (GetAppearance() == eaDead || delaytimer || spellend_timer.Enabled() || IsFeared() || IsSilenced() || IsAmnesiad() || GetHP() < 0) { return false; } bool result = false; if (GetTarget() && AIautocastspell_timer->Check(false)) { LogAIDetail("Bot Engaged autocast check triggered: [{}]", GetCleanName()); LogBotSpellChecksDetail("{} says, 'AI_EngagedCastCheck started.'", GetCleanName()); AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. if (!IsAttackAllowed(GetTarget())) { return false; } auto cast_order = GetSpellTypesPrioritized(BotPriorityCategories::Engaged); Mob* tar = nullptr; for (auto& current_cast : cast_order) { if (current_cast.priority == 0) { SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeHeldDelay)); LogBotSpellChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(current_cast.spellType)); continue; } if (!RuleB(Bots, AllowAIMez) && (current_cast.spellType == BotSpellTypes::AEMez || current_cast.spellType == BotSpellTypes::Mez)) { continue; } if (IsCommandedBotSpellType(current_cast.spellType)) { // Unsupported by AI currently. continue; } if (AIBot_spells_by_type[current_cast.spellType].empty() && AIBot_spells_by_type[GetParentSpellType(current_cast.spellType)].empty()) { continue; } if (!SpellTypeAIDelayCheck(current_cast.spellType)) { continue; } result = AttemptAICastSpell(current_cast.spellType, nullptr); SetSpellTypeAITimer(current_cast.spellType, RuleI(Bots, AICastSpellTypeDelay)); if (!result && IsBotSpellTypeBeneficial(current_cast.spellType)) { result = AttemptCloseBeneficialSpells(current_cast.spellType); } if (result) { break; } } if (!AIautocastspell_timer->Enabled()) { AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenInCombatCastAttempts), RuleI(Bots, MaxDelayBetweenInCombatCastAttempts)), false); } } return result; } bool Bot::AIHealRotation(Mob* tar, bool useFastHeals) { if (!tar) { return false; } if (!AI_HasSpells()) return false; if (tar->GetAppearance() == eaDead) { if ((tar->IsClient() && tar->CastToClient()->GetFeigned()) || tar->IsBot()) { // do nothing } else { return false; } } uint8 botLevel = GetLevel(); bool castedSpell = false; BotSpell botSpell; botSpell.SpellId = 0; botSpell.SpellIndex = 0; botSpell.ManaCost = 0; if (useFastHeals) { botSpell = GetBestBotSpellForRegularSingleTargetHeal(this, tar); if (!IsValidSpell(botSpell.SpellId)) botSpell = GetBestBotSpellForFastHeal(this, tar); } else { botSpell = GetBestBotSpellForPercentageHeal(this, tar); if (!IsValidSpell(botSpell.SpellId)) { botSpell = GetBestBotSpellForRegularSingleTargetHeal(this, tar); } if (!IsValidSpell(botSpell.SpellId)) { botSpell = GetFirstBotSpellForSingleTargetHeal(this, tar); } if (!IsValidSpell(botSpell.SpellId)) { botSpell = GetFirstBotSpellBySpellType(this, BotSpellTypes::RegularHeal); } } LogAIDetail("heal spellid [{}] fastheals [{}] casterlevel [{}]", botSpell.SpellId, ((useFastHeals) ? ('T') : ('F')), GetLevel()); LogAIDetail("target [{}] current_time [{}] donthealmebefore [{}]", tar->GetCleanName(), Timer::GetCurrentTime(), tar->DontHealMeBefore()); // If there is still no spell id, then there isn't going to be one so we are done if (!IsValidSpell(botSpell.SpellId)) { return false; } // Can we cast this spell on this target? if (! ( spells[botSpell.SpellId].target_type == ST_GroupTeleport || spells[botSpell.SpellId].target_type == ST_Target || tar == this ) && tar->CanBuffStack(botSpell.SpellId, botLevel, true) < 0 ) { return false; } uint32 TempDontHealMeBeforeTime = tar->DontHealMeBefore(); if (IsValidSpellRange(botSpell.SpellId, tar)) { castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontHealMeBeforeTime); } if (castedSpell) { RaidGroupSay( fmt::format( "Casting {} on {}, please stay in range!", spells[botSpell.SpellId].name, tar->GetCleanName() ).c_str() ); } return castedSpell; } std::list Bot::GetBotSpellsForSpellEffect(Bot* caster, uint16 spell_type, int spell_effect) { std::list result; if (!caster) { return result; } if (auto bot_owner = caster->GetBotOwner(); !bot_owner) { return result; } if (caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if ( caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == GetParentSpellType(spell_type)) && caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) && (IsEffectInSpell(bot_spell_list[i].spellid, spell_effect) || GetSpellTriggerSpellID(bot_spell_list[i].spellid, spell_effect)) ) { BotSpell bot_spell; bot_spell.SpellId = bot_spell_list[i].spellid; bot_spell.SpellIndex = bot_spell_list[i].index; bot_spell.ManaCost = bot_spell_list[i].manacost; result.push_back(bot_spell); } } } return result; } std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* caster, uint16 spell_type, int spell_effect, SpellTargetType target_type) { std::list result; if (!caster) { return result; } if (auto bot_owner = caster->GetBotOwner(); !bot_owner) { return result; } if (caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if ( caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == GetParentSpellType(spell_type)) && caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) && ( IsEffectInSpell(bot_spell_list[i].spellid, spell_effect) || GetSpellTriggerSpellID(bot_spell_list[i].spellid, spell_effect) ) && (target_type == ST_TargetOptional || spells[bot_spell_list[i].spellid].target_type == target_type) ) { BotSpell bot_spell; bot_spell.SpellId = bot_spell_list[i].spellid; bot_spell.SpellIndex = bot_spell_list[i].index; bot_spell.ManaCost = bot_spell_list[i].manacost; result.push_back(bot_spell); } } } return result; } std::list Bot::GetBotSpellsBySpellType(Bot* caster, uint16 spell_type) { std::list result; if (!caster) { return result; } if (auto bot_owner = caster->GetBotOwner(); !bot_owner) { return result; } if (caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if ( caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == GetParentSpellType(spell_type)) && caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) ) { BotSpell bot_spell; bot_spell.SpellId = bot_spell_list[i].spellid; bot_spell.SpellIndex = bot_spell_list[i].index; bot_spell.ManaCost = bot_spell_list[i].manacost; result.push_back(bot_spell); } } } return result; } std::vector Bot::GetPrioritizedBotSpellsBySpellType(Bot* caster, uint16 spell_type, Mob* tar, bool AE, uint16 sub_target_type, uint16 sub_type) { std::vector result; if (caster && caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if (spell_type == BotSpellTypes::HateRedux && caster->GetClass() == Class::Bard) { if (spells[bot_spell_list[i].spellid].target_type != ST_Target) { continue; } } if ( caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == GetParentSpellType(spell_type)) && caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) ) { if ( caster->IsCommandedSpell() && ( !caster->IsValidSpellTypeSubType(spell_type, sub_target_type, bot_spell_list[i].spellid) || !caster->IsValidSpellTypeSubType(spell_type, sub_type, bot_spell_list[i].spellid) ) ) { continue; } if (!AE && IsAnyAESpell(bot_spell_list[i].spellid) && !IsGroupSpell(bot_spell_list[i].spellid)) { continue; } else if (AE && !IsAnyAESpell(bot_spell_list[i].spellid)) { continue; } if ( !caster->IsInGroupOrRaid(tar, true) && ( !RuleB(Bots, EnableBotTGB) || ( IsGroupSpell(bot_spell_list[i].spellid) && !IsTGBCompatibleSpell(bot_spell_list[i].spellid) ) ) ) { continue; } if (!IsPBAESpell(bot_spell_list[i].spellid) && !caster->CastChecks(bot_spell_list[i].spellid, tar, spell_type, false, IsAEBotSpellType(spell_type))) { continue; } if ( caster->IsCommandedSpell() || !AE || !BotSpellTypeRequiresAEChecks(spell_type) || caster->HasValidAETarget(caster, bot_spell_list[i].spellid, spell_type, tar) ) { BotSpell_wPriority bot_spell; bot_spell.SpellId = bot_spell_list[i].spellid; bot_spell.SpellIndex = bot_spell_list[i].index; bot_spell.ManaCost = bot_spell_list[i].manacost; bot_spell.Priority = bot_spell_list[i].priority; result.emplace_back(bot_spell); } } } if (result.size() > 1) { std::sort(result.begin(), result.end(), [](BotSpell_wPriority const& l, BotSpell_wPriority const& r) { return l.Priority < r.Priority; }); } } return result; } BotSpell Bot::GetFirstBotSpellBySpellType(Bot* caster, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster && caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if ( caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) && (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == GetParentSpellType(spell_type)) && caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) ) { result.SpellId = bot_spell_list[i].spellid; result.SpellIndex = bot_spell_list[i].index; result.ManaCost = bot_spell_list[i].manacost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForVeryFastHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::CurrentHP); for (auto bot_spell_list_itr : bot_spell_list) { if ( IsVeryFastHealSpell(bot_spell_list_itr.SpellId) && caster->CastChecks(bot_spell_list_itr.SpellId, tar, spell_type)) { result.SpellId = bot_spell_list_itr.SpellId; result.SpellIndex = bot_spell_list_itr.SpellIndex; result.ManaCost = bot_spell_list_itr.ManaCost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForFastHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::CurrentHP); for (auto bot_spell_list_itr : bot_spell_list) { if (IsFastHealSpell(bot_spell_list_itr.SpellId) && caster->CastChecks(bot_spell_list_itr.SpellId, tar, spell_type)) { result.SpellId = bot_spell_list_itr.SpellId; result.SpellIndex = bot_spell_list_itr.SpellIndex; result.ManaCost = bot_spell_list_itr.ManaCost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::HealOverTime); for (auto bot_spell_list_itr : bot_spell_list) { if (IsHealOverTimeSpell(bot_spell_list_itr.SpellId) && caster->CastChecks(bot_spell_list_itr.SpellId, tar, spell_type)) { result.SpellId = bot_spell_list_itr.SpellId; result.SpellIndex = bot_spell_list_itr.SpellIndex; result.ManaCost = bot_spell_list_itr.ManaCost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster && caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if ( (bot_spell_list[i].type == spell_type || bot_spell_list[i].type == GetParentSpellType(spell_type)) && caster->IsValidSpellTypeBySpellID(spell_type, bot_spell_list[i].spellid) && IsCompleteHealSpell(bot_spell_list[i].spellid) && caster->CastChecks(bot_spell_list[i].spellid, tar, spell_type) ) { result.SpellId = bot_spell_list[i].spellid; result.SpellIndex = bot_spell_list[i].index; result.ManaCost = bot_spell_list[i].manacost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::CurrentHP); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsRegularSingleTargetHealSpell(bot_spell_list_itr->SpellId) && caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type)) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } return result; } BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::CurrentHP); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsRegularSingleTargetHealSpell(bot_spell_list_itr->SpellId) && caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type)) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!caster->TargetValidation(tar)) { return result; } std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::CurrentHP); int target_count = 0; int required_count = caster->GetSpellTypeAEOrGroupTargetCount(spell_type); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsRegularGroupHealSpell(bot_spell_list_itr->SpellId)) { uint16 spell_id = bot_spell_list_itr->SpellId; if (caster->TargetValidation(tar) && !caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); if (target_count < required_count) { continue; } } result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } return result; } BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!caster->TargetValidation(tar)) { return result; } std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::HealOverTime); int target_count = 0; int required_count = caster->GetSpellTypeAEOrGroupTargetCount(spell_type); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsGroupHealOverTimeSpell(bot_spell_list_itr->SpellId)) { uint16 spell_id = bot_spell_list_itr->SpellId; if (caster->TargetValidation(tar) && !caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); if (target_count < required_count) { continue; } } result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } return result; } BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!caster->TargetValidation(tar)) { return result; } std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::CompleteHeal); int target_count = 0; int required_count = caster->GetSpellTypeAEOrGroupTargetCount(spell_type); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsGroupCompleteHealSpell(bot_spell_list_itr->SpellId)) { uint16 spell_id = bot_spell_list_itr->SpellId; if (caster->TargetValidation(tar) && !caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); if (target_count < required_count) { continue; } } result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } return result; } BotSpell Bot::GetBestBotSpellForMez(Bot* caster, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::Mez); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if ( IsMesmerizeSpell(bot_spell_list_itr->SpellId) && caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId) ) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } return result; } Mob* Bot::GetFirstIncomingMobToMez(Bot* caster, uint16 spell_id, uint16 spell_type, bool AE) { Mob* result = nullptr; if (caster && caster->GetOwner()) { int spell_range = caster->GetActSpellRange(spell_id, spells[spell_id].range); int spell_ae_range = caster->GetAOERange(spell_id); bool is_pbae_spell = IsPBAESpell(spell_id); NPC* npc = nullptr; for (auto& close_mob : caster->m_close_mobs) { npc = close_mob.second->CastToNPC(); if (!npc) { continue; } if (!caster->IsValidMezTarget(caster->GetOwner(), npc, spell_id)) { continue; } if (is_pbae_spell) { if (spell_ae_range < Distance(caster->GetPosition(), npc->GetPosition())) { continue; } } else { if (spell_range < Distance(caster->GetPosition(), npc->GetPosition())) { continue; } } if (AE) { int target_count = 0; for (auto& close_mob : caster->m_close_mobs) { Mob* m = close_mob.second; if (!caster->IsValidMezTarget(caster->GetOwner(), m, spell_id)) { continue; } if (spell_ae_range < Distance(npc->GetPosition(), m->GetPosition())) { continue; } if (caster->CastChecks(spell_id, m, spell_type, true, true)) { ++target_count; } if (target_count >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { break; } } if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { continue; } result = npc; } else { if (spell_range < Distance(caster->GetPosition(), npc->GetPosition())) { continue; } if (!caster->CastChecks(spell_id, npc, spell_type, true)) { continue; } result = npc; } if (result) { return result; } } } return result; } BotSpell Bot::GetBestBotMagicianPetSpell(Bot* caster, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::SummonPet); std::string pet_type = GetBotMagicianPetType(caster); for(std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if ( IsSummonPetSpell(bot_spell_list_itr->SpellId) && caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId) && !strncmp(spells[bot_spell_list_itr->SpellId].teleport_zone, pet_type.c_str(), pet_type.length()) ) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } return result; } std::string Bot::GetBotMagicianPetType(Bot* caster) { std::string result; if (caster) { uint8 pet_type = caster->GetPetChooserID(); uint8 bot_level = caster->GetLevel(); bool epic_allowed = false; std::string epic_spell_name = RuleS(Bots, EpicPetSpellName); if (epic_spell_name.empty()) { epic_spell_name = "SumMageMultiElement"; } if (RuleB(Bots, AllowMagicianEpicPet)) { if (bot_level >= RuleI(Bots, AllowMagicianEpicPetLevel)) { if (!RuleI(Bots, RequiredMagicianEpicPetItemID)) { epic_allowed = true; } else { bool has_item = caster->HasBotItem(RuleI(Bots, RequiredMagicianEpicPetItemID)) != INVALID_INDEX; if (has_item) { epic_allowed = true; } } } } if (pet_type > 0) { switch (pet_type) { case SumWater: result = std::string("SumWater"); break; case SumFire: result = std::string("SumFire"); break; case SumAir: result = std::string("SumAir"); break; case SumEarth: result = std::string("SumEarth"); break; case MonsterSum: result = std::string("MonsterSum"); break; case SumMageMultiElement: if (epic_allowed) { result = epic_spell_name; } else { caster->SetPetChooserID(0); } break; } } else { uint8 air_min_level = 255; uint8 fire_min_level = 255; uint8 water_min_level = 255; uint8 earth_min_level = 255; uint8 monster_min_level = 255; uint8 epic_min_level = 255; std::list bot_spell_list = caster->GetBotSpellsBySpellType(caster, BotSpellTypes::Pet); for (const auto& s : bot_spell_list) { if (!IsValidSpell(s.SpellId)) { continue; } if (!IsEffectInSpell(s.SpellId, SpellEffect::SummonPet)) { continue; } auto spell = spells[s.SpellId]; if (!strncmp(spell.teleport_zone, "SumWater", 8) && spell.classes[Class::Magician - 1] < water_min_level) { water_min_level = spell.classes[Class::Magician - 1]; } else if (!strncmp(spell.teleport_zone, "SumFire", 7) && spell.classes[Class::Magician - 1] < fire_min_level) { fire_min_level = spell.classes[Class::Magician - 1]; } else if (!strncmp(spell.teleport_zone, "SumAir", 6) && spell.classes[Class::Magician - 1] < air_min_level) { air_min_level = spell.classes[Class::Magician - 1]; } else if (!strncmp(spell.teleport_zone, "SumEarth", 8) && spell.classes[Class::Magician - 1] < earth_min_level) { earth_min_level = spell.classes[Class::Magician - 1]; } else if (!strncmp(spell.teleport_zone, "MonsterSum", 10) && spell.classes[Class::Magician - 1] < monster_min_level) { monster_min_level = spell.classes[Class::Magician - 1]; } else if (!strncmp(spell.teleport_zone, epic_spell_name.c_str(), epic_spell_name.length()) && spell.classes[Class::Magician - 1] < epic_min_level) { epic_min_level = spell.classes[Class::Magician - 1]; } } if (epic_allowed) { epic_min_level = std::max(int(epic_min_level), RuleI(Bots, AllowMagicianEpicPetLevel)); if (bot_level >= epic_min_level) { result = epic_spell_name; } } else { bool found = false; uint8 count = 0; while (count <= 4 && !found) { int counter = zone->random.Int(1, 4); switch (counter) { case SumWater: if (bot_level >= water_min_level) { result = std::string("SumWater"); } found = true; break; case SumFire: if (bot_level >= fire_min_level) { result = std::string("SumFire"); } found = true; break; case SumAir: if (bot_level >= air_min_level) { result = std::string("SumAir"); } found = true; break; case SumEarth: if (bot_level >= earth_min_level) { result = std::string("SumEarth"); } found = true; break; } ++count; } } } } return result; } BotSpell Bot::GetBestBotSpellForNukeByTargetType(Bot* caster, SpellTargetType target_type, uint16 spell_type, bool AE, Mob* tar) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (tar == nullptr) { tar = caster->GetTarget(); } if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SpellEffect::CurrentHP, target_type); for(std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsPureNukeSpell(bot_spell_list_itr->SpellId) || IsDamageSpell(bot_spell_list_itr->SpellId)) { if (!AE && IsAnyAESpell(bot_spell_list_itr->SpellId) && !IsGroupSpell(bot_spell_list_itr->SpellId)) { continue; } else if (AE && !IsAnyAESpell(bot_spell_list_itr->SpellId)) { continue; } if (!IsPBAESpell(bot_spell_list_itr->SpellId) && !caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type, false, IsAEBotSpellType(spell_type))) { continue; } if ( caster->IsCommandedSpell() || !AE || (AE && caster->HasValidAETarget(caster, bot_spell_list_itr->SpellId, spell_type, tar)) ) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } } return result; } BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* caster, SpellTargetType target_type, uint16 spell_type, bool AE, Mob* tar) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (tar == nullptr) { tar = caster->GetTarget(); } if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SpellEffect::Stun, target_type); for(std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (IsStunSpell(bot_spell_list_itr->SpellId)) { if (!AE && IsAnyAESpell(bot_spell_list_itr->SpellId) && !IsGroupSpell(bot_spell_list_itr->SpellId)) { continue; } else if (AE && !IsAnyAESpell(bot_spell_list_itr->SpellId)) { continue; } if (!IsPBAESpell(bot_spell_list_itr->SpellId) && !caster->CastChecks(bot_spell_list_itr->SpellId, tar, spell_type, false, IsAEBotSpellType(spell_type))) { continue; } if ( caster->IsCommandedSpell() || !AE || (AE && caster->HasValidAETarget(caster, bot_spell_list_itr->SpellId, spell_type, tar)) ) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } } return result; } BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* caster, Mob* target, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster && target) { const int lure_resis_value = -100; int32 level_mod = (target->GetLevel() - caster->GetLevel()) * (target->GetLevel() - caster->GetLevel()) / 2; if (target->GetLevel() - caster->GetLevel() < 0) { level_mod = -level_mod; } const int max_target_resist_value = caster->GetSpellTypeResistLimit(spell_type); bool select_lure_nuke = false; if (((target->GetMR() + level_mod) > max_target_resist_value) && ((target->GetCR() + level_mod) > max_target_resist_value) && ((target->GetFR() + level_mod) > max_target_resist_value)) { select_lure_nuke = true; } std::list bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SpellEffect::CurrentHP, ST_Target); BotSpell first_wizard_magic_nuke_spell_found; first_wizard_magic_nuke_spell_found.SpellId = 0; first_wizard_magic_nuke_spell_found.SpellIndex = 0; first_wizard_magic_nuke_spell_found.ManaCost = 0; bool spell_selected = false; for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (!caster->IsValidSpellRange(bot_spell_list_itr->SpellId, target)) { continue; } if (select_lure_nuke && (spells[bot_spell_list_itr->SpellId].resist_difficulty < lure_resis_value)) { if (caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type)) { spell_selected = true; } } else if (!select_lure_nuke && IsPureNukeSpell(bot_spell_list_itr->SpellId)) { if ( ((target->GetMR() < target->GetCR()) || (target->GetMR() < target->GetFR())) && (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_MAGIC) && (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) ) { spell_selected = true; } else if ( ((target->GetCR() < target->GetMR()) || (target->GetCR() < target->GetFR())) && (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_COLD) && (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) ) { spell_selected = true; } else if ( ((target->GetFR() < target->GetCR()) || (target->GetFR() < target->GetMR())) && (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_FIRE) && (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) ) { spell_selected = true; } else if ( (GetSpellResistType(bot_spell_list_itr->SpellId) == RESIST_MAGIC) && (spells[bot_spell_list_itr->SpellId].resist_difficulty > lure_resis_value) && caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) ) { first_wizard_magic_nuke_spell_found.SpellId = bot_spell_list_itr->SpellId; first_wizard_magic_nuke_spell_found.SpellIndex = bot_spell_list_itr->SpellIndex; first_wizard_magic_nuke_spell_found.ManaCost = bot_spell_list_itr->ManaCost; } } if (spell_selected) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } if (!spell_selected) { for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if (caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId)) { if (caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type)) { spell_selected = true; } } if (spell_selected) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } if (result.SpellId == 0) { result = first_wizard_magic_nuke_spell_found; } } return result; } BotSpell Bot::GetDebuffBotSpell(Bot* caster, Mob *tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!tar || !caster) return result; if (caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if (((bot_spell_list[i].type == BotSpellTypes::Debuff) || IsDebuffSpell(bot_spell_list[i].spellid)) && (!tar->IsImmuneToSpell(bot_spell_list[i].spellid, caster) && tar->CanBuffStack(bot_spell_list[i].spellid, caster->GetLevel(), true) >= 0) && caster->CheckSpellRecastTimer(bot_spell_list[i].spellid)) { result.SpellId = bot_spell_list[i].spellid; result.SpellIndex = bot_spell_list[i].index; result.ManaCost = bot_spell_list[i].manacost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* caster, Mob *tar, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!tar || !caster) { return result; } int level_mod = (tar->GetLevel() - caster->GetLevel())* (tar->GetLevel() - caster->GetLevel()) / 2; if (tar->GetLevel() - caster->GetLevel() < 0) { level_mod = -level_mod; } bool needs_magic_resist_debuff = (tar->GetMR() + level_mod) > 100; bool needs_cold_resist_debuff = (tar->GetCR() + level_mod) > 100; bool needs_fire_resist_debuff = (tar->GetFR() + level_mod) > 100; bool needs_poison_resist_debuff = (tar->GetPR() + level_mod) > 100; bool needs_disease_resist_debuff = (tar->GetDR() + level_mod) > 100; if (caster->AI_HasSpells()) { const std::vector& bot_spell_list = caster->BotGetSpellsByType(spell_type); for (int i = bot_spell_list.size() - 1; i >= 0; i--) { if (!IsValidSpell(bot_spell_list[i].spellid)) { continue; } if (BotRequiresLoSToCast(spell_type, bot_spell_list[i].spellid) && !caster->HasLoS()) { continue; } if ( (bot_spell_list[i].type == BotSpellTypes::Debuff || IsResistDebuffSpell(bot_spell_list[i].spellid)) && ( (needs_magic_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistMagic) || IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistAll))) || (needs_cold_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistCold) || IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistAll))) || (needs_fire_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistFire) || IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistAll))) || (needs_poison_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistPoison) || IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistAll))) || (needs_disease_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistDisease) || IsEffectInSpell(bot_spell_list[i].spellid, SpellEffect::ResistAll))) ) && !tar->IsImmuneToSpell(bot_spell_list[i].spellid, caster) && tar->CanBuffStack(bot_spell_list[i].spellid, caster->GetLevel(), true) >= 0 && caster->CheckSpellRecastTimer(bot_spell_list[i].spellid) ) { result.SpellId = bot_spell_list[i].spellid; result.SpellIndex = bot_spell_list[i].index; result.ManaCost = bot_spell_list[i].manacost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForCure(Bot* caster, Mob* tar, uint16 spell_type) { BotSpell_wPriority result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!tar) { return result; } if (caster) { std::vector bot_spell_list_itr = GetPrioritizedBotSpellsBySpellType(caster, spell_type, tar); if (IsGroupBotSpellType(spell_type)) { int count_needs_cured = 0; uint16 count_poisoned = 0; uint16 count_diseased = 0; uint16 count_cursed = 0; uint16 count_corrupted = 0; for (std::vector::iterator itr = bot_spell_list_itr.begin(); itr != bot_spell_list_itr.end(); ++itr) { if (!IsValidSpell(itr->SpellId) || !IsGroupSpell(itr->SpellId)) { continue; } for (Mob* m : (IsGroupBotSpellType(spell_type) ? caster->GetSpellTargetList() : caster->GetSpellTargetList(true))) { if (caster->GetNeedsCured(m)) { if (caster->CastChecks(itr->SpellId, m, spell_type, true, IsGroupBotSpellType(spell_type))) { if (m->FindType(SpellEffect::PoisonCounter)) { ++count_poisoned; } if (m->FindType(SpellEffect::DiseaseCounter)) { ++count_diseased; } if (m->FindType(SpellEffect::CurseCounter)) { ++count_cursed; } if (m->FindType(SpellEffect::CorruptionCounter)) { ++count_corrupted; } } } } if ( (count_poisoned >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SpellEffect::PoisonCounter)) || (count_diseased >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SpellEffect::DiseaseCounter)) || (count_cursed >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SpellEffect::CurseCounter)) || (count_corrupted >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SpellEffect::CorruptionCounter)) ) { result.SpellId = itr->SpellId; result.SpellIndex = itr->SpellIndex; result.ManaCost = itr->ManaCost; break; } } } else { for (std::vector::iterator itr = bot_spell_list_itr.begin(); itr != bot_spell_list_itr.end(); ++itr) { if (!IsValidSpell(itr->SpellId) || IsGroupSpell(itr->SpellId)) { continue; } if ( tar->FindType(SpellEffect::PoisonCounter) && IsEffectInSpell(itr->SpellId, SpellEffect::PoisonCounter) || tar->FindType(SpellEffect::DiseaseCounter) && IsEffectInSpell(itr->SpellId, SpellEffect::DiseaseCounter) || tar->FindType(SpellEffect::CurseCounter) && IsEffectInSpell(itr->SpellId, SpellEffect::CurseCounter) || tar->FindType(SpellEffect::CorruptionCounter) && IsEffectInSpell(itr->SpellId, SpellEffect::CorruptionCounter) ) { result.SpellId = itr->SpellId; result.SpellIndex = itr->SpellIndex; result.ManaCost = itr->ManaCost; break; } } } } return result; } uint8 Bot::GetChanceToCastBySpellType(uint16 spell_type) { switch (spell_type) { case BotSpellTypes::AENukes: case BotSpellTypes::AERains: case BotSpellTypes::AEStun: case BotSpellTypes::AESnare: case BotSpellTypes::AESlow: case BotSpellTypes::AEDebuff: case BotSpellTypes::AEFear: case BotSpellTypes::AEDispel: case BotSpellTypes::AEDoT: case BotSpellTypes::AELifetap: case BotSpellTypes::AERoot: case BotSpellTypes::PBAENuke: case BotSpellTypes::AEHateLine: return RuleI(Bots, PercentChanceToCastAEs); case BotSpellTypes::GroupHeals: case BotSpellTypes::GroupCompleteHeals: case BotSpellTypes::GroupHoTHeals: return RuleI(Bots, PercentChanceToCastGroupHeal); case BotSpellTypes::Nuke: return RuleI(Bots, PercentChanceToCastNuke); case BotSpellTypes::Root: return RuleI(Bots, PercentChanceToCastRoot); case BotSpellTypes::Buff: case BotSpellTypes::PetBuffs: case BotSpellTypes::ResistBuffs: case BotSpellTypes::PetResistBuffs: case BotSpellTypes::DamageShields: case BotSpellTypes::PetDamageShields: return RuleI(Bots, PercentChanceToCastBuff); case BotSpellTypes::Escape: return RuleI(Bots, PercentChanceToCastEscape); case BotSpellTypes::Lifetap: return RuleI(Bots, PercentChanceToCastLifetap); case BotSpellTypes::Snare: return RuleI(Bots, PercentChanceToCastSnare); case BotSpellTypes::DOT: return RuleI(Bots, PercentChanceToCastDOT); case BotSpellTypes::Dispel: return RuleI(Bots, PercentChanceToCastDispel); case BotSpellTypes::InCombatBuff: return RuleI(Bots, PercentChanceToCastInCombatBuff); case BotSpellTypes::HateLine: return RuleI(Bots, PercentChanceToCastHateLine); case BotSpellTypes::Mez: return RuleI(Bots, PercentChanceToCastMez); case BotSpellTypes::Slow: return RuleI(Bots, PercentChanceToCastSlow); case BotSpellTypes::Debuff: return RuleI(Bots, PercentChanceToCastDebuff); case BotSpellTypes::Cure: case BotSpellTypes::PetCures: return RuleI(Bots, PercentChanceToCastCure); case BotSpellTypes::GroupCures: return RuleI(Bots, PercentChanceToCastGroupCure); case BotSpellTypes::HateRedux: return RuleI(Bots, PercentChanceToCastHateRedux); case BotSpellTypes::Fear: return RuleI(Bots, PercentChanceToCastFear); case BotSpellTypes::RegularHeal: case BotSpellTypes::CompleteHeal: case BotSpellTypes::FastHeals: case BotSpellTypes::VeryFastHeals: case BotSpellTypes::HoTHeals: case BotSpellTypes::PetRegularHeals: case BotSpellTypes::PetCompleteHeals: case BotSpellTypes::PetFastHeals: case BotSpellTypes::PetVeryFastHeals: case BotSpellTypes::PetHoTHeals: return RuleI(Bots, PercentChanceToCastHeal); case BotSpellTypes::AEMez: return RuleI(Bots, PercentChanceToCastAEMez); default: return RuleI(Bots, PercentChanceToCastOtherType); } return RuleI(Bots, PercentChanceToCastOtherType); } bool Bot::AI_AddBotSpells(uint32 bot_spell_id) { // ok, this function should load the list, and the parent list then shove them into the struct and sort npc_spells_id = bot_spell_id; AIBot_spells.clear(); AIBot_spells_enforced.clear(); AIBot_spells_by_type.clear(); if (!bot_spell_id) { AIautocastspell_timer->Disable(); return false; } auto* spell_list = content_db.GetBotSpells(bot_spell_id); if (!spell_list) { AIautocastspell_timer->Disable(); return false; } auto* parentlist = content_db.GetBotSpells(spell_list->parent_list); auto debug_msg = fmt::format( "Loading Bot spells onto {}: dbspellsid={}, level={}", GetName(), bot_spell_id, GetLevel() ); debug_msg.append( fmt::format( " (found, {})", spell_list->entries.size() ) ); LogAI("[{}]", debug_msg); for (const auto &iter: spell_list->entries) { LogAIDetail("([{}]) [{}]", iter.spellid, spells[iter.spellid].name); } LogAI("fin (spell list)"); uint16 attack_proc_spell = -1; int8 proc_chance = 3; uint16 range_proc_spell = -1; int16 rproc_chance = 0; uint16 defensive_proc_spell = -1; int16 dproc_chance = 0; uint32 _fail_recast = 0; uint32 _engaged_no_sp_recast_min = 0; uint32 _engaged_no_sp_recast_max = 0; uint8 _engaged_beneficial_self_chance = 0; uint8 _engaged_beneficial_other_chance = 0; uint8 _engaged_detrimental_chance = 0; uint32 _pursue_no_sp_recast_min = 0; uint32 _pursue_no_sp_recast_max = 0; uint8 _pursue_detrimental_chance = 0; uint32 _idle_no_sp_recast_min = 0; uint32 _idle_no_sp_recast_max = 0; uint8 _idle_beneficial_chance = 0; if (parentlist) { attack_proc_spell = parentlist->attack_proc; proc_chance = parentlist->proc_chance; range_proc_spell = parentlist->range_proc; rproc_chance = parentlist->rproc_chance; defensive_proc_spell = parentlist->defensive_proc; dproc_chance = parentlist->dproc_chance; _fail_recast = parentlist->fail_recast; _engaged_no_sp_recast_min = parentlist->engaged_no_sp_recast_min; _engaged_no_sp_recast_max = parentlist->engaged_no_sp_recast_max; _engaged_beneficial_self_chance = parentlist->engaged_beneficial_self_chance; _engaged_beneficial_other_chance = parentlist->engaged_beneficial_other_chance; _engaged_detrimental_chance = parentlist->engaged_detrimental_chance; _pursue_no_sp_recast_min = parentlist->pursue_no_sp_recast_min; _pursue_no_sp_recast_max = parentlist->pursue_no_sp_recast_max; _pursue_detrimental_chance = parentlist->pursue_detrimental_chance; _idle_no_sp_recast_min = parentlist->idle_no_sp_recast_min; _idle_no_sp_recast_max = parentlist->idle_no_sp_recast_max; _idle_beneficial_chance = parentlist->idle_beneficial_chance; for (auto &e : parentlist->entries) { if ( EQ::ValueWithin(GetLevel(), e.minlevel, e.maxlevel) && e.spellid && !IsSpellInBotList(spell_list, e.spellid) ) { if (!e.bucket_name.empty() && !e.bucket_value.empty()) { if (!CheckDataBucket(e.bucket_name, e.bucket_value, e.bucket_comparison)) { continue; } } const auto& bs = GetBotSpellSetting(e.spellid); if (bs) { if (!bs->is_enabled) { continue; } AddSpellToBotList( bs->priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.minlevel, e.maxlevel, bs->min_hp, bs->max_hp, e.bucket_name, e.bucket_value, e.bucket_comparison ); continue; } if (!GetBotEnforceSpellSetting()) { AddSpellToBotList( e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.minlevel, e.maxlevel, e.min_hp, e.max_hp, e.bucket_name, e.bucket_value, e.bucket_comparison ); } else { AddSpellToBotEnforceList( e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.minlevel, e.maxlevel, e.min_hp, e.max_hp, e.bucket_name, e.bucket_value, e.bucket_comparison ); } } } } attack_proc_spell = spell_list->attack_proc; proc_chance = spell_list->proc_chance; range_proc_spell = spell_list->range_proc; rproc_chance = spell_list->rproc_chance; defensive_proc_spell = spell_list->defensive_proc; dproc_chance = spell_list->dproc_chance; //If any casting variables are defined in the current list, ignore those in the parent list. if ( spell_list->fail_recast || spell_list->engaged_no_sp_recast_min || spell_list->engaged_no_sp_recast_max || spell_list->engaged_beneficial_self_chance || spell_list->engaged_beneficial_other_chance || spell_list->engaged_detrimental_chance || spell_list->pursue_no_sp_recast_min || spell_list->pursue_no_sp_recast_max || spell_list->pursue_detrimental_chance || spell_list->idle_no_sp_recast_min || spell_list->idle_no_sp_recast_max || spell_list->idle_beneficial_chance ) { _fail_recast = spell_list->fail_recast; _engaged_no_sp_recast_min = spell_list->engaged_no_sp_recast_min; _engaged_no_sp_recast_max = spell_list->engaged_no_sp_recast_max; _engaged_beneficial_self_chance = spell_list->engaged_beneficial_self_chance; _engaged_beneficial_other_chance = spell_list->engaged_beneficial_other_chance; _engaged_detrimental_chance = spell_list->engaged_detrimental_chance; _pursue_no_sp_recast_min = spell_list->pursue_no_sp_recast_min; _pursue_no_sp_recast_max = spell_list->pursue_no_sp_recast_max; _pursue_detrimental_chance = spell_list->pursue_detrimental_chance; _idle_no_sp_recast_min = spell_list->idle_no_sp_recast_min; _idle_no_sp_recast_max = spell_list->idle_no_sp_recast_max; _idle_beneficial_chance = spell_list->idle_beneficial_chance; } for (auto &e : spell_list->entries) { if (EQ::ValueWithin(GetLevel(), e.minlevel, e.maxlevel) && e.spellid) { if (!e.bucket_name.empty() && !e.bucket_value.empty()) { if (!CheckDataBucket(e.bucket_name, e.bucket_value, e.bucket_comparison)) { continue; } } const auto& bs = GetBotSpellSetting(e.spellid); if (bs) { if (!bs->is_enabled) { continue; } AddSpellToBotList( bs->priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.minlevel, e.maxlevel, bs->min_hp, bs->max_hp, e.bucket_name, e.bucket_value, e.bucket_comparison ); continue; } if (!GetBotEnforceSpellSetting()) { AddSpellToBotList( e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.minlevel, e.maxlevel, e.min_hp, e.max_hp, e.bucket_name, e.bucket_value, e.bucket_comparison ); } else { AddSpellToBotEnforceList( e.priority, e.spellid, e.type, e.manacost, e.recast_delay, e.resist_adjust, e.minlevel, e.maxlevel, e.min_hp, e.max_hp, e.bucket_name, e.bucket_value, e.bucket_comparison ); } } } std::sort(AIBot_spells.begin(), AIBot_spells.end(), [](const BotSpells& a, const BotSpells& b) { return a.priority > b.priority; }); if (IsValidSpell(attack_proc_spell)) { AddProcToWeapon(attack_proc_spell, true, proc_chance); if (RuleB(Spells, NPCInnateProcOverride)) { innate_proc_spell_id = attack_proc_spell; } } if (IsValidSpell(range_proc_spell)) { AddRangedProc(range_proc_spell, (rproc_chance + 100)); } if (IsValidSpell(defensive_proc_spell)) { AddDefensiveProc(defensive_proc_spell, (dproc_chance + 100)); } //Set AI casting variables AISpellVar.fail_recast = (_fail_recast) ? _fail_recast : RuleI(Spells, AI_SpellCastFinishedFailRecast); AISpellVar.engaged_no_sp_recast_min = (_engaged_no_sp_recast_min) ? _engaged_no_sp_recast_min : RuleI(Spells, AI_EngagedNoSpellMinRecast); AISpellVar.engaged_no_sp_recast_max = (_engaged_no_sp_recast_max) ? _engaged_no_sp_recast_max : RuleI(Spells, AI_EngagedNoSpellMaxRecast); AISpellVar.engaged_beneficial_self_chance = (_engaged_beneficial_self_chance) ? _engaged_beneficial_self_chance : RuleI(Spells, AI_EngagedBeneficialSelfChance); AISpellVar.engaged_beneficial_other_chance = (_engaged_beneficial_other_chance) ? _engaged_beneficial_other_chance : RuleI(Spells, AI_EngagedBeneficialOtherChance); AISpellVar.engaged_detrimental_chance = (_engaged_detrimental_chance) ? _engaged_detrimental_chance : RuleI(Spells, AI_EngagedDetrimentalChance); AISpellVar.pursue_no_sp_recast_min = (_pursue_no_sp_recast_min) ? _pursue_no_sp_recast_min : RuleI(Spells, AI_PursueNoSpellMinRecast); AISpellVar.pursue_no_sp_recast_max = (_pursue_no_sp_recast_max) ? _pursue_no_sp_recast_max : RuleI(Spells, AI_PursueNoSpellMaxRecast); AISpellVar.pursue_detrimental_chance = (_pursue_detrimental_chance) ? _pursue_detrimental_chance : RuleI(Spells, AI_PursueDetrimentalChance); AISpellVar.idle_no_sp_recast_min = (_idle_no_sp_recast_min) ? _idle_no_sp_recast_min : RuleI(Spells, AI_IdleNoSpellMinRecast); AISpellVar.idle_no_sp_recast_max = (_idle_no_sp_recast_max) ? _idle_no_sp_recast_max : RuleI(Spells, AI_IdleNoSpellMaxRecast); AISpellVar.idle_beneficial_chance = (_idle_beneficial_chance) ? _idle_beneficial_chance : RuleI(Spells, AI_IdleBeneficialChance); if (AIBot_spells.empty()) { AIautocastspell_timer->Disable(); } else { AIautocastspell_timer->Trigger(); AssignBotSpellsToTypes(AIBot_spells, AIBot_spells_by_type); // Assign AIBot_spells to AIBot_spells_by_type with an index } return true; } bool IsSpellInBotList(DBbotspells_Struct* spell_list, uint16 iSpellID) { auto it = std::find_if ( spell_list->entries.begin(), spell_list->entries.end(), [iSpellID](const DBbotspells_entries_Struct &a) { return a.spellid == iSpellID; } ); return it != spell_list->entries.end(); } DBbotspells_Struct* ZoneDatabase::GetBotSpells(uint32 bot_spell_id) { if (!bot_spell_id) { return nullptr; } auto c = bot_spells_cache.find(bot_spell_id); if (c != bot_spells_cache.end()) { // it's in the cache, easy =) return &c->second; } if (!bot_spells_loadtried.count(bot_spell_id)) { // no reason to ask the DB again if we have failed once already bot_spells_loadtried.insert(bot_spell_id); auto n = NpcSpellsRepository::FindOne(content_db, bot_spell_id); if (!n.id) { return nullptr; } DBbotspells_Struct spell_set; spell_set.parent_list = n.parent_list; spell_set.attack_proc = n.attack_proc; spell_set.proc_chance = n.proc_chance; spell_set.range_proc = n.range_proc; spell_set.rproc_chance = n.rproc_chance; spell_set.defensive_proc = n.defensive_proc; spell_set.dproc_chance = n.dproc_chance; spell_set.fail_recast = n.fail_recast; spell_set.engaged_no_sp_recast_min = n.engaged_no_sp_recast_min; spell_set.engaged_no_sp_recast_max = n.engaged_no_sp_recast_max; spell_set.engaged_beneficial_self_chance = n.engaged_b_self_chance; spell_set.engaged_beneficial_other_chance = n.engaged_b_other_chance; spell_set.engaged_detrimental_chance = n.engaged_d_chance; spell_set.pursue_no_sp_recast_min = n.pursue_no_sp_recast_min; spell_set.pursue_no_sp_recast_max = n.pursue_no_sp_recast_max; spell_set.pursue_detrimental_chance = n.pursue_d_chance; spell_set.idle_no_sp_recast_min = n.idle_no_sp_recast_min; spell_set.idle_no_sp_recast_max = n.idle_no_sp_recast_max; spell_set.idle_beneficial_chance = n.idle_b_chance; auto bse = BotSpellsEntriesRepository::GetWhere( content_db, fmt::format( "npc_spells_id = {}", bot_spell_id ) ); if (!bse.empty()) { for (const auto& e : bse) { DBbotspells_entries_Struct entry; entry.spellid = e.spell_id; entry.type = e.type; entry.minlevel = e.minlevel; entry.maxlevel = e.maxlevel; entry.manacost = e.manacost; entry.recast_delay = e.recast_delay; entry.priority = e.priority; entry.min_hp = e.min_hp; entry.max_hp = e.max_hp; entry.resist_adjust = e.resist_adjust; entry.bucket_name = e.bucket_name; entry.bucket_value = e.bucket_value; entry.bucket_comparison = e.bucket_comparison; // some spell types don't make much since to be priority 0, so fix that if (!IsBotSpellTypeInnate(entry.type) && entry.priority == 0) { entry.priority = 1; } if (e.resist_adjust) { entry.resist_adjust = e.resist_adjust; } else if (IsValidSpell(e.spell_id)) { entry.resist_adjust = spells[e.spell_id].resist_difficulty; } spell_set.entries.push_back(entry); } } bot_spells_cache.emplace(std::make_pair(bot_spell_id, spell_set)); return &bot_spells_cache[bot_spell_id]; } return nullptr; } // adds a spell to the list, taking into account priority and resorting list as needed. void Bot::AddSpellToBotList( int16 in_priority, uint16 in_spell_id, uint32 in_type, int16 in_mana_cost, int32 in_recast_delay, int16 in_resist_adjust, uint8 in_min_level, uint8 in_max_level, int8 in_min_hp, int8 in_max_hp, std::string in_bucket_name, std::string in_bucket_value, uint8 in_bucket_comparison ) { if (!IsValidSpell(in_spell_id)) { return; } HasAISpell = true; BotSpells t; t.priority = in_priority; t.spellid = in_spell_id; t.type = in_type; t.manacost = in_mana_cost; t.recast_delay = in_recast_delay; t.time_cancast = 0; t.resist_adjust = in_resist_adjust; t.minlevel = in_min_level; t.maxlevel = in_max_level; t.min_hp = in_min_hp; t.max_hp = in_max_hp; t.bucket_name = in_bucket_name; t.bucket_value = in_bucket_value; t.bucket_comparison = in_bucket_comparison; AIBot_spells.push_back(t); // If we're going from an empty list, we need to start the timer if (AIBot_spells.empty()) { AIautocastspell_timer->Start(RandomTimer(0, 300), false); } } // adds spells to the list ^spells that are returned if ^enforce is enabled void Bot::AddSpellToBotEnforceList( int16 iPriority, uint16 iSpellID, uint32 iType, int16 iManaCost, int32 iRecastDelay, int16 iResistAdjust, uint8 min_level, uint8 max_level, int8 min_hp, int8 max_hp, std::string bucket_name, std::string bucket_value, uint8 bucket_comparison ) { if (!IsValidSpell(iSpellID)) { return; } HasAISpell = true; BotSpells t; t.priority = iPriority; t.spellid = iSpellID; t.type = iType; t.manacost = iManaCost; t.recast_delay = iRecastDelay; t.time_cancast = 0; t.resist_adjust = iResistAdjust; t.minlevel = min_level; t.maxlevel = maxlevel; t.min_hp = min_hp; t.max_hp = max_hp; t.bucket_name = bucket_name; t.bucket_value = bucket_value; t.bucket_comparison = bucket_comparison; AIBot_spells_enforced.push_back(t); } //this gets called from InterruptSpell() for failure or SpellFinished() for success void Bot::AI_Bot_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot) { if (slot == 1) { uint32 recovery_time = 0; if (iCastSucceeded) { if (casting_spell_AIindex < AIBot_spells.size()) { recovery_time += spells[AIBot_spells[casting_spell_AIindex].spellid].recovery_time; if (AIBot_spells[casting_spell_AIindex].recast_delay >= 0) { if (AIBot_spells[casting_spell_AIindex].recast_delay < 10000) { AIBot_spells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + (AIBot_spells[casting_spell_AIindex].recast_delay*1000); } } else { AIBot_spells[casting_spell_AIindex].time_cancast = Timer::GetCurrentTime() + spells[AIBot_spells[casting_spell_AIindex].spellid].recast_time; } } if (recovery_time < AIautocastspell_timer->GetSetAtTrigger()) { recovery_time = AIautocastspell_timer->GetSetAtTrigger(); } AIautocastspell_timer->Start(recovery_time, false); } else { AIautocastspell_timer->Start(AISpellVar.fail_recast, false); } casting_spell_AIindex = AIBot_spells.size(); } } bool Bot::HasBotSpellEntry(uint16 spell_id) { auto* spell_list = content_db.GetBotSpells(GetBotSpellID()); if (!spell_list) { return false; } // Check if Spell ID is found in Bot Spell Entries for (auto& e : spell_list->entries) { if (spell_id == e.spellid) { return true; } } return false; } bool Bot::CanUseBotSpell(uint16 spell_id) { if (AIBot_spells.empty()) { return false; } for (const auto& s : AIBot_spells) { if (!IsValidSpell(s.spellid)) { return false; } if (s.spellid != spell_id) { continue; } if (s.minlevel > GetLevel()) { return false; } return true; } return false; } bool Bot::IsValidSpellRange(uint16 spell_id, Mob* tar) { if (!IsValidSpell(spell_id) || !tar) { return false; } float range = spells[spell_id].range + GetRangeDistTargetSizeMod(tar); if ( spells[spell_id].target_type != ST_AETargetHateList && !IsTargetableAESpell(spell_id) && IsAnyAESpell(spell_id) ) { range = GetAOERange(spell_id); } if (RuleB(Bots, EnableBotTGB) && IsTGBCompatibleSpell(spell_id) && IsGroupSpell(spell_id)) { range = spells[spell_id].aoe_range; } range = GetActSpellRange(spell_id, range); if (HasProjectIllusion() && IsIllusionSpell(spell_id)) { range = 100; } float dist2 = DistanceSquared(m_Position, tar->GetPosition()); float range2 = range * range; float min_range2 = spells[spell_id].min_range * spells[spell_id].min_range; if (dist2 > range2) { //target is out of range. return false; } else if (dist2 < min_range2) { //target is too close range. return false; } return true; } BotSpell Bot::GetBestBotSpellForNukeByBodyType(Bot* caster, uint8 body_type, uint16 spell_type, bool AE, Mob* tar) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (!caster || !body_type) { return result; } if (tar == nullptr) { tar = caster->GetTarget(); } switch (body_type) { case BodyType::Undead: case BodyType::SummonedUndead: case BodyType::Vampire: result = GetBestBotSpellForNukeByTargetType(caster, (!AE ? ST_Undead : ST_UndeadAE), spell_type, AE, tar); break; case BodyType::Summoned: case BodyType::Summoned2: case BodyType::Summoned3: result = GetBestBotSpellForNukeByTargetType(caster, (!AE ? ST_Summoned : ST_SummonedAE), spell_type, AE, tar); break; case BodyType::Animal: result = GetBestBotSpellForNukeByTargetType(caster, ST_Animal, spell_type, AE, tar); break; case BodyType::Plant: result = GetBestBotSpellForNukeByTargetType(caster, ST_Plant, spell_type, AE, tar); break; case BodyType::Giant: result = GetBestBotSpellForNukeByTargetType(caster, ST_Giant, spell_type, AE, tar); break; case BodyType::Dragon: result = GetBestBotSpellForNukeByTargetType(caster, ST_Dragon, spell_type, AE, tar); break; default: break; } return result; } BotSpell Bot::GetBestBotSpellForRez(Bot* caster, Mob* target, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::Revive); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if ( IsResurrectSpell(bot_spell_list_itr->SpellId) && caster->CheckSpellRecastTimer(bot_spell_list_itr->SpellId) ) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } return result; } BotSpell Bot::GetBestBotSpellForCharm(Bot* caster, Mob* target, uint16 spell_type) { BotSpell result; result.SpellId = 0; result.SpellIndex = 0; result.ManaCost = 0; if (caster) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SpellEffect::Charm); for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { if ( IsCharmSpell(bot_spell_list_itr->SpellId) && caster->CastChecks(bot_spell_list_itr->SpellId, target, spell_type) ) { result.SpellId = bot_spell_list_itr->SpellId; result.SpellIndex = bot_spell_list_itr->SpellIndex; result.ManaCost = bot_spell_list_itr->ManaCost; break; } } } return result; }