/* EQEMu: Everquest Server Emulator Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) 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; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY except by those people which sell it, which are required to give you total support for your newly bought product; 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #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 (chance < 100 && 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(DoLosChecks(tar)); } else { 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 (!IsValidSpellAndLoS(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 (IsBotSpellTypeOtherBeneficial(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 (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { continue; } if (!IsCommandedSpell()) { Mob* add_mob = GetFirstIncomingMobToMez(this, s.SpellId, spell_type, IsAEBotSpellType(spell_type)); if (!add_mob) { return false; } tar = add_mob; } if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { if (IsBotSpellTypeOtherBeneficial(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 (!IsValidSpellAndLoS(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, SE_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 (!IsValidSpellAndLoS(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 (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 (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { return false; } } if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { bot_spell = GetBestBotSpellForNukeByBodyType(this, tar->GetBodyType(), spell_type, IsAEBotSpellType(spell_type), tar); } if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS()) && spell_type == BotSpellTypes::Nuke && bot_class == Class::Wizard) { bot_spell = GetBestBotWizardNukeSpellByTargetResists(this, tar, spell_type); } if (!IsValidSpellAndLoS(bot_spell.SpellId, HasLoS())) { std::vector bot_spell_list = GetPrioritizedBotSpellsBySpellType(this, spell_type, tar, IsAEBotSpellType(spell_type)); for (const auto& s : bot_spell_list) { if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { 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) { 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 && AIBot_spells[i].type == BotSpellTypes::Buff ) { 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 (!IsValidSpellAndLoS(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 (!IsValidSpellAndLoS(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 (!IsValidSpellAndLoS(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 (!IsValidSpellAndLoS(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 (!IsValidSpellAndLoS(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, SE_CurrentHP); for (auto bot_spell_list_itr : bot_spell_list) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_CurrentHP); for (auto bot_spell_list_itr : bot_spell_list) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_HealOverTime); for (auto bot_spell_list_itr : bot_spell_list) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP); int target_count = 0; for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order if (IsRegularGroupHealSpell(bot_spell_list_itr->SpellId)) { uint16 spell_id = bot_spell_list_itr->SpellId; if (!caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { 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) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_HealOverTime); int target_count = 0; for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order if (IsGroupHealOverTimeSpell(bot_spell_list_itr->SpellId)) { uint16 spell_id = bot_spell_list_itr->SpellId; if (!caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { 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) { std::list bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CompleteHeal); int target_count = 0; for (std::list::iterator bot_spell_list_itr = bot_spell_list.begin(); bot_spell_list_itr != bot_spell_list.end(); ++bot_spell_list_itr) { // Assuming all the spells have been loaded into this list by level and in descending order if (IsGroupCompleteHealSpell(bot_spell_list_itr->SpellId)) { uint16 spell_id = bot_spell_list_itr->SpellId; if (!caster->IsCommandedSpell() && caster->IsValidSpellRange(spell_id, tar)) { target_count = caster->GetNumberNeedingHealedInGroup(tar, spell_type, spell_id, caster->GetAOERange(spell_id)); if (target_count < caster->GetSpellTypeAEOrGroupTargetCount(spell_type)) { 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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, int16 spell_id, uint16 spell_type, bool AE) { Mob* result = nullptr; if (caster && caster->GetOwner()) { int spell_range = (!AE ? caster->GetActSpellRange(spell_id, spells[spell_id].range) : caster->GetActSpellRange(spell_id, spells[spell_id].aoe_range)); int buff_count = 0; NPC* npc = nullptr; for (auto& close_mob : caster->m_close_mobs) { buff_count = 0; npc = close_mob.second->CastToNPC(); if (!npc) { continue; } if (!caster->IsValidMezTarget(caster->GetOwner(), npc, spell_id)) { continue; } if (AE) { int target_count = 0; for (auto& close_mob : caster->m_close_mobs) { Mob* m = close_mob.second; if (npc == m) { continue; } if (!caster->IsValidMezTarget(caster->GetOwner(), m, spell_id)) { continue; } if (IsPBAESpell(spell_id)) { if (spell_range < Distance(caster->GetPosition(), m->GetPosition())) { continue; } } else { if (spell_range < Distance(m->GetPosition(), npc->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; } if (zone->random.Int(1, 100) < RuleI(Bots, AEMezChance)) { caster->SetSpellTypeRecastTimer(spell_type, RuleI(Bots, MezFailDelay)); return result; } result = npc; } else { if (spell_range < Distance(caster->GetPosition(), npc->GetPosition())) { continue; } if (!caster->CastChecks(spell_id, npc, spell_type, true)) { continue; } if (zone->random.Int(1, 100) < RuleI(Bots, MezChance)) { caster->SetSpellTypeRecastTimer(spell_type, RuleI(Bots, MezAEFailDelay)); return result; } result = npc; } if (result) { caster->SetHasLoS(true); 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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) { // Assuming all the spells have been loaded into this list by level and in descending order 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 (!IsValidSpellAndLoS(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 (!IsValidSpellAndLoS(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, SE_ResistMagic) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || (needs_cold_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistCold) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || (needs_fire_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistFire) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || (needs_poison_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistPoison) || IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistAll))) || (needs_disease_resist_debuff && (IsEffectInSpell(bot_spell_list[i].spellid, SE_ResistDisease) || IsEffectInSpell(bot_spell_list[i].spellid, SE_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(SE_PoisonCounter)) { ++count_poisoned; } if (m->FindType(SE_DiseaseCounter)) { ++count_diseased; } if (m->FindType(SE_CurseCounter)) { ++count_cursed; } if (m->FindType(SE_CorruptionCounter)) { ++count_corrupted; } } } } if ( (count_poisoned >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_PoisonCounter)) || (count_diseased >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_DiseaseCounter)) || (count_cursed >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_CurseCounter)) || (count_corrupted >= caster->GetSpellTypeAEOrGroupTargetCount(spell_type) && IsEffectInSpell(itr->SpellId, SE_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(SE_PoisonCounter) && IsEffectInSpell(itr->SpellId, SE_PoisonCounter) || tar->FindType(SE_DiseaseCounter) && IsEffectInSpell(itr->SpellId, SE_DiseaseCounter) || tar->FindType(SE_CurseCounter) && IsEffectInSpell(itr->SpellId, SE_CurseCounter) || tar->FindType(SE_CorruptionCounter) && IsEffectInSpell(itr->SpellId, SE_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::AEMez: 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); 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 (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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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, SE_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) { // Assuming all the spells have been loaded into this list by level and in descending order 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; } void Bot::CheckBotSpells() { auto spell_list = BotSpellsEntriesRepository::All(content_db); uint16 spell_id; SPDat_Spell_Struct spell; uint16 correct_type; uint16 parent_type; for (const auto& s : spell_list) { if (!IsValidSpell(s.spell_id)) { LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id); continue; } spell = spells[s.spell_id]; spell_id = spell.id; if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] >= 255) { LogBotSpellTypeChecks("{} [#{}] is not usable by a {} [#{}].", GetSpellName(spell_id), spell_id, GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX), s.npc_spells_id); } else { if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] > s.minlevel) { LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the min level is currently set to {}." , GetSpellName(spell_id) , spell_id , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) , s.npc_spells_id , s.minlevel ); LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]" , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , spell_id , s.npc_spells_id , GetSpellName(spell_id) , spell_id , s.minlevel , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) , s.npc_spells_id ); } if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] < s.minlevel) { LogBotSpellTypeChecks("{} [#{}] could be used starting at level {} for a {} [#{}] instead of the current min level of {}." , GetSpellName(spell_id) , spell_id , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) , s.npc_spells_id , s.minlevel ); LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]" , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , spell_id , s.npc_spells_id , GetSpellName(spell_id) , spell_id , s.minlevel , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) , s.npc_spells_id ); } if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] > s.maxlevel) { LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the max level is currently set to {}." , GetSpellName(spell_id) , spell_id , spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) , s.npc_spells_id , s.maxlevel ); } } correct_type = GetCorrectBotSpellType(s.type, spell_id); parent_type = GetParentSpellType(correct_type); if (RuleB(Bots, UseParentSpellTypeForChecks)) { if (s.type == parent_type || s.type == correct_type) { continue; } } else { if (IsPetBotSpellType(s.type)) { correct_type = GetPetBotSpellType(correct_type); } } if (correct_type == s.type) { continue; } if (correct_type == UINT16_MAX) { LogBotSpellTypeChecks("{} [#{}] is incorrect. It is currently set as {} [#{}] but the correct type is unknown." , GetSpellName(spell_id) , spell_id , GetSpellTypeNameByID(s.type) , s.type ); } else { LogBotSpellTypeChecks("{} [#{}] is incorrect. It is currently set as {} [#{}] and should be {} [#{}]" , GetSpellName(spell_id) , spell_id , GetSpellTypeNameByID(s.type) , s.type , GetSpellTypeNameByID(correct_type) , correct_type ); LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `type` = {} WHERE `spell_id` = {}; -- {} [#{}] from {} [#{}] to {} [#{}]" , correct_type , spell_id , GetSpellName(spell_id) , spell_id , GetSpellTypeNameByID(s.type) , s.type , GetSpellTypeNameByID(correct_type) , correct_type ); } } } void Bot::MapSpellTypeLevels() { commanded_spells_min_level.clear(); auto start = std::min({ BotSpellTypes::START, BotSpellTypes::COMMANDED_START, BotSpellTypes::DISCIPLINE_START }); auto end = std::max({ BotSpellTypes::END, BotSpellTypes::COMMANDED_END, BotSpellTypes::DISCIPLINE_END }); for (int i = start; i <= end; ++i) { if (!Bot::IsValidBotSpellType(i)) { continue; } for (int x = Class::Warrior; x <= Class::Berserker; ++x) { commanded_spells_min_level[i][x] = { UINT8_MAX, "" }; } } auto spell_list = BotSpellsEntriesRepository::All(content_db); for (const auto& s : spell_list) { if (!IsValidSpell(s.spell_id)) { LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id); continue; } uint16_t spell_type = s.type; int32_t bot_class = s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX; uint8_t min_level = s.minlevel; if ( !EQ::ValueWithin(bot_class, Class::Warrior, Class::Berserker) || !Bot::IsValidBotSpellType(spell_type) ) { continue; } auto& spell_info = commanded_spells_min_level[spell_type][bot_class]; if (min_level < spell_info.min_level) { spell_info.min_level = min_level; spell_info.description = StringFormat( "%s [#%u]: Level %u", GetClassIDName(bot_class), bot_class, min_level ); } } }