From 37e9d419e60d6de7bce90b1e2f03480658c61c8a Mon Sep 17 00:00:00 2001 From: neckkola <65987027+neckkola@users.noreply.github.com> Date: Sat, 15 Jan 2022 11:27:50 -0400 Subject: [PATCH] More bot raid changes --- bin/zone/bot_raid.cpp | 34 - zone/bot.cpp | 19 +- zone/bot.h | 8 +- zone/bot_raid.cpp | 1530 +++++++++++++++++++++++++++++++++ {bin/zone => zone}/bot_raid.h | 9 +- zone/botspellsai.cpp | 7 + zone/raids.h | 1 + 7 files changed, 1555 insertions(+), 53 deletions(-) delete mode 100644 bin/zone/bot_raid.cpp create mode 100644 zone/bot_raid.cpp rename {bin/zone => zone}/bot_raid.h (93%) diff --git a/bin/zone/bot_raid.cpp b/bin/zone/bot_raid.cpp deleted file mode 100644 index dd65d68ad..000000000 --- a/bin/zone/bot_raid.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* 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 -*/ - -#ifdef BOTS - -#include "bot.h" -#include "bot_raid.h" -#include "object.h" -#include "raids.h" -#include "doors.h" -#include "quest_parser_collection.h" -#include "lua_parser.h" -#include "../common/string_util.h" -#include "../common/say_link.h" - -extern volatile bool is_zone_loaded; -extern bool Critical; - -#endif diff --git a/zone/bot.cpp b/zone/bot.cpp index 09486f678..e345bf50a 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -2300,7 +2300,11 @@ bool Bot::Process() } // Bot AI - AI_Process(); + Raid* raid = entity_list.GetRaidByBot(this); + if (raid) + AI_Process_Raid(); + else + AI_Process(); return true; } @@ -2540,12 +2544,10 @@ void Bot::AI_Process() Client* bot_owner = (GetBotOwner() && GetBotOwner()->IsClient() ? GetBotOwner()->CastToClient() : nullptr); Group* bot_group = GetGroup(); - Raid* bot_raid = entity_list.GetRaidByBot(this); - //#pragma region PRIMARY AI SKIP CHECKS // Primary reasons for not processing AI - if (!bot_owner || (!bot_group && !bot_raid) || !IsAIControlled()) { + if (!bot_owner || (!bot_group) || !IsAIControlled()) { return; } @@ -2563,15 +2565,6 @@ void Bot::AI_Process() if (bot_group) { leash_owner = (bot_group->GetLeader() && bot_group->GetLeader()->IsClient() ? bot_group->GetLeader()->CastToClient() : bot_owner); } - else if (bot_raid) { - int bot_raid_group = bot_raid->GetGroup(GetName()); - if (bot_raid_group > 0) { - leash_owner = bot_raid->GetGroupLeader(bot_raid_group)->CastToClient(); - } - else { - leash_owner = bot_raid->GetLeader(); - } - } if (!leash_owner) { return; diff --git a/zone/bot.h b/zone/bot.h index a553c2f6c..8062c645a 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -362,8 +362,6 @@ public: static std::string RaceIdToString(uint16 raceId); static bool IsBotAttackAllowed(Mob* attacker, Mob* target, bool& hasRuleDefined); static Bot* GetBotByBotClientOwnerAndBotName(Client* c, std::string botName); - static void ProcessRaidInvite(Bot* invitee, Client* invitor); //Mitch - static void ProcessRaidInvite(Client* invitee, Client* invitor); //Mitch static void ProcessBotGroupInvite(Client* c, std::string botName); static void ProcessBotGroupDisband(Client* c, std::string botName); static void BotOrderCampAll(Client* c); @@ -381,6 +379,12 @@ public: static bool CheckDisciplineRecastTimers(Bot *caster, int timer_index); static uint32 GetDisciplineRemainingTime(Bot *caster, int timer_index); + //Raid methods + void PetAIProcess_Raid(); + void AI_Process_Raid(); + static void ProcessRaidInvite(Bot* invitee, Client* invitor); //Mitch + static void ProcessRaidInvite(Client* invitee, Client* invitor); //Mitch + static std::list GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect); static std::list GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, int spellEffect, SpellTargetType targetType); static std::list GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType); diff --git a/zone/bot_raid.cpp b/zone/bot_raid.cpp new file mode 100644 index 000000000..b695b2de6 --- /dev/null +++ b/zone/bot_raid.cpp @@ -0,0 +1,1530 @@ +/* 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 +*/ + +#ifdef BOTS + +#include "bot.h" +#include "bot_raid.h" +#include "object.h" +#include "raids.h" +#include "doors.h" +#include "quest_parser_collection.h" +#include "lua_parser.h" +#include "../common/string_util.h" +#include "../common/say_link.h" + +extern volatile bool is_zone_loaded; +extern bool Critical; + +// AI Processing for the Bot object + +constexpr float MAX_CASTER_DISTANCE[PLAYER_CLASS_COUNT] = { + 0, (34 * 34), (24 * 24), (28 * 28), (26 * 26), (42 * 42), 0, (30 * 30), 0, (38 * 38), (54 * 54), (48 * 48), (52 * 52), (50 * 50), (32 * 32), 0 + // W C P R S D M B R S N W M E B B + // A L A N H R N R O H E I A N S E + // R R L G D U K D G M C Z G C T R +}; + +void Bot::AI_Process_Raid() +{ +#define TEST_COMBATANTS() if (!GetTarget() || GetAppearance() == eaDead) { return; } +#define PULLING_BOT (GetPullingFlag() || GetReturningFlag()) +#define NOT_PULLING_BOT (!GetPullingFlag() && !GetReturningFlag()) +#define GUARDING (GetGuardFlag()) +#define NOT_GUARDING (!GetGuardFlag()) +#define HOLDING (GetHoldFlag()) +#define NOT_HOLDING (!GetHoldFlag()) +#define PASSIVE (GetBotStance() == EQ::constants::stancePassive) +#define NOT_PASSIVE (GetBotStance() != EQ::constants::stancePassive) + + Raid* raid = entity_list.GetRaidByBot(this); + Client* bot_owner = (GetBotOwner() && GetBotOwner()->IsClient() ? GetBotOwner()->CastToClient() : nullptr); + int r_group = raid->GetGroup(GetName()); + + LogAI("Bot_Raid: Entered Raid Process() for [{}].", this->GetCleanName()); + + //#pragma region PRIMARY AI SKIP CHECKS + + // Primary reasons for not processing AI + if (!bot_owner || (!raid) || !IsAIControlled()) { + return; + } + + if (bot_owner->IsDead()) { + + SetTarget(nullptr); + SetBotOwner(nullptr); + + return; + } + + // We also need a leash owner and follow mob (subset of primary AI criteria) + Client* leash_owner = nullptr; + if (r_group > 0) { + leash_owner = raid->GetGroupLeader(r_group)->CastToClient(); + } + else { + leash_owner = raid->GetLeader(); + } + + if (!leash_owner) { + return; + } + + //#pragma endregion + + Mob* follow_mob = entity_list.GetMob(GetFollowID()); + if (!follow_mob) { + + follow_mob = leash_owner; + SetFollowID(leash_owner->GetID()); + } + + // Berserk updates should occur if primary AI criteria are met + if (GetClass() == WARRIOR || GetClass() == BERSERKER) { + + if (!berserk && GetHP() > 0 && GetHPRatio() < 30.0f) { + + entity_list.MessageCloseString(this, false, 200, 0, BERSERK_START, GetName()); + berserk = true; + } + + if (berserk && GetHPRatio() >= 30.0f) { + + entity_list.MessageCloseString(this, false, 200, 0, BERSERK_END, GetName()); + berserk = false; + } + } + + //#pragma region SECONDARY AI SKIP CHECKS + + // Secondary reasons for not processing AI + if (GetPauseAI() || IsStunned() || IsMezzed() || (GetAppearance() == eaDead)) { + + if (IsCasting()) { + InterruptSpell(); + } + + if (IsMyHealRotationSet() || (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this)) { + + AdvanceHealRotation(false); + m_member_of_heal_rotation->SetMemberIsCasting(this, false); + } + + return; + } + + //#pragma endregion + + float fm_distance = DistanceSquared(m_Position, follow_mob->GetPosition()); + float lo_distance = DistanceSquared(m_Position, leash_owner->GetPosition()); + float leash_distance = RuleR(Bots, LeashDistance); + + //#pragma region CURRENTLY CASTING CHECKS + + if (IsCasting()) { + + if (IsHealRotationMember() && + m_member_of_heal_rotation->CastingOverride() && + m_member_of_heal_rotation->CastingTarget() != nullptr && + m_member_of_heal_rotation->CastingReady() && + m_member_of_heal_rotation->CastingMember() == this && + !m_member_of_heal_rotation->MemberIsCasting(this)) + { + InterruptSpell(); + } + else if (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this) { + + AdvanceHealRotation(false); + return; + } + else if (GetClass() != BARD) { + + if (IsEngaged()) { + return; + } + + if ( + (NOT_GUARDING && fm_distance > GetFollowDistance()) || // Cancel out-of-combat casting if movement to follow mob is required + (GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) > GetFollowDistance()) // Cancel out-of-combat casting if movement to guard point is required + ) { + InterruptSpell(); + } + + return; + } + } + else if (IsHealRotationMember()) { + m_member_of_heal_rotation->SetMemberIsCasting(this, false); + } + + //#pragma endregion + + // Can't move if rooted... + if (IsRooted() && IsMoving()) { + + StopMoving(); + return; + } + + //#pragma region HEAL ROTATION CASTING CHECKS + + if (IsMyHealRotationSet()) { + + if (AIHealRotation(HealRotationTarget(), UseHealRotationFastHeals())) { + + m_member_of_heal_rotation->SetMemberIsCasting(this); + m_member_of_heal_rotation->UpdateTargetHealingStats(HealRotationTarget()); + AdvanceHealRotation(); + } + else { + + m_member_of_heal_rotation->SetMemberIsCasting(this, false); + AdvanceHealRotation(false); + } + } + + //#pragma endregion + + bool bo_alt_combat = (RuleB(Bots, AllowOwnerOptionAltCombat) && bot_owner->GetBotOption(Client::booAltCombat)); + + //#pragma region ATTACK FLAG + + if (GetAttackFlag()) { // Push owner's target onto our hate list + + if (GetPet() && PULLING_BOT) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + + if (NOT_HOLDING && NOT_PASSIVE) { + + auto attack_target = bot_owner->GetTarget(); + if (attack_target) { + + InterruptSpell(); + WipeHateList(); + AddToHateList(attack_target, 1); + SetTarget(attack_target); + SetAttackingFlag(); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->WipeHateList(); + GetPet()->AddToHateList(attack_target, 1); + GetPet()->SetTarget(attack_target); + } + } + } + } + + //#pragma endregion + + //#pragma region PULL FLAG + + else if (GetPullFlag()) { // Push owner's target onto our hate list and set flags so other bots do not aggro + + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + + if (NOT_HOLDING && NOT_PASSIVE) { + + auto pull_target = bot_owner->GetTarget(); + if (pull_target) { + + Bot::BotGroupSay(this, "Pulling %s to the group..", pull_target->GetCleanName()); + InterruptSpell(); + WipeHateList(); + AddToHateList(pull_target, 1); + SetTarget(pull_target); + SetPullingFlag(); + bot_owner->SetBotPulling(); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + m_previous_pet_order = GetPet()->GetPetOrder(); + GetPet()->SetPetOrder(SPO_Guard); + } + } + } + } + + //#pragma endregion + + //#pragma region ALT COMBAT (ACQUIRE HATE) + + else if (bo_alt_combat && m_alt_combat_hate_timer.Check(false)) { // 'Alt Combat' gives some more 'control' options on how bots process aggro + + // Empty hate list - let's find some aggro + if (!IsEngaged() && NOT_HOLDING && NOT_PASSIVE && (!bot_owner->GetBotPulling() || NOT_PULLING_BOT)) { + + Mob* lo_target = leash_owner->GetTarget(); + if (lo_target && + lo_target->IsNPC() && + !lo_target->IsMezzed() && + ((bot_owner->GetBotOption(Client::booAutoDefend) && lo_target->GetHateAmount(leash_owner)) || leash_owner->AutoAttackEnabled()) && + lo_distance <= leash_distance && + DistanceSquared(m_Position, lo_target->GetPosition()) <= leash_distance && + (CheckLosFN(lo_target) || leash_owner->CheckLosFN(lo_target)) && + IsAttackAllowed(lo_target)) + { + AddToHateList(lo_target, 1); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->AddToHateList(lo_target, 1); + GetPet()->SetTarget(lo_target); + } + } + else { + + std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); + for (RaidMember iter : raid_group_members) { + +// for (int counter = 0; counter < raid->GroupCount(r_group); counter++) { +// Group* bot_group = this->GetGroup(); + Mob* bg_member = iter.member;// bot_group->members[counter]; + if (!bg_member) { + continue; + } + + Mob* bgm_target = bg_member->GetTarget(); + if (!bgm_target || !bgm_target->IsNPC()) { + continue; + } + + if (!bgm_target->IsMezzed() && + ((bot_owner->GetBotOption(Client::booAutoDefend) && bgm_target->GetHateAmount(bg_member)) || leash_owner->AutoAttackEnabled()) && + lo_distance <= leash_distance && + DistanceSquared(m_Position, bgm_target->GetPosition()) <= leash_distance && + (CheckLosFN(bgm_target) || leash_owner->CheckLosFN(bgm_target)) && + IsAttackAllowed(bgm_target)) + { + AddToHateList(bgm_target, 1); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->AddToHateList(bgm_target, 1); + GetPet()->SetTarget(bgm_target); + } + + break; + } + } + } + } + } + + //#pragma endregion + + glm::vec3 Goal(0, 0, 0); + + // We have aggro to choose from + if (IsEngaged()) { + + if (rest_timer.Enabled()) { + rest_timer.Disable(); + } + + //#pragma region PULLING FLAG (TARGET VALIDATION) + + if (GetPullingFlag()) { + + if (!GetTarget()) { + + WipeHateList(); + SetTarget(nullptr); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + + return; + } + else if (GetTarget()->GetHateList().size()) { + + WipeHateList(); + SetTarget(nullptr); + SetPullingFlag(false); + SetReturningFlag(); + + return; + } + else { + // Default action is to aggress towards enemy + } + } + + //#pragma endregion + + //#pragma region RETURNING FLAG + + else if (GetReturningFlag()) { + + // Need to make it back to group before clearing return flag + if (fm_distance <= GetFollowDistance()) { + + // Once we're back, clear blocking flags so everyone else can join in + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + } + + // Need to keep puller out of combat until they reach their 'return to' destination + if (HasTargetReflection()) { + + SetTarget(nullptr); + WipeHateList(); + + return; + } + } + + //#pragma endregion + + //#pragma region ALT COMBAT (ACQUIRE TARGET) + + else if (bo_alt_combat && m_alt_combat_hate_timer.Check()) { // Find a mob from hate list to target + + // Group roles can be expounded upon in the future + Group* bot_group = this->GetGroup(); //Mitch + auto assist_mob = entity_list.GetMob(bot_group->GetMainAssistName()); + bool find_target = true; + + if (assist_mob) { + + if (assist_mob->GetTarget()) { + + if (assist_mob != this) { + + SetTarget(assist_mob->GetTarget()); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + // This artificially inflates pet's target aggro..but, less expensive than checking hate each AI process + GetPet()->AddToHateList(assist_mob->GetTarget(), 1); + GetPet()->SetTarget(assist_mob->GetTarget()); + } + } + + find_target = false; + } + else if (assist_mob != this) { + + SetTarget(nullptr); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + + find_target = false; + } + } + + if (find_target) { + + if (IsRooted()) { + SetTarget(hate_list.GetClosestEntOnHateList(this, true)); + } + else { + + // This will keep bots on target for now..but, future updates will allow for rooting/stunning + SetTarget(hate_list.GetEscapingEntOnHateList(leash_owner, leash_distance)); + if (!GetTarget()) { + SetTarget(hate_list.GetEntWithMostHateOnList(this, nullptr, true)); + } + } + } + } + + //#pragma endregion + + //#pragma region DEFAULT (ACQUIRE TARGET) + + else { + + // Default behavior doesn't have a means of acquiring a target from the bot's hate list.. + // ..that action occurs through commands or out-of-combat checks + // (Use current target, if already in combat) + } + + //#pragma endregion + + //#pragma region VERIFY TARGET AND STANCE + + Mob* tar = GetTarget(); // We should have a target..if not, we're awaiting new orders + if (!tar || PASSIVE) { + + SetTarget(nullptr); + WipeHateList(); + SetAttackFlag(false); + SetAttackingFlag(false); + if (PULLING_BOT) { + + // 'Flags' should only be set on the bot that is pulling + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + } + + if (GetArchetype() == ARCHETYPE_CASTER) { + BotMeditate(true); + } + + return; + } + + //#pragma endregion + + //#pragma region ATTACKING FLAG (HATE VALIDATION) + + if (GetAttackingFlag() && tar->CheckAggro(this)) { + SetAttackingFlag(false); + } + + //#pragma endregion + + float tar_distance = DistanceSquared(m_Position, tar->GetPosition()); + + //#pragma region TARGET VALIDATION + + // DOUBLE-CHECK THIS CRITERIA + + // Verify that our target has attackable criteria + if (HOLDING || + !tar->IsNPC() || + tar->IsMezzed() || + lo_distance > leash_distance || + tar_distance > leash_distance || + (!GetAttackingFlag() && !CheckLosFN(tar) && !leash_owner->CheckLosFN(tar)) || // This is suppose to keep bots from attacking things behind walls + !IsAttackAllowed(tar) || + (bo_alt_combat && + (!GetAttackingFlag() && NOT_PULLING_BOT && !leash_owner->AutoAttackEnabled() && !tar->GetHateAmount(this) && !tar->GetHateAmount(leash_owner)) + ) + ) + { + // Normally, we wouldn't want to do this without class checks..but, too many issues can arise if we let enchanter animation pets run rampant + if (HasPet()) { + + GetPet()->RemoveFromHateList(tar); + GetPet()->SetTarget(nullptr); + } + + RemoveFromHateList(tar); + SetTarget(nullptr); + + SetAttackFlag(false); + SetAttackingFlag(false); + if (PULLING_BOT) { + + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + } + + if (IsMoving()) { + StopMoving(); + } + + return; + } + + //#pragma endregion + + // This causes conflicts with default pet handler (bounces between targets) + if (NOT_PULLING_BOT && HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + // We don't add to hate list here because it's assumed to already be on the list + GetPet()->SetTarget(tar); + } + + if (DivineAura()) { + return; + } + + if (!(m_PlayerState & static_cast(PlayerState::Aggressive))) { + SendAddPlayerState(PlayerState::Aggressive); + } + + //#pragma region PULLING FLAG (ACTIONABLE RANGE) + + if (GetPullingFlag()) { + + constexpr size_t PULL_AGGRO = 5225; // spells[5225]: 'Throw Stone' - 0 cast time + + if (tar_distance <= (spells[PULL_AGGRO].range * spells[PULL_AGGRO].range)) { + + StopMoving(); + CastSpell(PULL_AGGRO, tar->GetID()); + return; + } + } + + //#pragma endregion + + //#pragma region COMBAT RANGE CALCS + + bool atCombatRange = false; + + const auto* p_item = GetBotItem(EQ::invslot::slotPrimary); + const auto* s_item = GetBotItem(EQ::invslot::slotSecondary); + + bool behind_mob = false; + bool backstab_weapon = false; + if (GetClass() == ROGUE) { + + behind_mob = BehindMob(tar, GetX(), GetY()); // Can be separated for other future use + backstab_weapon = p_item && p_item->GetItemBackstabDamage(); + } + + // Calculate melee distances + float melee_distance_max = 0.0f; + float melee_distance = 0.0f; + { + float size_mod = GetSize(); + float other_size_mod = tar->GetSize(); + + if (GetRace() == RT_DRAGON || GetRace() == RT_WURM || GetRace() == RT_DRAGON_7) { // For races with a fixed size + size_mod = 60.0f; + } + else if (size_mod < 6.0f) { + size_mod = 8.0f; + } + + if (tar->GetRace() == RT_DRAGON || tar->GetRace() == RT_WURM || tar->GetRace() == RT_DRAGON_7) { // For races with a fixed size + other_size_mod = 60.0f; + } + else if (other_size_mod < 6.0f) { + other_size_mod = 8.0f; + } + + if (other_size_mod > size_mod) { + size_mod = other_size_mod; + } + + if (size_mod > 29.0f) { + size_mod *= size_mod; + } + else if (size_mod > 19.0f) { + size_mod *= (size_mod * 2.0f); + } + else { + size_mod *= (size_mod * 4.0f); + } + + // Prevention of ridiculously sized hit boxes + if (size_mod > 10000.0f) { + size_mod = (size_mod / 7.0f); + } + + melee_distance_max = size_mod; + + switch (GetClass()) { + case WARRIOR: + case PALADIN: + case SHADOWKNIGHT: + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + melee_distance = melee_distance_max * 0.45f; + } + else if ((s_item && s_item->GetItem()->IsTypeShield()) || (!p_item && !s_item)) { + melee_distance = melee_distance_max * 0.35f; + } + else { + melee_distance = melee_distance_max * 0.40f; + } + + break; + case NECROMANCER: + case WIZARD: + case MAGICIAN: + case ENCHANTER: + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + melee_distance = melee_distance_max * 0.95f; + } + else { + melee_distance = melee_distance_max * 0.75f; + } + + break; + case ROGUE: + if (behind_mob && backstab_weapon) { + if (p_item->GetItem()->IsType2HWeapon()) { // 'p_item' tested in 'backstab_weapon' check above + melee_distance = melee_distance_max * 0.30f; + } + else { + melee_distance = melee_distance_max * 0.25f; + } + + break; + } + // Fall-through + default: + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + melee_distance = melee_distance_max * 0.70f; + } + else { + melee_distance = melee_distance_max * 0.50f; + } + + break; + } + } + float melee_distance_min = melee_distance / 2.0f; + + // Calculate caster distances + float caster_distance_max = 0.0f; + float caster_distance_min = 0.0f; + float caster_distance = 0.0f; + { + if (GetLevel() >= GetStopMeleeLevel() && GetClass() >= WARRIOR && GetClass() <= BERSERKER) { + caster_distance_max = MAX_CASTER_DISTANCE[(GetClass() - 1)]; + } + + if (caster_distance_max) { + + caster_distance_min = melee_distance_max; + if (caster_distance_max <= caster_distance_min) { + caster_distance_max = caster_distance_min * 1.25f; + } + + caster_distance = ((caster_distance_max + caster_distance_min) / 2); + } + } + + bool atArcheryRange = IsArcheryRange(tar); + + if (GetRangerAutoWeaponSelect()) { + + bool changeWeapons = false; + + if (atArcheryRange && !IsBotArcher()) { + + SetBotArcher(true); + changeWeapons = true; + } + else if (!atArcheryRange && IsBotArcher()) { + + SetBotArcher(false); + changeWeapons = true; + } + + if (changeWeapons) { + ChangeBotArcherWeapons(IsBotArcher()); + } + } + + if (IsBotArcher() && atArcheryRange) { + atCombatRange = true; + } + else if (caster_distance_max && tar_distance <= caster_distance_max) { + atCombatRange = true; + } + else if (tar_distance <= melee_distance) { + atCombatRange = true; + } + + //#pragma endregion + + //#pragma region ENGAGED AT COMBAT RANGE + + // We can fight + if (atCombatRange) { + + //if (IsMoving() || GetCombatJitterFlag()) { // StopMoving() needs to be called so that the jitter timer can be reset + if (IsMoving()) { + + // Since we're using a pseudo-shadowstep for jitter, disregard the combat jitter flag + //if (!GetCombatJitterFlag()) { + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); + //} + + return; + } + + // Combat 'jitter' code + // Note: Combat Jitter is disabled until a working movement solution can be found + if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == BARD)) { + + if (!IsRooted()) { + + if (HasTargetReflection()) { + + if (!tar->IsFeared() && !tar->IsStunned()) { + + if (GetClass() == ROGUE) { + + if (m_evade_timer.Check(false)) { // Attempt to evade + + int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000; + if (timer_duration < 0) { + timer_duration = 0; + } + + m_evade_timer.Start(timer_duration); + if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide)) { + RogueEvade(tar); + } + + return; + } + } + + //if (tar->IsRooted()) { // Move caster/rogue back from rooted mob - out of combat range, if necessary + + // if (GetArchetype() == ARCHETYPE_CASTER || GetClass() == ROGUE) { + + // if (tar_distance <= melee_distance_max) { + + // if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionBehindMeFacingTarget(tar, Goal.x, Goal.y, Goal.z)) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + //} + } + } + //else { + + // if (caster_distance_min && tar_distance < caster_distance_min && !tar->IsFeared()) { // Caster back-off adjustment + + // if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionBehindMeFacingTarget(tar, Goal.x, Goal.y, Goal.z)) { + + // if (DistanceSquared(Goal, tar->GetPosition()) <= caster_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + // else if (tar_distance < melee_distance_min) { // Melee back-off adjustment + + // if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionBehindMeFacingTarget(tar, Goal.x, Goal.y, Goal.z)) { + + // if (DistanceSquared(Goal, tar->GetPosition()) <= melee_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + // else if (backstab_weapon && !behind_mob) { // Move the rogue to behind the mob + + // if (PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionOnArcBehindTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance)) { + + // float distance_squared = DistanceSquared(Goal, tar->GetPosition()); + // if (/*distance_squared >= melee_distance_min && */distance_squared <= melee_distance_max) { + + // Teleport(Goal); + // //RunTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + // else if (m_combat_jitter_timer.Check()) { + + // if (!caster_distance && PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { + // //if (!caster_distance && PlotPositionOnArcInFrontOfTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance)) { + + // float distance_squared = DistanceSquared(Goal, tar->GetPosition()); + // if (/*distance_squared >= melee_distance_min && */distance_squared <= melee_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // else if (caster_distance && PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { + // //else if (caster_distance && PlotPositionOnArcInFrontOfTarget(tar, Goal.x, Goal.y, Goal.z, caster_distance)) { + + // float distance_squared = DistanceSquared(Goal, tar->GetPosition()); + // if (/*distance_squared >= caster_distance_min && */distance_squared <= caster_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + + // if (!IsFacingMob(tar)) { + + // FaceTarget(tar); + // return; + // } + //} + } + else { + + if (!IsSitting() && !IsFacingMob(tar)) { + + FaceTarget(tar); + return; + } + } + } + + if (!IsBotNonSpellFighter() && AI_EngagedCastCheck()) { + return; + } + + // Up to this point, GetTarget() has been safe to dereference since the initial + // TEST_COMBATANTS() call. Due to the chance of the target dying and our pointer + // being nullified, we need to test it before dereferencing to avoid crashes + + if (IsBotArcher() && ranged_timer.Check(false)) { // Can shoot mezzed, stunned and dead!? + + TEST_COMBATANTS(); + if (GetTarget()->GetHPRatio() <= 99.0f) { + BotRangedAttack(tar); + } + } + else if (!IsBotArcher() && GetLevel() < GetStopMeleeLevel()) { + + // We can't fight if we don't have a target, are stun/mezzed or dead.. + // Stop attacking if the target is enraged + TEST_COMBATANTS(); + if (tar->IsEnraged() && !BehindMob(tar, GetX(), GetY())) { + return; + } + + // First, special attack per class (kick, backstab etc..) + TEST_COMBATANTS(); + DoClassAttacks(tar); + + TEST_COMBATANTS(); + if (attack_timer.Check()) { // Process primary weapon attacks + + Attack(tar, EQ::invslot::slotPrimary); + + TEST_COMBATANTS(); + TriggerDefensiveProcs(tar, EQ::invslot::slotPrimary, false); + + TEST_COMBATANTS(); + TryWeaponProc(p_item, tar, EQ::invslot::slotPrimary); + + // bool tripleSuccess = false; + + TEST_COMBATANTS(); + if (CanThisClassDoubleAttack()) { + + if (CheckBotDoubleAttack()) { + Attack(tar, EQ::invslot::slotPrimary, true); + } + + TEST_COMBATANTS(); + if (GetSpecialAbility(SPECATK_TRIPLE) && CheckBotDoubleAttack(true)) { + // tripleSuccess = true; + Attack(tar, EQ::invslot::slotPrimary, true); + } + + TEST_COMBATANTS(); + // quad attack, does this belong here?? + if (GetSpecialAbility(SPECATK_QUAD) && CheckBotDoubleAttack(true)) { + Attack(tar, EQ::invslot::slotPrimary, true); + } + } + + TEST_COMBATANTS(); + // Live AA - Flurry, Rapid Strikes ect (Flurry does not require Triple Attack). + int32 flurrychance = (aabonuses.FlurryChance + spellbonuses.FlurryChance + itembonuses.FlurryChance); + if (flurrychance) { + + if (zone->random.Int(0, 100) < flurrychance) { + + MessageString(Chat::NPCFlurry, YOU_FLURRY); + Attack(tar, EQ::invslot::slotPrimary, false); + + TEST_COMBATANTS(); + Attack(tar, EQ::invslot::slotPrimary, false); + } + } + + TEST_COMBATANTS(); + int32 ExtraAttackChanceBonus = (spellbonuses.ExtraAttackChance + itembonuses.ExtraAttackChance + aabonuses.ExtraAttackChance); + if (ExtraAttackChanceBonus) { + + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + + if (zone->random.Int(0, 100) < ExtraAttackChanceBonus) { + Attack(tar, EQ::invslot::slotPrimary, false); + } + } + } + } + + TEST_COMBATANTS(); + if (attack_dw_timer.Check() && CanThisClassDualWield()) { // Process secondary weapon attacks + + const EQ::ItemData* s_itemdata = nullptr; + // Can only dual wield without a weapon if you're a monk + if (s_item || (GetClass() == MONK)) { + + if (s_item) { + s_itemdata = s_item->GetItem(); + } + + int weapon_type = 0; // No weapon type. + bool use_fist = true; + if (s_itemdata) { + + weapon_type = s_itemdata->ItemType; + use_fist = false; + } + + if (use_fist || !s_itemdata->IsType2HWeapon()) { + + float DualWieldProbability = 0.0f; + + int32 Ambidexterity = (aabonuses.Ambidexterity + spellbonuses.Ambidexterity + itembonuses.Ambidexterity); + DualWieldProbability = ((GetSkill(EQ::skills::SkillDualWield) + GetLevel() + Ambidexterity) / 400.0f); // 78.0 max + + int32 DWBonus = (spellbonuses.DualWieldChance + itembonuses.DualWieldChance); + DualWieldProbability += (DualWieldProbability * float(DWBonus) / 100.0f); + + float random = zone->random.Real(0, 1); + if (random < DualWieldProbability) { // Max 78% of DW + + Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand + + TEST_COMBATANTS(); + TryWeaponProc(s_item, tar, EQ::invslot::slotSecondary); + + TEST_COMBATANTS(); + if (CanThisClassDoubleAttack() && CheckBotDoubleAttack()) { + + if (tar->GetHP() > -10) { + Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand + } + } + } + } + } + } + } + + if (GetAppearance() == eaDead) { + return; + } + } + + //#pragma endregion + + //#pragma region ENGAGED NOT AT COMBAT RANGE + + else { // To far away to fight (GetTarget() validity can be iffy below this point - including outer scopes) + + // This code actually gets processed when we are too far away from target and have not engaged yet, too + if (/*!GetCombatJitterFlag() && */AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == BARD)) { // Pursue processing + + if (GetTarget() && !IsRooted()) { + + LogAI("Pursuing [{}] while engaged", GetTarget()->GetCleanName()); + Goal = GetTarget()->GetPosition(); + if (DistanceSquared(m_Position, Goal) <= leash_distance) { + RunTo(Goal.x, Goal.y, Goal.z); + } + else { + + WipeHateList(); + SetTarget(nullptr); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + } + + return; + } + else { + + if (IsMoving()) { + StopMoving(); + } + return; + } + } + + if (GetTarget() && GetTarget()->IsFeared() && !spellend_timer.Enabled() && AI_think_timer->Check()) { + + if (!IsFacingMob(GetTarget())) { + FaceTarget(GetTarget()); + } + + // This is a mob that is fleeing either because it has been feared or is low on hitpoints + AI_PursueCastCheck(); // This appears to always return true..can't trust for success/fail + + return; + } + } // End not in combat range + +//#pragma endregion + + if (!IsMoving() && !spellend_timer.Enabled()) { // This may actually need work... + + if (GetTarget() && AI_EngagedCastCheck()) { + BotMeditate(false); + } + else if (GetArchetype() == ARCHETYPE_CASTER) { + BotMeditate(true); + } + + return; + } + } + else { // Out-of-combat behavior + + SetAttackFlag(false); + SetAttackingFlag(false); + if (!bot_owner->GetBotPulling()) { + + SetPullingFlag(false); + SetReturningFlag(false); + } + + //#pragma region AUTO DEFEND + + // This is as close as I could get without modifying the aggro mechanics and making it an expensive process... + // 'class Client' doesn't make use of hate_list... + if (RuleB(Bots, AllowOwnerOptionAutoDefend) && bot_owner->GetBotOption(Client::booAutoDefend)) { + + if (!m_auto_defend_timer.Enabled()) { + + m_auto_defend_timer.Start(zone->random.Int(250, 1250)); // random timer to simulate 'awareness' (cuts down on scanning overhead) + return; + } + + if (m_auto_defend_timer.Check() && bot_owner->GetAggroCount()) { + + if (NOT_HOLDING && NOT_PASSIVE) { + + auto xhaters = bot_owner->GetXTargetAutoMgr(); + if (xhaters && !xhaters->empty()) { + + for (auto hater_iter : xhaters->get_list()) { + + if (!hater_iter.spawn_id) { + continue; + } + + if (bot_owner->GetBotPulling() && bot_owner->GetTarget() && hater_iter.spawn_id == bot_owner->GetTarget()->GetID()) { + continue; + } + + auto hater = entity_list.GetMob(hater_iter.spawn_id); + if (hater && !hater->IsMezzed() && DistanceSquared(hater->GetPosition(), bot_owner->GetPosition()) <= leash_distance) { + + // This is roughly equivilent to npc attacking a client pet owner + AddToHateList(hater, 1); + SetTarget(hater); + SetAttackingFlag(); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->AddToHateList(hater, 1); + GetPet()->SetTarget(hater); + } + + m_auto_defend_timer.Disable(); + + return; + } + } + } + } + } + } + + //#pragma endregion + + SetTarget(nullptr); + + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + + if (m_PlayerState & static_cast(PlayerState::Aggressive)) { + SendRemovePlayerState(PlayerState::Aggressive); + } + + //#pragma region OK TO IDLE + + // Ok to idle + if ((NOT_GUARDING && fm_distance <= GetFollowDistance()) || (GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) <= GetFollowDistance())) { + + if (!IsMoving() && AI_think_timer->Check() && !spellend_timer.Enabled()) { + + if (NOT_PASSIVE) { + + if (!AI_IdleCastCheck() && !IsCasting() && GetClass() != BARD) { + BotMeditate(true); + } + } + else { + + if (GetClass() != BARD) { + BotMeditate(true); + } + } + + return; + } + } + + // Non-engaged movement checks + if (AI_movement_timer->Check() && (!IsCasting() || GetClass() == BARD)) { + + if (GUARDING) { + Goal = GetGuardPoint(); + } + else { + Goal = follow_mob->GetPosition(); + } + float destination_distance = DistanceSquared(GetPosition(), Goal); + + if ((!bot_owner->GetBotPulling() || PULLING_BOT) && (destination_distance > GetFollowDistance())) { + + if (!IsRooted()) { + + if (rest_timer.Enabled()) { + rest_timer.Disable(); + } + + bool running = true; + + if (destination_distance < GetFollowDistance() + BOT_FOLLOW_DISTANCE_WALK) { + running = false; + } + + if (running) { + RunTo(Goal.x, Goal.y, Goal.z); + } + else { + WalkTo(Goal.x, Goal.y, Goal.z); + } + + return; + } + } + else { + + if (IsMoving()) { + + StopMoving(); + return; + } + } + } + + // Basically, bard bots get a chance to cast idle spells while moving + if (GetClass() == BARD && IsMoving() && NOT_PASSIVE) { + + if (!spellend_timer.Enabled() && AI_think_timer->Check()) { + + AI_IdleCastCheck(); + return; + } + } + + //#pragma endregion + + } + +#undef TEST_COMBATANTS +#undef PULLING_BOT +#undef NOT_PULLING_BOT +#undef GUARDING +#undef NOT_GUARDING +#undef HOLDING +#undef NOT_HOLDING +#undef PASSIVE +#undef NOT_PASSIVE +} + +// AI Processing for a Bot object's pet if Bot is a member of a raid +void Bot::PetAIProcess_Raid() { + if (!HasPet() || !GetPet() || !GetPet()->IsNPC()) + return; + + Mob* BotOwner = this->GetBotOwner(); + NPC* botPet = this->GetPet()->CastToNPC(); + if (!botPet->GetOwner() || !botPet->GetID() || !botPet->GetOwnerID()) { + Kill(); + return; + } + + if (!botPet->IsAIControlled() || botPet->GetAttackTimer().Check(false) || botPet->IsCasting() || !botPet->GetOwner()->IsBot()) + return; + + if (IsEngaged()) { + if (botPet->IsRooted()) + botPet->SetTarget(hate_list.GetClosestEntOnHateList(botPet)); + else + botPet->SetTarget(hate_list.GetEntWithMostHateOnList(botPet)); + + // Let's check if we have a los with our target. + // If we don't, our hate_list is wiped. + // It causes some cpu stress but without it, it was causing the bot/pet to aggro behind wall, floor etc... + if (!botPet->CheckLosFN(botPet->GetTarget()) || botPet->GetTarget()->IsMezzed() || !botPet->IsAttackAllowed(GetTarget())) { + botPet->WipeHateList(); + botPet->SetTarget(botPet->GetOwner()); + return; + } + + botPet->FaceTarget(botPet->GetTarget()); + bool is_combat_range = botPet->CombatRange(botPet->GetTarget()); + // Ok, we're engaged, each class type has a special AI + // Only melee class will go to melee. Casters and healers will stay behind, following the leader by default. + // I should probably make the casters staying in place so they can cast.. + + // Ok, we 're a melee or any other class lvl<12. Yes, because after it becomes hard to go in melee for casters.. even for bots.. + if (is_combat_range) { + botPet->GetAIMovementTimer()->Check(); + if (botPet->IsMoving()) { + botPet->SetHeading(botPet->GetTarget()->GetHeading()); + if (moved) { + moved = false; + botPet->SetRunAnimSpeed(0); + } + } + + if (!botPet->IsMoving()) { + float newX = 0; + float newY = 0; + float newZ = 0; + bool petHasAggro = false; + if (botPet->GetTarget() && botPet->GetTarget()->GetHateTop() && botPet->GetTarget()->GetHateTop() == botPet) + petHasAggro = true; + + if (botPet->GetClass() == ROGUE && !petHasAggro && !botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY())) { + // Move the rogue to behind the mob + if (botPet->PlotPositionAroundTarget(botPet->GetTarget(), newX, newY, newZ)) { + botPet->RunTo(newX, newY, newZ); + return; + } + } + else if (GetTarget() == botPet->GetTarget() && !petHasAggro && !botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY())) { + // If the bot owner and the bot are fighting the same mob, then move the pet to the rear arc of the mob + if (botPet->PlotPositionAroundTarget(botPet->GetTarget(), newX, newY, newZ)) { + botPet->RunTo(newX, newY, newZ); + return; + } + } + else if (DistanceSquaredNoZ(botPet->GetPosition(), botPet->GetTarget()->GetPosition()) < botPet->GetTarget()->GetSize()) { + // Let's try to adjust our melee range so we don't appear to be bunched up + bool isBehindMob = false; + bool moveBehindMob = false; + if (botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY())) + isBehindMob = true; + + if (!isBehindMob && !petHasAggro) + moveBehindMob = true; + + if (botPet->PlotPositionAroundTarget(botPet->GetTarget(), newX, newY, newZ, moveBehindMob)) { + botPet->RunTo(newX, newY, newZ); + return; + } + } + } + + // we can't fight if we don't have a target, are stun/mezzed or dead.. + if (botPet->GetTarget() && !botPet->IsStunned() && !botPet->IsMezzed() && (botPet->GetAppearance() != eaDead)) { + // check the delay on the attack + if (botPet->GetAttackTimer().Check()) { + // Stop attacking while we are on a front arc and the target is enraged + if (!botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY()) && botPet->GetTarget()->IsEnraged()) + return; + + if (botPet->Attack(GetTarget(), EQ::invslot::slotPrimary)) // try the main hand + if (botPet->GetTarget()) { + // We're a pet so we re able to dual attack + int32 RandRoll = zone->random.Int(0, 99); + if (botPet->CanThisClassDoubleAttack() && (RandRoll < (botPet->GetLevel() + NPCDualAttackModifier))) { + if (botPet->Attack(botPet->GetTarget(), EQ::invslot::slotPrimary)) {} + } + } + + if (botPet->GetOwner()->IsBot()) { + int aa_chance = 0; + int aa_skill = 0; + // Magician AA + aa_skill += botPet->GetOwner()->GetAA(aaElementalAlacrity); + // Necromancer AA + aa_skill += botPet->GetOwner()->GetAA(aaQuickeningofDeath); + // Beastlord AA + aa_skill += botPet->GetOwner()->GetAA(aaWardersAlacrity); + if (aa_skill >= 1) + aa_chance += ((aa_skill > 5 ? 5 : aa_skill) * 4); + + if (aa_skill >= 6) + aa_chance += ((aa_skill - 5 > 3 ? 3 : aa_skill - 5) * 7); + + if (aa_skill >= 9) + aa_chance += ((aa_skill - 8 > 3 ? 3 : aa_skill - 8) * 3); + + if (aa_skill >= 12) + aa_chance += ((aa_skill - 11) * 1); + + + //aa_chance += botPet->GetOwner()->GetAA(aaCompanionsAlacrity) * 3; + + if (zone->random.Int(1, 100) < aa_chance) + Flurry(nullptr); + } + + // Ok now, let's check pet's offhand. + if (botPet->GetAttackDWTimer().Check() && botPet->GetOwnerID() && botPet->GetOwner() && ((botPet->GetOwner()->GetClass() == MAGICIAN) || (botPet->GetOwner()->GetClass() == NECROMANCER) || (botPet->GetOwner()->GetClass() == SHADOWKNIGHT) || (botPet->GetOwner()->GetClass() == BEASTLORD))) { + if (botPet->GetOwner()->GetLevel() >= 24) { + float DualWieldProbability = ((botPet->GetSkill(EQ::skills::SkillDualWield) + botPet->GetLevel()) / 400.0f); + DualWieldProbability -= zone->random.Real(0, 1); + if (DualWieldProbability < 0) { + botPet->Attack(botPet->GetTarget(), EQ::invslot::slotSecondary); + if (botPet->CanThisClassDoubleAttack()) { + int32 RandRoll = zone->random.Int(0, 99); + if (RandRoll < (botPet->GetLevel() + 20)) + botPet->Attack(botPet->GetTarget(), EQ::invslot::slotSecondary); + } + } + } + } + if (!botPet->GetOwner()) + return; + + // Special attack + botPet->DoClassAttacks(botPet->GetTarget()); + } + // See if the pet can cast any spell + botPet->AI_EngagedCastCheck(); + } + } + else { + // Now, if we cannot reach our target + if (!botPet->HateSummon()) { + if (botPet->GetTarget() && botPet->AI_PursueCastCheck()) {} + else if (botPet->GetTarget() && botPet->GetAIMovementTimer()->Check()) { + botPet->SetRunAnimSpeed(0); + if (!botPet->IsRooted()) { + LogAI("Pursuing [{}] while engaged", botPet->GetTarget()->GetCleanName()); + botPet->RunTo(botPet->GetTarget()->GetX(), botPet->GetTarget()->GetY(), botPet->GetTarget()->GetZ()); + return; + } + else { + botPet->SetHeading(botPet->GetTarget()->GetHeading()); + if (moved) { + moved = false; + StopNavigation(); + botPet->StopNavigation(); + } + } + } + } + } + } + else { + // Ok if we're not engaged, what's happening.. + if (botPet->GetTarget() != botPet->GetOwner()) + botPet->SetTarget(botPet->GetOwner()); + + if (!IsMoving()) + botPet->AI_IdleCastCheck(); + + if (botPet->GetAIMovementTimer()->Check()) { + switch (pStandingPetOrder) { + case SPO_Follow: { + float dist = DistanceSquared(botPet->GetPosition(), botPet->GetTarget()->GetPosition()); + botPet->SetRunAnimSpeed(0); + if (dist > 184) { + botPet->RunTo(botPet->GetTarget()->GetX(), botPet->GetTarget()->GetY(), botPet->GetTarget()->GetZ()); + return; + } + else { + botPet->SetHeading(botPet->GetTarget()->GetHeading()); + if (moved) { + moved = false; + StopNavigation(); + botPet->StopNavigation(); + } + } + break; + } + case SPO_Sit: + botPet->SetAppearance(eaSitting); + break; + case SPO_Guard: + botPet->NextGuardPosition(); + break; + } + } + } +} + +std::vector Raid::GetRaidGroupMembers(uint32 gid) +{ + std::vector raid_group_members; + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) + { + if (members[i].member && members[i].GroupNumber == gid) + { + raid_group_members.emplace_back(members[i]); + } + } + return raid_group_members; +} + + +#endif diff --git a/bin/zone/bot_raid.h b/zone/bot_raid.h similarity index 93% rename from bin/zone/bot_raid.h rename to zone/bot_raid.h index ab012b381..66d55bab8 100644 --- a/bin/zone/bot_raid.h +++ b/zone/bot_raid.h @@ -16,8 +16,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#ifndef BOT_H -#define BOT_H +#ifndef BOT_RAID_H +#define BOT_RAID_H #ifdef BOTS @@ -38,9 +38,10 @@ #include - extern WorldServer worldserver; +//void Bot::PetAIProcess_Raid(); + #endif // BOTS -#endif // BOT_H +#endif // BOT_RAID_H diff --git a/zone/botspellsai.cpp b/zone/botspellsai.cpp index e03c996fd..5dd7569de 100644 --- a/zone/botspellsai.cpp +++ b/zone/botspellsai.cpp @@ -31,6 +31,13 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { + // Bot AI + Raid* raid = entity_list.GetRaidByBot(this); + if (raid) { + AI_CastSpell_Raid(); + return true; + } + if (!tar) { return false; } diff --git a/zone/raids.h b/zone/raids.h index 9961be2e8..432140982 100644 --- a/zone/raids.h +++ b/zone/raids.h @@ -123,6 +123,7 @@ public: bool IsGroupLeader(const char *who); bool IsRaidMember(const char *name); void UpdateLevel(const char *name, int newLevel); + std::vector GetRaidGroupMembers(uint32 gid); uint32 GetFreeGroup(); uint8 GroupCount(uint32 gid);