mirror of
https://github.com/EQEmu/Server.git
synced 2026-05-17 07:18:37 +00:00
1531 lines
43 KiB
C++
1531 lines
43 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
|
|
*/
|
|
|
|
#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<RaidMember> 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<uint32>(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<uint32>(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<RaidMember> Raid::GetRaidGroupMembers(uint32 gid)
|
|
{
|
|
std::vector<RaidMember> 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
|