eqemu-server/zone/botspellsai.cpp
nytmyr f466964db8
[Bots] Bot Overhaul (#4580)
* Implement spell AI pulling, fix throw stone

* more pull tweaks

* holding check at start of ai process

* fully implement ^pull logic to always return, can still be overidden by ^attack

* Rewrite ^pull logic and handling. **MORE**

Add ^setassistee command to set who your bots will assist. Bots will always assist you first before anyone else.

If the rule Bots, AllowCrossGroupRaidAssist is enabled bots will assist the group or raid main assists.

Rewrites logic in handling of pull and returning to ensure bots make it back to their location.

* Move HateLine to a better ID

* cleanup ST_Self logic in CastChecks

* Removed unused BotSpellTypeRequiresLoS

* Move fizzle message to define

* add timer checks to Idle/Engaged/Pursue CastCheck to early terminate

* Add back !IsBotNonSpellFighter() check to the different CastCheck

* Correct IsValidSpellRange

* Implement AAs and harmtouch/layonhands to ^cast --- fix IsValidSpellRange

* Add PetDamageShields and PetResistBuffs to IsPetBotSpellType()

* Add priorities to HateLine inserts for db update

* Remove SpellTypeRequiresCastChecks

* Add bot check to DetermineSpellTargets for IsIllusionSpell

* merge with previous

* Correct bot checks for ST_GroupClientAndPet

* Remove misc target_type checks

* Add lull/aelull to ^cast

* Add more checks for CommandedSubTypes::AETarget

* remove unneeded checks on IsValidSpellTypeBySpellID

* add to aelull

* rewrite GetCorrectSpellType

* Add IsBlockedBuff to CastChecks

* Add spellid option to ^cast to allow casting of a specific spell by ID

* ^cast adjustments for spellid casts

* Add missing alert round for ranged attacks

* More castcheck improvements

* CanUseBotSpell for ^cast

* remove ht/loh from attack ai

* remove SetCombatRoundForAlerts that triggered every engagement

* Add RangedAttackImmunity checks before trying to ranged attack

* move bot backstab to mob

* fix MinStatusToBypassCreateLimit

* more backstab to mob cleanup

* add bot checks to tryheadshot / tryassassinate

* adjust version number for bots

* add back m_mob_check_moving_timer, necessary?

* add sanity checks for classattacks

* Get rid of Bots:BotGroupXP and change logic to support Bots:SameRaidGroupForXP

Bots won't do anything if not in the same group so this should more accurately control only when in the same raid group.

* add "confirm" check to ^delete

* Update bot.cpp

* Remove `id` from bot_settings, correct types

* Implement blocked_buffs and blocked_pet_buffs

* more blocked buff tweaks

* add beneficial check to ^blockedbuffs

* command grammar

* missing )

* Move getnames for categories and settings to mob, rename hptomed/manatomed

* add GetBotSpellCategoryIDByShortName and CopyBotBlockedPetBuffs, update ^defaultsettings command

* cls cleanup

* Allow bots to clear HasProjectIllusion flag

* Add PercentChanceToCastGroupCure

* Implmenet PetCures, add some missing types for defaults/chance to cast

* Change GetRaidByBotName to GetRaidByBot

* Typo on PetBuffs implement

* Change GetSpellListSpellType to GetParentSpellType

* missing from GetChanceToCastBySpellType

* Fix performance in IsValidSpellRange by flipping HasProjectIllusion

* merge with prev

* merge with cls cleanup

* Reorder IsTargetAlreadyReceivingSpell/CheckSpellLevelRestriction/IsBlockedBuff

* Combine GatherGroupSpellTargets and GatherSpellTargets

* Cleanup IsTargetAlreadyReceivingSpell

* Fix ^petsettype to account for usable levels of spells and remove hardcoded level limits.

* Remove Bot_AICheckCloseBeneficialSpells and use AttemptCloseBeneficialSpells for better performance

* remove default hold for resist buffa

* move IsValidSpellRange further down castchecks

* raid optimizations

* correct name checking to match players

* more name checks and add proper soft deletes to bots

* organize some checks in IsImmuneToBotSpell

* Fix GetRaidByBotName and GetRaidByBot checks to not loop unnecessarily

* Move GatherSpellTargets to mob

* Change GetPrioritizedBotSpellsBySpellType to vector

Some slipped through in "organize some checks in IsImmuneToBotSpell"

* Move GatherSpellTargets and Raid to stored variables.

Missing some in "organize some checks in IsImmuneToBotSpell"

* comment out precheck, delays, thresholds, etc logging

missed some in "organize some checks in IsImmuneToBotSpell"

* Missing IsInGroupOrRaid cleanup

* Implement AIBot_spells_by_type to reduce looping when searching for spells

* Add _tempSpellType as placeholder for any future passthru

* todo

* Move bot_list from std::list to std::unordered_map like other entities

* Fix missing raid assignment for GetStoredRaid in IsInGroupOrRaid

* TempPet owned by bots that get the kill will now give exp like a client would

* Remove unnecessary checks in bot process (closescanmoving timer, verify raid, send hp/mana/end packet

* Fix client spell commands from saving the wrong setting

* Cleanup ^copysettings command and add new commands

* Add pet option to ^taunt

No longer has toggle, required on/off option and an optional "pet" option to control pets' taunting state

* Allow pet types to ^cast, prevent failure spam, add cure check

* more raid optimizations, should be final.

10 clients, 710 bots, 10 raids, ~250 pets sits around 3.5% CPU idle

* Move spell range check to proper location

* Implement ^discipline

* remove ^aggressive/^defensive

* remove this for a separate PR

* cleanup

* Add BotGroupSay method

* todo list

* Add missing bot_blocked_buffs to schema

* Remove plural on ^spelltypeidsand ^spelltypenames

* Move spelltype names, spell subtypes, category names and setting names to maps.

* move los checks to mob.cpp

* Bot CampAll fix

* Bots special_attacks.cpp fix

* Add zero check for bot spawn limits

If the spawn limit rule is set to 0 and spawn limit is set by bucket, if no class buckets are set, it defaults to the rule of 0 and renders the player unable to spawn bots.

This adds a check where if the rule and class bucket are 0, it will check for the spawn limit bucket

* Add HasSkill checks to bot special abilities (kick/bash/etc)

* code cleanup 1

* code cleanup 2

* code cleanup 3

* code cleanup 4

* fix ^cast wirh commanded types

* Remove bcspells, fix helper_send_usage_required_bots

* linux build fix

* remove completed todo

* Allow inventory give to specific ID slots

* Update TODO

* Correct slot ranges for inventorygive

* Add zone specific spawn limits and zone specific forced spawn limits

* remove bd. from update queries where it doesn't exist

* Rename _spellSettings to m_bot_spell_settings

* Add IsPetOwnerOfClientBot(), add Lua and Perl methods

* Make botOwnerCharacterID snakecase

* Throw bot_camp_timer behind Bots:Enabled rule

* Move various Bot<>Checks logging to BotSpellChecks

* Remove from LogCategoryName

* Consolidate IsInGroupOrRaid

* Consolidate GatherSpellTargets

* Add missing Bot Spell Type Checks to log

* Add GetParentSpellType when checking spelltypes for idle, engaged, pursue CastChecks.

* Consolidate AttemptForcedCastSpell

* Consolidate SetBotBlockedBuff/SetBotBlockedPetBuff

* Add list option to ^spellpriority commands.

* Move client functions to client_bot

* Move mob functions to mob_bot

* Move bot spdat functions to spdat_bot

* Move SendCommandHelpWindow to SendBotCommandHelpWindow and simplify

* Change char_id to character_id for bot_settings

* update todo

* Fix typo on merge conflict

* Cleanup command format changes, remove hardcoded class IDs in examples.

* Set #illusionblock for players to guide access

* Move client commands for bot spells from gm commands to existing bot commands

* Fix alignment issues

* More alignment fixes

* More cleanup 1

* More cleanup 2

* Fix BotMeditate to med at proper percentages

* Correct GetStopMeleeLevel checks for some buff checks

* Add back hpmanaend update to bot raid, force timer update to prevent spamming

* Remove log

* Cleanup ranged and ammo calculations - Adds throwing check for match

* Add check in distance calculations to stay at range if set even if no ammo or ranged

* Move melee distance calculations to better function

* Add GetBuffTargets helper

* Missing p_item, s_item in CombatRangeInput

* Linux test?

* Reduce GetCorrectBotSpellType branching slightly

This is still an ugly ass function but my brain is melted

* Line fixes

* Make bot pets only do half damage in pvp

* Add bot pet pvp damage to tune

* Add bot pet check for AIYellForHelp

* Add bots to UseSpellImpliedTargeting

* Move toggleranged, togglehelm and illusionblock to new help window. Add actionable support

* Add bot and bot pet checks to various spells, auras and targeting checks that were missing.

* update todo

* New lines

* Correct DoLosChecks

* Remove Log TestDebug

* Remove _Struct from struct declarations

* Add bot check to IsAttackAllowed for GetUltimateOwner to skip entity list where possible

* Wrap SaveBotSettings in Bots Enabled check

* Remove comment

* Wrap bot setting loading for clients in bots enabled rule

* Cleanup BlockedBuffs logic in SpellOnTarget

* Rename BotSpells_Struct/BotSpells_Struct_wIndex

* Rename spawn/create status bypass rules, fix return for spawn limit

* Remove unnecessary return in CanBuffStack, cleanup

* Enable recastdelay support for clients

* Remove unused variables

* Rename _assistee to bot_assistee

* hardcode BotCommandHelpWindow colors

* todo

* Fix ^cast summoncorpse

* todo

* Reimplement secondary colors to BotSendCommandHelpWindow

* Give ^copysettings/^defaultsettings more options, cleanup.

* Cleanup some commands

* Add comment to CheckLosCheat/CheckLosCheatExempt

* Make struct BotSpellSettings snake case

* Allow duplicate casts of same spell on target for heals and cures

* Add default delay to cures

* Remove unused methods

* Implement missing ^spellresistlimits/^resistlimits command

* Move functions out of mob.h and cleanup

* Return for GetRawBotList

This checks offline bots too

* Rename BotGroupSay to RaidGroupSay

* Prevent bots from forming their own group if a bot that is a group leader is removed from the raid

* Linux fix?

* IsPetOwner fixes

* Add remove option to list for ^blockedbuffs / ^blockedpetbuffs

* Implement ^spellannouncecasts to toggle announcing casts of spell types

* Remove rule Bots:BardsAnnounceCasts

* Update bot.h

* Remove unused no_pets option from GatherSpellTargets

* Move ^attack response back to normal chat window (other)

* Set lower limit of spell delays to 100 rather than 1

* Correct pet checks on GetUltimateSpell functions

* Add rules (Bots, AICastSpellTypeDelay, Bots, AICastSpellTypeHeldDelay) to prevent spamming of failed spell type AI casts

* Correct pet buff type logic to catch DS/Resists with other spell effects in them

* Fix defaults for clients

* Add more logic for necros/shaman for default heal thresholds due to lich and canni

* Rename SpellHold, SpellDelay, SpellMinThreshold, SpellMaxThreshold, SpellRecastDelay to fit SpellType style naming

* Use GetTempSpellType() for announce check in RaidGroupSay

* Make all spell shortnames plural where applicable

* Update bot.cpp

* Bots:BotsUseLiveBlockedMessage filter to spell failure

* Move GetSpellTargetList to only get called when necessary to reduce overhead

* formatting

* Formatting

* Simplify case SE_Illusion and SE_IllusionCopy for GetIllusionBlock

* Clean up InterruptSpell

* Cleanup IsBot() checks for DetermineSpellTargets->ST_GroupClientAndPet

* Cleanup range/aoe_range check in SpellFinished

* Cleanup DetermineSpellTargets->ST_GroupNoPets

* Cleanup DetermineSpellTargets->ST_Self for bot summon corpse

* Cleanup DetermineSpellTargets->ST_Pet

* Cleanup bot logic in TryBackstab

* Cleanup IsAttackAllowed checks for bots and their pets

* Cleanup StopMoving for bots

* Cleanup CanThisClassTripleAttack

* Fix casting for GetIllusionBlock checks

* Formatting

* Fix DetermineSpellTargets for group spells (this also wasn't properly checking the rule Character:EnableTGB in master)

* Cleanup spelltarget grabbing logic, consolidate group heals in to GetNumberNeedingHealedInGroup

* Throw added client los pet checks behind LoS cheat rule for bots

* CLeanup give_exp on npc death logic and ensure client pets always pass.

* Undo unintended rename from previous refactor

* Remove pointless Bots, SameRaidGroupForXP rule

* Revision to 0690783a9d1e99005d6bee0824597ea920e26df9

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2025-02-03 04:02:42 -06:00

3044 lines
89 KiB
C++

/* 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<BotSpell_wPriority> 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(
this,
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(
this,
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<BotSpell_wPriority> 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(
this,
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(
this,
fmt::format(
"Curing the group with {}.",
GetSpellName(bot_spell.SpellId)
).c_str()
);
}
else {
if (!IsCommandedSpell()) {
SetBotSpellRecastTimer(spell_type, tar, true);
}
RaidGroupSay(
this,
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(
this,
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<BotSpell_wPriority> 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(
this,
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(
this,
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(
this,
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(
this,
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(
this,
fmt::format(
"Casting {} on {}, please stay in range!",
spells[botSpell.SpellId].name,
tar->GetCleanName()
).c_str()
);
}
return castedSpell;
}
std::list<BotSpell> Bot::GetBotSpellsForSpellEffect(Bot* caster, uint16 spell_type, int spell_effect) {
std::list<BotSpell> result;
if (!caster) {
return result;
}
if (auto bot_owner = caster->GetBotOwner(); !bot_owner) {
return result;
}
if (caster->AI_HasSpells()) {
std::vector<BotSpells_wIndex> 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 == caster->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<BotSpell> Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* caster, uint16 spell_type, int spell_effect, SpellTargetType target_type) {
std::list<BotSpell> result;
if (!caster) {
return result;
}
if (auto bot_owner = caster->GetBotOwner(); !bot_owner) {
return result;
}
if (caster->AI_HasSpells()) {
std::vector<BotSpells_wIndex> 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 == caster->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<BotSpell> Bot::GetBotSpellsBySpellType(Bot* caster, uint16 spell_type) {
std::list<BotSpell> result;
if (!caster) {
return result;
}
if (auto bot_owner = caster->GetBotOwner(); !bot_owner) {
return result;
}
if (caster->AI_HasSpells()) {
std::vector<BotSpells_wIndex> 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 == caster->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<BotSpell_wPriority> Bot::GetPrioritizedBotSpellsBySpellType(Bot* caster, uint16 spell_type, Mob* tar, bool AE, uint16 sub_target_type, uint16 sub_type) {
std::vector<BotSpell_wPriority> result;
if (caster && caster->AI_HasSpells()) {
std::vector<BotSpells_wIndex> 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 == caster->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()) {
std::vector<BotSpells_wIndex> 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 == caster->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<BotSpell> 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<BotSpell> 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<BotSpell> 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()) {
std::vector<BotSpells_wIndex> 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 == caster->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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP);
for (std::list<BotSpell>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP);
for (std::list<BotSpell>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CurrentHP);
int target_count = 0;
for (std::list<BotSpell>::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::GetBestBotSpellForGroupHealOverTime(Bot* caster, Mob* tar, uint16 spell_type) {
BotSpell result;
result.SpellId = 0;
result.SpellIndex = 0;
result.ManaCost = 0;
if (caster) {
std::list<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_HealOverTime);
int target_count = 0;
for (std::list<BotSpell>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_CompleteHeal);
int target_count = 0;
for (std::list<BotSpell>::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::GetBestBotSpellForMez(Bot* caster, uint16 spell_type) {
BotSpell result;
result.SpellId = 0;
result.SpellIndex = 0;
result.ManaCost = 0;
if (caster) {
std::list<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_Mez);
for (std::list<BotSpell>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_SummonPet);
std::string pet_type = GetBotMagicianPetType(caster);
for(std::list<BotSpell>::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<BotSpell> 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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SE_CurrentHP, target_type);
for(std::list<BotSpell>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffectAndTargetType(caster, spell_type, SE_Stun, target_type);
for(std::list<BotSpell>::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<BotSpell> 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<BotSpell>::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<BotSpell>::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()) {
std::vector<BotSpells_wIndex> 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()) {
std::vector<BotSpells_wIndex> 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<BotSpell_wPriority> 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<BotSpell_wPriority>::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<BotSpell_wPriority>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_Revive);
for (std::list<BotSpell>::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<BotSpell> bot_spell_list = GetBotSpellsForSpellEffect(caster, spell_type, SE_Charm);
for (std::list<BotSpell>::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
);
}
}
}