From 12204dd9278c98640e1150621221606ba30bab4f Mon Sep 17 00:00:00 2001 From: Uleat Date: Sat, 5 Oct 2019 18:17:23 -0400 Subject: [PATCH] Updated Bot AI to make use of neglected commands/features --- zone/attack.cpp | 8 +- zone/bot.cpp | 1392 ++++++++++++++++++++++++++++------------- zone/bot.h | 59 +- zone/bot_command.cpp | 378 ++++++++--- zone/bot_command.h | 1 + zone/bot_database.cpp | 15 +- zone/client.cpp | 4 + zone/client.h | 9 +- zone/mob.cpp | 35 +- zone/mob.h | 10 +- 10 files changed, 1377 insertions(+), 534 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index b53b247b7..17c23971a 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -2693,18 +2693,22 @@ void Mob::AddToHateList(Mob* other, uint32 hate /*= 0*/, int32 damage /*= 0*/, b #ifdef BOTS // if other is a bot, add the bots client to the hate list while (other->IsBot()) { + auto other_ = other->CastToBot(); - if (!other_ || !other_->GetBotOwner()) + if (!other_ || !other_->GetBotOwner()) { break; + } auto owner_ = other_->GetBotOwner()->CastToClient(); - if (!owner_ || owner_->IsDead() || !owner_->InZone()) // added isdead and inzone checks to avoid issues in AddAutoXTarget(...) below + if (!owner_ || owner_->IsDead() || !owner_->InZone()) { // added isdead and inzone checks to avoid issues in AddAutoXTarget(...) below break; + } if (owner_->GetFeigned()) { AddFeignMemory(owner_); } else if (!hate_list.IsEntOnHateList(owner_)) { + hate_list.AddEntToHateList(owner_, 0, 0, false, true); owner_->AddAutoXTarget(this); // this was being called on dead/out-of-zone clients } diff --git a/zone/bot.cpp b/zone/bot.cpp index 238fb06da..bba29fb38 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -81,6 +81,19 @@ Bot::Bot(NPCType *npcTypeData, Client* botOwner) : NPC(npcTypeData, nullptr, glm SetAltOutOfCombatBehavior(GetClass() == BARD); // will need to be updated if more classes make use of this flag SetShowHelm(true); SetPauseAI(false); + + m_alt_combat_hate_timer.Start(250); + //m_combat_jitter_timer.Disable(); + //SetCombatJitterFlag(false); + SetGuardFlag(false); + SetHoldFlag(false); + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + m_previous_pet_order = SPO_Guard; + rest_timer.Disable(); ping_timer.Disable(); SetFollowDistance(BOT_FOLLOW_DISTANCE_DEFAULT); @@ -166,6 +179,18 @@ Bot::Bot(uint32 botID, uint32 botOwnerCharacterID, uint32 botSpellsID, double to SetTaunting((GetClass() == WARRIOR || GetClass() == PALADIN || GetClass() == SHADOWKNIGHT) && (GetBotStance() == EQEmu::constants::stanceAggressive)); SetPauseAI(false); + m_alt_combat_hate_timer.Start(250); + //m_combat_jitter_timer.Disable(); + //SetCombatJitterFlag(false); + SetGuardFlag(false); + SetHoldFlag(false); + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + m_previous_pet_order = SPO_Guard; + rest_timer.Disable(); ping_timer.Disable(); SetFollowDistance(BOT_FOLLOW_DISTANCE_DEFAULT); @@ -195,8 +220,9 @@ Bot::Bot(uint32 botID, uint32 botOwnerCharacterID, uint32 botSpellsID, double to for (int i = 0; i < MaxTimer; i++) timers[i] = 0; - if (GetClass() == ROGUE) - evade_timer.Start(); + if (GetClass() == ROGUE) { + m_evade_timer.Start(); + } m_CastingRoles.GroupHealer = false; m_CastingRoles.GroupSlower = false; @@ -1969,79 +1995,103 @@ bool Bot::DeletePet() return true; } -bool Bot::Process() { - if(IsStunned() && stunned_timer.Check()) +bool Bot::Process() +{ + if (IsStunned() && stunned_timer.Check()) { Mob::UnStun(); + } - if(!GetBotOwner()) + if (!GetBotOwner()) { return false; + } if (GetDepop()) { + _botOwner = 0; _botOwnerCharacterID = 0; _previousTarget = 0; + return false; } SpellProcess(); if(tic_timer.Check()) { - //6 seconds, or whatever the rule is set to has passed, send this position to everyone to avoid ghosting + + // 6 seconds, or whatever the rule is set to has passed, send this position to everyone to avoid ghosting if(!IsMoving() && !IsEngaged()) { + if(IsSitting()) { - if(!rest_timer.Enabled()) + + if (!rest_timer.Enabled()) { rest_timer.Start(RuleI(Character, RestRegenTimeToActivate) * 1000); + } } } BuffProcess(); CalcRestState(); - if(currently_fleeing) + + if (currently_fleeing) { ProcessFlee(); + } - if(GetHP() < GetMaxHP()) + if (GetHP() < GetMaxHP()) { SetHP(GetHP() + CalcHPRegen() + RestRegenHP); + } - if(GetMana() < GetMaxMana()) + if (GetMana() < GetMaxMana()) { SetMana(GetMana() + CalcManaRegen() + RestRegenMana); + } CalcATK(); - if(GetEndurance() < GetMaxEndurance()) + + if (GetEndurance() < GetMaxEndurance()) { SetEndurance(GetEndurance() + CalcEnduranceRegen() + RestRegenEndurance); + } } if (send_hp_update_timer.Check(false)) { + SendHPUpdate(); - if(HasPet()) + if (HasPet()) { GetPet()->SendHPUpdate(); + } // hack fix until percentage changes can be implemented auto g = GetGroup(); if (g) { + g->SendManaPacketFrom(this); g->SendEndurancePacketFrom(this); } } - if(GetAppearance() == eaDead && GetHP() > 0) + if (GetAppearance() == eaDead && GetHP() > 0) { SetAppearance(eaStanding); + } if (IsMoving()) { ping_timer.Disable(); } else { - if (!ping_timer.Enabled()) - ping_timer.Start(BOT_KEEP_ALIVE_INTERVAL); - if (ping_timer.Check()) + if (!ping_timer.Enabled()) { + ping_timer.Start(BOT_KEEP_ALIVE_INTERVAL); + } + + if (ping_timer.Check()) { SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0); + } } - if (IsStunned() || IsMezzed()) + if (IsStunned() || IsMezzed()) { return true; + } // Bot AI AI_Process(); + return true; } @@ -2241,32 +2291,53 @@ void Bot::SetStopMeleeLevel(uint8 level) { } void Bot::SetGuardMode() { - WipeHateList(); - SetTarget(nullptr); - SetFollowID(GetID()); + StopMoving(); m_GuardPoint = GetPosition(); + SetGuardFlag(); - if (HasPet()) { - GetPet()->WipeHateList(); - GetPet()->SetTarget(nullptr); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { GetPet()->StopMoving(); } } -// AI Processing for the Bot object -void Bot::AI_Process() { +void Bot::SetHoldMode() { -#define TEST_TARGET() if (!GetTarget()) { return; } + SetHoldFlag(); +} + +// AI Processing for the Bot object +void Bot::AI_Process() +{ + constexpr float MAX_CASTER_DISTANCE[PLAYER_CLASS_COUNT] = { + 0, (34 * 34), (24 * 24), (28 * 28), (26 * 26), (42 * 42), 0, 0, 0, (38 * 38), (54 * 54), (48 * 48), (52 * 52), (50 * 50), (30 * 30), 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 + }; + +#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() == EQEmu::constants::stancePassive) +#define NOT_PASSIVE (GetBotStance() != EQEmu::constants::stancePassive) Client* bot_owner = (GetBotOwner() && GetBotOwner()->IsClient() ? GetBotOwner()->CastToClient() : nullptr); Group* bot_group = GetGroup(); - + +//#pragma region PRIMARY AI SKIP CHECKS + // Primary reasons for not processing AI - if (!bot_owner || !bot_group || !IsAIControlled()) + if (!bot_owner || !bot_group || !IsAIControlled()) { return; + } if (bot_owner->IsDead()) { + SetTarget(nullptr); SetBotOwner(nullptr); @@ -2275,47 +2346,62 @@ void Bot::AI_Process() { // We also need a leash owner and follow mob (subset of primary AI criteria) Client* leash_owner = (bot_group->GetLeader() && bot_group->GetLeader()->IsClient() ? bot_group->GetLeader()->CastToClient() : bot_owner); - if (!leash_owner) + 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()) + + if (IsCasting()) { InterruptSpell(); + } + if (IsMyHealRotationSet() || (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this)) { + AdvanceHealRotation(false); m_member_of_heal_rotation->SetMemberIsCasting(this, false); } - + return; } - - bool guard_mode = (follow_mob == this); - auto fm_dist = DistanceSquared(m_Position, follow_mob->GetPosition()); - auto lo_distance = DistanceSquared(m_Position, leash_owner->GetPosition()); +//#pragma endregion + + float fm_distance = DistanceSquared(m_Position, follow_mob->GetPosition()); + float lo_distance = DistanceSquared(m_Position, leash_owner->GetPosition()); + +//#pragma region CURRENTLY CASTING CHECKS if (IsCasting()) { + if (IsHealRotationMember() && m_member_of_heal_rotation->CastingOverride() && m_member_of_heal_rotation->CastingTarget() != nullptr && @@ -2326,26 +2412,23 @@ void Bot::AI_Process() { InterruptSpell(); } else if (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this) { + AdvanceHealRotation(false); return; } else if (GetClass() != BARD) { - if (IsEngaged()) - return; - if (fm_dist > GetFollowDistance()) // Cancel out-of-combat casting if movement is required - InterruptSpell(); - if (guard_mode) { - auto& my_pos = GetPosition(); - auto& my_guard = GetGuardPoint(); - if (my_pos.x != my_guard.x || - my_pos.y != my_guard.y || - my_pos.z != my_guard.z) - { - InterruptSpell(); - } + 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; } } @@ -2353,134 +2436,365 @@ void Bot::AI_Process() { 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()) { - Mob* delete_me = HealRotationTarget(); + if (AIHealRotation(HealRotationTarget(), UseHealRotationFastHeals())) { -#if (EQDEBUG >= 12) - LogError("Bot::AI_Process() - Casting succeeded (m: [{}], t: [{}]) : AdvHR(true)", GetCleanName(), ((delete_me) ? (delete_me->GetCleanName()) : ("nullptr"))); -#endif + m_member_of_heal_rotation->SetMemberIsCasting(this); m_member_of_heal_rotation->UpdateTargetHealingStats(HealRotationTarget()); AdvanceHealRotation(); } else { -#if (EQDEBUG >= 12) - LogError("Bot::AI_Process() - Casting failed (m: [{}], t: [{}]) : AdvHR(false)", GetCleanName(), ((delete_me) ? (delete_me->GetCleanName()) : ("nullptr"))); -#endif + m_member_of_heal_rotation->SetMemberIsCasting(this, false); AdvanceHealRotation(false); } } - // Empty hate list - let's find a target - if (!guard_mode && !IsEngaged()) { - Mob* lo_target = leash_owner->GetTarget(); +//#pragma endregion - if (lo_target && lo_target->IsNPC() && - !lo_target->IsMezzed() && - (lo_target->GetHateAmount(leash_owner) || leash_owner->AutoAttackEnabled()) && - lo_distance <= BOT_LEASH_DISTANCE && - DistanceSquared(m_Position, lo_target->GetPosition()) <= BOT_LEASH_DISTANCE && - (CheckLosFN(lo_target) || leash_owner->CheckLosFN(lo_target)) && - IsAttackAllowed(lo_target)) - { - AddToHateList(lo_target, 1); - if (HasPet()) - GetPet()->AddToHateList(lo_target, 1); + bool bo_alt_combat = 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); } - else { - for (int counter = 0; counter < bot_group->GroupCount(); counter++) { - Mob* bg_member = bot_group->members[counter]; - if (!bg_member) - continue; - Mob* bgm_target = bg_member->GetTarget(); - if (!bgm_target || !bgm_target->IsNPC()) - continue; + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); - if (!bgm_target->IsMezzed() && - bgm_target->GetHateAmount(bg_member) && - lo_distance <= BOT_LEASH_DISTANCE && - DistanceSquared(m_Position, bgm_target->GetPosition()) <= BOT_LEASH_DISTANCE && - (CheckLosFN(bgm_target) || leash_owner->CheckLosFN(bgm_target)) && - IsAttackAllowed(bgm_target)) - { - AddToHateList(bgm_target, 1); - if (HasPet()) - GetPet()->AddToHateList(bgm_target, 1); + if (NOT_HOLDING && NOT_PASSIVE) { - break; + 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 <= BOT_LEASH_DISTANCE && + DistanceSquared(m_Position, lo_target->GetPosition()) <= BOT_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 { + + for (int counter = 0; counter < bot_group->GroupCount(); counter++) { + + Mob* bg_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 <= BOT_LEASH_DISTANCE && + DistanceSquared(m_Position, bgm_target->GetPosition()) <= BOT_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()) + + if (rest_timer.Enabled()) { rest_timer.Disable(); - - // Group roles can be expounded upon in the future - 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()); - - find_target = false; - } - else if (assist_mob != this) { - SetTarget(nullptr); - if (HasPet()) - GetPet()->SetTarget(nullptr); - - find_target = false; - } } - if (find_target) { - if (IsRooted()) { - SetTarget(hate_list.GetClosestEntOnHateList(this)); +//#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 (HasTargetReflection()) { + + WipeHateList(); + SetTarget(nullptr); + SetPullingFlag(false); + SetReturningFlag(); + + return; } else { - // This will keep bots on target for now..but, future updates will allow for rooting/stunning - SetTarget(hate_list.GetEscapingEntOnHateList(leash_owner, BOT_LEASH_DISTANCE)); - if (!GetTarget()) - SetTarget(hate_list.GetEntWithMostHateOnList(this)); + // Default action is to aggress towards enemy } } - - TEST_TARGET(); - Mob* tar = GetTarget(); - if (!tar) +//#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 + 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)); + } + else { + + // This will keep bots on target for now..but, future updates will allow for rooting/stunning + SetTarget(hate_list.GetEscapingEntOnHateList(leash_owner, BOT_LEASH_DISTANCE)); + if (!GetTarget()) { + SetTarget(hate_list.GetEntWithMostHateOnList(this)); + } + } + } + } + +//#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()); - // Let's check if we have a los with our target. - // If we don't, our hate_list is wiped. - // Else, it was causing the bot to aggro behind wall etc... causing massive trains. - if (guard_mode || +//#pragma region TARGET VALIDATION + + // DOUBLE-CHECK THIS CRITERIA + + // Verify that our target has attackable criteria + if (HOLDING || !tar->IsNPC() || tar->IsMezzed() || - (!tar->GetHateAmount(this) && !tar->GetHateAmount(leash_owner) && !leash_owner->AutoAttackEnabled()) || lo_distance > BOT_LEASH_DISTANCE || tar_distance > BOT_LEASH_DISTANCE || - (!CheckLosFN(tar) && !leash_owner->CheckLosFN(tar)) || - !IsAttackAllowed(tar)) + (!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); } @@ -2488,20 +2802,59 @@ void Bot::AI_Process() { RemoveFromHateList(tar); SetTarget(nullptr); - if (IsMoving()) + 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; } - if (HasPet()) // this causes conflicts with default pet handler (bounces between targets) - GetPet()->SetTarget(tar); +//#pragma endregion - if (DivineAura()) + // 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))) + } + + 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; @@ -2511,251 +2864,312 @@ void Bot::AI_Process() { bool behind_mob = false; bool backstab_weapon = false; if (GetClass() == ROGUE) { - behind_mob = BehindMob(tar, GetX(), GetY()); // can be separated for other future use + + behind_mob = BehindMob(tar, GetX(), GetY()); // Can be separated for other future use backstab_weapon = p_item && p_item->GetItemBackstabDamage(); } - // Calculate melee distance + // 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 + 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) + } + 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 + 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) + } + else if (other_size_mod < 6.0f) { other_size_mod = 8.0f; + } - if (other_size_mod > size_mod) + if (other_size_mod > size_mod) { size_mod = other_size_mod; + } - if (size_mod > 29.0f) + if (size_mod > 29.0f) { size_mod *= size_mod; - else if (size_mod > 19.0f) + } + else if (size_mod > 19.0f) { size_mod *= (size_mod * 2.0f); - else + } + else { size_mod *= (size_mod * 4.0f); + } - // prevention of ridiculously sized hit boxes - if (size_mod > 10000.0f) + // Prevention of ridiculously sized hit boxes + if (size_mod > 10000.0f) { size_mod = (size_mod / 7.0f); + } melee_distance_max = size_mod; - } - float melee_distance = 0.0f; + 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; + } - 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 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; - case ROGUE: - if (behind_mob && backstab_weapon) { - if (p_item->GetItem()->IsType2HWeapon()) // p_item tested 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; } - // 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 casting distance + // Calculate caster distances float caster_distance_max = 0.0f; - { - if (GetLevel() >= GetStopMeleeLevel()) { - switch (GetClass()) { - case CLERIC: - caster_distance_max = 1156.0f; // as DSq value (34 units) - break; - case PALADIN: - caster_distance_max = 576.0f; // as DSq value (24 units) - break; - case RANGER: - caster_distance_max = 784.0f; // as DSq value (28 units) - break; - case SHADOWKNIGHT: - caster_distance_max = 676.0f; // as DSq value (26 units) - break; - case DRUID: - caster_distance_max = 1764.0f; // as DSq value (42 units) - break; - case SHAMAN: - caster_distance_max = 1444.0f; // as DSq value (38 units) - break; - case NECROMANCER: - caster_distance_max = 2916.0f; // as DSq value (54 units) - break; - case WIZARD: - caster_distance_max = 2304.0f; // as DSq value (48 units) - break; - case MAGICIAN: - caster_distance_max = 2704.0f; // as DSq value (52 units) - break; - case ENCHANTER: - caster_distance_max = 2500.0f; // as DSq value (50 units) - break; - case BEASTLORD: - caster_distance_max = 900.0f; // as DSq value (30 units) - break; - default: - // pure melee classes (and BARD) do not get this option - break; - } - } - } - float caster_distance_min = 0.0f; - if (caster_distance_max) { - caster_distance_min = melee_distance_max; + 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) - caster_distance_max = caster_distance_min * 1.25f; + 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) + if (changeWeapons) { ChangeBotArcherWeapons(IsBotArcher()); + } } - // all of this needs review... + 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; + } - 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()) { - StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); + + // 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 (evade_timer.Check(false)) { // Attempt to evade + + if (m_evade_timer.Check(false)) { // Attempt to evade + int timer_duration = (HideReuseTime - GetSkillReuseTime(EQEmu::skills::SkillHide)) * 1000; - if (timer_duration < 0) + if (timer_duration < 0) { timer_duration = 0; - - evade_timer.Start(timer_duration); - if (zone->random.Int(0, 260) < (int)GetSkill(EQEmu::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)) { - WalkTo(Goal.x, Goal.y, Goal.z); - 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 (DistanceSquared(Goal, tar->GetPosition()) <= caster_distance_max) { - WalkTo(Goal.x, Goal.y, Goal.z); - return; - } - } - } - else if (tar_distance < melee_distance_min) { // Melee back-off adjustment - if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { - if (DistanceSquared(Goal, tar->GetPosition()) <= melee_distance_max) { - WalkTo(Goal.x, Goal.y, Goal.z); - return; - } - } - } - else if (backstab_weapon && !behind_mob) { // Move the rogue to behind the mob - if (PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { - if (DistanceSquared(Goal, tar->GetPosition()) <= melee_distance_max) { - RunTo(Goal.x, Goal.y, Goal.z); - return; - } - } - } - else { - if (caster_distance_max == 0.0f && // Not a caster or a caster still below melee stop level (standard combat jitter) - zone->random.Int(1, 100) >= 94 && // 7:100 chance - PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) // If we're behind the mob, we can attack when it's enraged - { - if (DistanceSquared(Goal, tar->GetPosition()) <= melee_distance_max) { - WalkTo(Goal.x, Goal.y, Goal.z); - return; - } - } - } - if (!IsFacingMob(tar)) { - FaceTarget(tar); - return; + m_evade_timer.Start(timer_duration); + if (zone->random.Int(0, 260) < (int)GetSkill(EQEmu::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; } @@ -2767,93 +3181,111 @@ void Bot::AI_Process() { } // Up to this point, GetTarget() has been safe to dereference since the initial - // TEST_TARGET() call. Due to the chance of the target dying and our pointer + // 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_TARGET(); - if (GetTarget()->GetHPRatio() <= 99.0f) + if (IsBotArcher() && ranged_timer.Check(false)) { // Can shoot mezzed, stunned and dead!? + + TEST_COMBATANTS(); + if (GetTarget()->GetHPRatio() <= 99.0f) { BotRangedAttack(tar); + } } else if (!IsBotArcher() && (IsBotNonSpellFighter() || GetLevel() < GetStopMeleeLevel())) { - // we can't fight if we don't have a target, are stun/mezzed or dead.. + + // We can't fight if we don't have a target, are stun/mezzed or dead.. // Stop attacking if the target is enraged - TEST_TARGET(); - if (GetBotStance() == EQEmu::constants::stancePassive || (tar->IsEnraged() && !BehindMob(tar, GetX(), GetY()))) + TEST_COMBATANTS(); + if (tar->IsEnraged() && !BehindMob(tar, GetX(), GetY())) { return; + } // First, special attack per class (kick, backstab etc..) - TEST_TARGET(); + TEST_COMBATANTS(); DoClassAttacks(tar); - TEST_TARGET(); + TEST_COMBATANTS(); if (attack_timer.Check()) { // Process primary weapon attacks + Attack(tar, EQEmu::invslot::slotPrimary); - TEST_TARGET(); + TEST_COMBATANTS(); TriggerDefensiveProcs(tar, EQEmu::invslot::slotPrimary, false); - - TEST_TARGET(); - TryWeaponProc(p_item, tar, EQEmu::invslot::slotPrimary); - - //bool tripleSuccess = false; - TEST_TARGET(); + TEST_COMBATANTS(); + TryWeaponProc(p_item, tar, EQEmu::invslot::slotPrimary); + + // bool tripleSuccess = false; + + TEST_COMBATANTS(); if (CanThisClassDoubleAttack()) { - if (CheckBotDoubleAttack()) - Attack(tar, EQEmu::invslot::slotPrimary, true); - - TEST_TARGET(); - if (GetSpecialAbility(SPECATK_TRIPLE) && CheckBotDoubleAttack(true)) { - //tripleSuccess = true; + + if (CheckBotDoubleAttack()) { Attack(tar, EQEmu::invslot::slotPrimary, true); } - TEST_TARGET(); - //quad attack, does this belong here?? - if (GetSpecialAbility(SPECATK_QUAD) && CheckBotDoubleAttack(true)) + TEST_COMBATANTS(); + if (GetSpecialAbility(SPECATK_TRIPLE) && CheckBotDoubleAttack(true)) { + // tripleSuccess = true; Attack(tar, EQEmu::invslot::slotPrimary, true); + } + + TEST_COMBATANTS(); + // quad attack, does this belong here?? + if (GetSpecialAbility(SPECATK_QUAD) && CheckBotDoubleAttack(true)) { + Attack(tar, EQEmu::invslot::slotPrimary, true); + } } - TEST_TARGET(); - //Live AA - Flurry, Rapid Strikes ect (Flurry does not require Triple Attack). + 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, EQEmu::invslot::slotPrimary, false); - - TEST_TARGET(); + + TEST_COMBATANTS(); Attack(tar, EQEmu::invslot::slotPrimary, false); } } - TEST_TARGET(); + 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) + + if (zone->random.Int(0, 100) < ExtraAttackChanceBonus) { Attack(tar, EQEmu::invslot::slotPrimary, false); + } } } } - TEST_TARGET(); + TEST_COMBATANTS(); if (attack_dw_timer.Check() && CanThisClassDualWield()) { // Process secondary weapon attacks + const EQEmu::ItemData* s_itemdata = nullptr; - //can only dual wield without a weapon if you're a monk + // Can only dual wield without a weapon if you're a monk if (s_item || (GetClass() == MONK)) { - if(s_item) + + 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); @@ -2863,140 +3295,209 @@ void Bot::AI_Process() { DualWieldProbability += (DualWieldProbability * float(DWBonus) / 100.0f); float random = zone->random.Real(0, 1); - - if (random < DualWieldProbability){ // Max 78% of DW + if (random < DualWieldProbability) { // Max 78% of DW + Attack(tar, EQEmu::invslot::slotSecondary); // Single attack with offhand - - TEST_TARGET(); + + TEST_COMBATANTS(); TryWeaponProc(s_item, tar, EQEmu::invslot::slotSecondary); - TEST_TARGET(); + TEST_COMBATANTS(); if (CanThisClassDoubleAttack() && CheckBotDoubleAttack()) { - if (tar->GetHP() > -10) + + if (tar->GetHP() > -10) { Attack(tar, EQEmu::invslot::slotSecondary); // Single attack with offhand + } } } } } } } - } - else { // To far away to fight (GetTarget() validity can be iffy below this point - including outer scopes) - if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == BARD)) { // Pursue processing - if (GetTarget() && !IsRooted()) { - LogAI("Pursuing [{}] while engaged", GetTarget()->GetCleanName()); + 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(); - - RunTo(Goal.x, Goal.y, Goal.z); + if (DistanceSquared(m_Position, Goal) <= BOT_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(); + if (IsMoving()) { + StopMoving(); + } return; } } - // Fix Z when following during pull, not when engaged and stationary - if (GetTarget() && GetTarget()->IsFeared() && !spellend_timer.Enabled() && AI_think_timer->Check()) { - if (!IsFacingMob(GetTarget())) + + if (!IsFacingMob(GetTarget())) { FaceTarget(GetTarget()); + } // This is a mob that is fleeing either because it has been feared or is low on hitpoints - if (GetBotStance() != EQEmu::constants::stancePassive) { - AI_PursueCastCheck(); // This appears to always return true..can't trust for success/fail - return; - } + AI_PursueCastCheck(); // This appears to always return true..can't trust for success/fail + + return; } - } // end not in combat range + } // End not in combat range + +//#pragma endregion if (!IsMoving() && !spellend_timer.Enabled()) { // This may actually need work... - if (GetBotStance() == EQEmu::constants::stancePassive) - return; - - if (GetTarget() && AI_EngagedCastCheck()) + + if (GetTarget() && AI_EngagedCastCheck()) { BotMeditate(false); - else if (GetArchetype() == ARCHETYPE_CASTER) + } + 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 (bot_owner->GetAggroCount() && bot_owner->GetBotOption(Client::booAutoDefend)) { + + 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 && DistanceSquared(hater->GetPosition(), bot_owner->GetPosition()) <= BOT_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); + } + + return; + } + } + } + } + } + +//#pragma endregion + SetTarget(nullptr); - if (HasPet()) { + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + GetPet()->WipeHateList(); GetPet()->SetTarget(nullptr); } - - if (m_PlayerState & static_cast(PlayerState::Aggressive)) + + if (m_PlayerState & static_cast(PlayerState::Aggressive)) { SendRemovePlayerState(PlayerState::Aggressive); - - // Check guard point - if (guard_mode) { - auto& my_pos = GetPosition(); - auto& my_guard = GetGuardPoint(); - - if (my_pos.x != my_guard.x || - my_pos.y != my_guard.y || - my_pos.z != my_guard.z) - { - if (IsMoving()) - StopMoving(); - - Teleport(my_guard); - - if (HasPet()) - GetPet()->Teleport(my_guard); - - return; - } - } - // Leash the bot - else if (lo_distance > BOT_LEASH_DISTANCE) { - if (IsMoving()) - StopMoving(); - - Teleport(leash_owner->GetPosition()); - - if (HasPet()) - GetPet()->Teleport(leash_owner->GetPosition()); - - return; } +//#pragma region OK TO IDLE + // Ok to idle - if (fm_dist <= GetFollowDistance()) { + if ((NOT_GUARDING && fm_distance <= GetFollowDistance()) || (GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) <= GetFollowDistance())) { + if (!IsMoving() && AI_think_timer->Check() && !spellend_timer.Enabled()) { - if (GetBotStance() != EQEmu::constants::stancePassive) { - if (!AI_IdleCastCheck() && !IsCasting() && GetClass() != BARD) + + if (NOT_PASSIVE) { + + if (!AI_IdleCastCheck() && !IsCasting() && GetClass() != BARD) { BotMeditate(true); + } } else { - if (GetClass() != BARD) + + if (GetClass() != BARD) { BotMeditate(true); + } } return; } } - + // Non-engaged movement checks if (AI_movement_timer->Check() && (!IsCasting() || GetClass() == BARD)) { - if (fm_dist > GetFollowDistance()) { + + 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()) + + if (rest_timer.Enabled()) { rest_timer.Disable(); + } bool running = true; - if (fm_dist < GetFollowDistance() + BOT_FOLLOW_DISTANCE_WALK) - running = false; - Goal = follow_mob->GetPosition(); + if (destination_distance < GetFollowDistance() + BOT_FOLLOW_DISTANCE_WALK) { + running = false; + } if (running) { RunTo(Goal.x, Goal.y, Goal.z); @@ -3004,27 +3505,43 @@ void Bot::AI_Process() { 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 (IsMoving()) { - if (GetBotStance() != EQEmu::constants::stancePassive) { - if (GetClass() == BARD && !spellend_timer.Enabled() && AI_think_timer->Check()) { - AI_IdleCastCheck(); - return; - } + 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 @@ -4276,6 +4793,10 @@ bool Bot::Death(Mob *killerMob, int32 damage, uint16 spell_id, EQEmu::skills::Sk LeaveHealRotationMemberPool(); + if ((GetPullingFlag() || GetReturningFlag()) && my_owner && my_owner->IsClient()) { + my_owner->CastToClient()->SetBotPulling(false); + } + entity_list.RemoveBot(this->GetID()); return true; } @@ -8796,18 +9317,19 @@ bool Bot::GetNeedsHateRedux(Mob *tar) { if (!tar || !tar->IsEngaged() || !tar->HasTargetReflection() || !tar->GetTarget()->IsNPC()) return false; - if (tar->IsClient()) { - switch (tar->GetClass()) { - // TODO: figure out affectable classes.. - // Might need flag to allow player to determine redux req... - default: - return false; - } - } - else if (tar->IsBot()) { + //if (tar->IsClient()) { + // switch (tar->GetClass()) { + // // TODO: figure out affectable classes.. + // // Might need flag to allow player to determine redux req... + // default: + // return false; + // } + //} + //else if (tar->IsBot()) { + if (tar->IsBot()) { switch (tar->GetClass()) { case ROGUE: - if (tar->CanFacestab() || tar->CastToBot()->evade_timer.Check(false)) + if (tar->CanFacestab() || tar->CastToBot()->m_evade_timer.Check(false)) return false; case CLERIC: case DRUID: @@ -9120,6 +9642,22 @@ std::string Bot::CreateSayLink(Client* c, const char* message, const char* name) return saylink; } +void Bot::StopMoving() +{ + //SetCombatJitterFlag(false); + //m_combat_jitter_timer.Start(zone->random.Int(BOT_COMBAT_JITTER_INTERVAL_MIN, BOT_COMBAT_JITTER_INTERVAL_MAX)); + + Mob::StopMoving(); +} + +void Bot::StopMoving(float new_heading) +{ + //SetCombatJitterFlag(false); + //m_combat_jitter_timer.Start(zone->random.Int(BOT_COMBAT_JITTER_INTERVAL_MIN, BOT_COMBAT_JITTER_INTERVAL_MAX)); + + Mob::StopMoving(new_heading); +} + uint8 Bot::spell_casting_chances[SPELL_TYPE_COUNT][PLAYER_CLASS_COUNT][EQEmu::constants::STANCE_TYPE_COUNT][cntHSND] = { 0 }; #endif diff --git a/zone/bot.h b/zone/bot.h index 20348181e..abc3e1dae 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -37,21 +37,24 @@ #include -#define BOT_FOLLOW_DISTANCE_DEFAULT 184 // as DSq value (~13.565 units) -#define BOT_FOLLOW_DISTANCE_DEFAULT_MAX 2500 // as DSq value (50 units) -#define BOT_FOLLOW_DISTANCE_WALK 1000 // as DSq value (~31.623 units) +constexpr float BOT_FOLLOW_DISTANCE_DEFAULT = 184.0f; // as DSq value (~13.565 units) +constexpr float BOT_FOLLOW_DISTANCE_DEFAULT_MAX = 2500.0f; // as DSq value (50 units) +constexpr float BOT_FOLLOW_DISTANCE_WALK = 1000.0f; // as DSq value (~31.623 units) -#define BOT_LEASH_DISTANCE 250000 // as DSq value (500 units) +constexpr float BOT_LEASH_DISTANCE = 250000.0f; // as DSq value (500 units) -#define BOT_KEEP_ALIVE_INTERVAL 5000 // 5 seconds +constexpr uint32 BOT_KEEP_ALIVE_INTERVAL = 5000; // 5 seconds + +//constexpr uint32 BOT_COMBAT_JITTER_INTERVAL_MIN = 5000; // 5 seconds +//constexpr uint32 BOT_COMBAT_JITTER_INTERVAL_MAX = 20000; // 20 seconds extern WorldServer worldserver; -const int BotAISpellRange = 100; // TODO: Write a method that calcs what the bot's spell range is based on spell, equipment, AA, whatever and replace this -const int MaxSpellTimer = 15; -const int MaxDisciplineTimer = 10; -const int DisciplineReuseStart = MaxSpellTimer + 1; -const int MaxTimer = MaxSpellTimer + MaxDisciplineTimer; +constexpr int BotAISpellRange = 100; // TODO: Write a method that calcs what the bot's spell range is based on spell, equipment, AA, whatever and replace this +constexpr int MaxSpellTimer = 15; +constexpr int MaxDisciplineTimer = 10; +constexpr int DisciplineReuseStart = MaxSpellTimer + 1; +constexpr int MaxTimer = MaxSpellTimer + MaxDisciplineTimer; @@ -260,10 +263,24 @@ public: void Stand(); bool IsSitting(); bool IsStanding(); - virtual int GetWalkspeed() const { return (int)((float)_GetWalkSpeed() * 1.785714f); } // 1.25 / 0.7 = 1.7857142857142857142857142857143 - virtual int GetRunspeed() const { return (int)((float)_GetRunSpeed() * 1.785714f); } + virtual int GetWalkspeed() const { return (int)((float)_GetWalkSpeed() * 1.785714285f); } // 1.25 / 0.7 = 1.7857142857142857142857142857143 + virtual int GetRunspeed() const { return (int)((float)_GetRunSpeed() * 1.785714285f); } virtual void WalkTo(float x, float y, float z); virtual void RunTo(float x, float y, float z); + virtual void StopMoving(); + virtual void StopMoving(float new_heading); + //bool GetCombatJitterFlag() { return m_combat_jitter_flag; } + bool GetGuardFlag() { return m_guard_flag; } + void SetGuardFlag(bool flag = true) { m_guard_flag = flag; } + bool GetHoldFlag() { return m_hold_flag; } + void SetHoldFlag(bool flag = true) { m_hold_flag = flag; } + bool GetAttackFlag() { return m_attack_flag; } + void SetAttackFlag(bool flag = true) { m_attack_flag = flag; } + bool GetAttackingFlag() { return m_attacking_flag; } + bool GetPullFlag() { return m_pull_flag; } + void SetPullFlag(bool flag = true) { m_pull_flag = flag; } + bool GetPullingFlag() { return m_pulling_flag; } + bool GetReturningFlag() { return m_returning_flag; } bool UseDiscipline(uint32 spell_id, uint32 target); uint8 GetNumberNeedingHealedInGroup(uint8 hpr, bool includePets); bool GetNeedsCured(Mob *tar); @@ -338,6 +355,7 @@ public: uint8 GetStopMeleeLevel() { return _stopMeleeLevel; } void SetStopMeleeLevel(uint8 level); void SetGuardMode(); + void SetHoldMode(); // Mob AI Virtual Override Methods virtual void AI_Process(); @@ -674,7 +692,18 @@ private: int32 end_regen; uint32 timers[MaxTimer]; - Timer evade_timer; // can be moved to pTimers at some point + Timer m_evade_timer; // can be moved to pTimers at some point + Timer m_alt_combat_hate_timer; + //Timer m_combat_jitter_timer; + //bool m_combat_jitter_flag; + bool m_guard_flag; + bool m_hold_flag; + bool m_attack_flag; + bool m_attacking_flag; + bool m_pull_flag; + bool m_pulling_flag; + bool m_returning_flag; + eStandingPetOrder m_previous_pet_order; BotCastingRoles m_CastingRoles; @@ -716,6 +745,10 @@ private: int32 GenerateBaseManaPoints(); void GenerateSpecialAttacks(); void SetBotID(uint32 botID); + //void SetCombatJitterFlag(bool flag = true) { m_combat_jitter_flag = flag; } + void SetAttackingFlag(bool flag = true) { m_attacking_flag = flag; } + void SetPullingFlag(bool flag = true) { m_pulling_flag = flag; } + void SetReturningFlag(bool flag = true) { m_returning_flag = flag; } // Private "Inventory" Methods void GetBotItems(EQEmu::InventoryProfile &inv, std::string* errorMessage); diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index 3ca3ab169..7a1a7cac4 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -71,6 +71,8 @@ #include "water_map.h" #include "worldserver.h" +#include + extern QueryServ* QServ; extern WorldServer worldserver; extern TaskManager *taskmanager; @@ -1391,7 +1393,7 @@ int bot_command_init(void) bot_command_add("healrotationstart", "Starts a heal rotation", 0, bot_subcommand_heal_rotation_start) || bot_command_add("healrotationstop", "Stops a heal rotation", 0, bot_subcommand_heal_rotation_stop) || bot_command_add("help", "List available commands and their description - specify partial command as argument to search", 0, bot_command_help) || - bot_command_add("hold", "Suspends a bot's AI processing until released", 0, bot_command_hold) || + bot_command_add("hold", "Prevents a bot from attacking until released", 0, bot_command_hold) || bot_command_add("identify", "Orders a bot to cast an item identification spell", 0, bot_command_identify) || bot_command_add("inventory", "Lists the available bot inventory [subcommands]", 0, bot_command_inventory) || bot_command_add("inventorygive", "Gives the item on your cursor to a bot", 0, bot_subcommand_inventory_give) || @@ -1418,6 +1420,7 @@ int bot_command_init(void) bot_command_add("sendhome", "Orders a bot to open a magical doorway home", 0, bot_command_send_home) || bot_command_add("size", "Orders a bot to change a player's size", 0, bot_command_size) || bot_command_add("summoncorpse", "Orders a bot to summon a corpse to its feet", 0, bot_command_summon_corpse) || + bot_command_add("suspend", "Suspends a bot's AI processing until released", 0, bot_command_suspend) || bot_command_add("taunt", "Toggles taunt use by a bot", 0, bot_command_taunt) || bot_command_add("track", "Orders a capable bot to track enemies", 0, bot_command_track) || bot_command_add("waterbreathing", "Orders a bot to cast a water breathing spell", 0, bot_command_water_breathing) @@ -2577,38 +2580,55 @@ void bot_command_aggressive(Client *c, const Seperator *sep) void bot_command_attack(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_attack", sep->arg[0], "attack")) + if (helper_command_alias_fail(c, "bot_command_attack", sep->arg[0], "attack")) { return; + } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(m_usage, "usage: %s [actionable: byname | ownergroup | botgroup | namesgroup | healrotation | spawned] ([actionable_name])", sep->arg[0]); + + c->Message(m_usage, "usage: %s [actionable: byname | ownergroup | botgroup | namesgroup | healrotation | default: spawned] ([actionable_name])", sep->arg[0]); return; } const int ab_mask = ActionableBots::ABM_Type2; Mob* target_mob = ActionableTarget::AsSingle_ByAttackable(c); if (!target_mob) { + c->Message(m_fail, "You must an enemy to use this command"); return; } + std::string ab_arg(sep->arg[1]); + if (ab_arg.empty()) { + ab_arg = "spawned"; + } + std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) + if (ActionableBots::PopulateSBL(c, ab_arg.c_str(), sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) { return; + } + size_t attacker_count = 0; + Bot *first_attacker = nullptr; sbl.remove(nullptr); for (auto bot_iter : sbl) { - bot_iter->WipeHateList(); - bot_iter->AddToHateList(target_mob, 1); - if (!bot_iter->GetPet()) - continue; - bot_iter->GetPet()->WipeHateList(); - bot_iter->GetPet()->AddToHateList(target_mob, 1); + if (bot_iter->GetAppearance() != eaDead && bot_iter->GetBotStance() != EQEmu::constants::stancePassive) { + + if (!first_attacker) { + first_attacker = bot_iter; + } + ++attacker_count; + + bot_iter->SetAttackFlag(); + } + } + + if (attacker_count == 1 && first_attacker) { + Bot::BotGroupSay(first_attacker, "Attacking %s!", target_mob->GetCleanName()); + } + else { + c->Message(m_action, "%i of your bots are attacking %s!", sbl.size(), target_mob->GetCleanName()); } - if (sbl.size() == 1) - Bot::BotGroupSay(sbl.front(), "Attacking %s", target_mob->GetCleanName()); - else - c->Message(m_action, "%i of your bots are attacking %s", sbl.size(), target_mob->GetCleanName()); } void bot_command_bind_affinity(Client *c, const Seperator *sep) @@ -3098,26 +3118,50 @@ void bot_command_follow(Client *c, const Seperator *sep) void bot_command_guard(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_guard", sep->arg[0], "guard")) + if (helper_command_alias_fail(c, "bot_command_guard", sep->arg[0], "guard")) { return; + } if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(m_usage, "usage: %s [actionable: target | byname | ownergroup | botgroup | namesgroup | healrotation | spawned] ([actionable_name])", sep->arg[0]); + + c->Message(m_usage, "usage: %s ([option: clear]) [actionable: target | byname | ownergroup | botgroup | namesgroup | healrotation | spawned] ([actionable_name])", sep->arg[0]); return; } const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_Type2); + bool clear = false; + int ab_arg = 1; + int name_arg = 2; + + std::string clear_arg = sep->arg[1]; + if (!clear_arg.compare("clear")) { + + clear = true; + ab_arg = 2; + name_arg = 3; + } + std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[name_arg]) == ActionableBots::ABT_None) { return; + } sbl.remove(nullptr); for (auto bot_iter : sbl) { - bot_iter->SetGuardMode(); + + if (clear) { + bot_iter->SetGuardFlag(false); + } + else { + bot_iter->SetGuardMode(); + } + } + + if (sbl.size() == 1) { + Bot::BotGroupSay(sbl.front(), "%suarding this position.", (clear ? "No longer g" : "G")); + } + else { + c->Message(m_action, "%i of your bots are %sguarding their positions.", sbl.size(), (clear ? "no longer " : "")); } - if (sbl.size() == 1) - Bot::BotGroupSay(sbl.front(), "Guarding this position"); - else - c->Message(m_action, "%i of your bots are guarding their positions", sbl.size()); } void bot_command_heal_rotation(Client *c, const Seperator *sep) @@ -3199,23 +3243,50 @@ void bot_command_help(Client *c, const Seperator *sep) void bot_command_hold(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_hold", sep->arg[0], "hold")) - return; - if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(m_usage, "usage: %s ([actionable: ] ([actionable_name]))", sep->arg[0]); + if (helper_command_alias_fail(c, "bot_command_hold", sep->arg[0], "hold")) { return; } - const int ab_mask = ActionableBots::ABM_NoFilter; + if (helper_is_help_or_usage(sep->arg[1])) { + + c->Message(m_usage, "usage: %s ([option: clear]) [actionable: target | byname | ownergroup | botgroup | namesgroup | healrotation | spawned] ([actionable_name])", sep->arg[0]); + return; + } + const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_Type2); + + bool clear = false; + int ab_arg = 1; + int name_arg = 2; + + std::string clear_arg = sep->arg[1]; + if (!clear_arg.compare("clear")) { + + clear = true; + ab_arg = 2; + name_arg = 3; + } std::list sbl; - if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) + if (ActionableBots::PopulateSBL(c, sep->arg[ab_arg], sbl, ab_mask, sep->arg[name_arg]) == ActionableBots::ABT_None) { return; + } sbl.remove(nullptr); - for (auto bot_iter : sbl) - bot_iter->SetPauseAI(true); - - c->Message(m_action, "%i of your bots %s suspended", sbl.size(), ((sbl.size() != 1) ? ("are") : ("is"))); + for (auto bot_iter : sbl) { + + if (clear) { + bot_iter->SetHoldFlag(false); + } + else { + bot_iter->SetHoldMode(); + } + } + + if (sbl.size() == 1) { + Bot::BotGroupSay(sbl.front(), "%solding my attacks.", (clear ? "No longer h" : "H")); + } + else { + c->Message(m_action, "%i of your bots are %sholding their attacks.", sbl.size(), (clear ? "no longer " : "")); + } } void bot_command_identify(Client *c, const Seperator *sep) @@ -3505,70 +3576,70 @@ void bot_command_owner_option(Client *c, const Seperator *sep) { if (helper_is_help_or_usage(sep->arg[1])) { - c->Message(m_usage, "usage: %s [option] [argument | null]", sep->arg[0]); + c->Message(m_usage, "usage: %s [option] [argument]", sep->arg[0]); std::string window_title = "Bot Owner Options"; std::string window_text = "" "" - "" - "" - "" + "" + "" + "" "" "" - "" - "" - "" + "" + "" + "" "" "" "" - "" - "" + "" + "" + "" + "" + "" + "" + "" "" "" "" - "" - "" + "" + "" "" "" - "" - "" - "" + "" + "" + "" "" "" "" - "" - "" + "" + "" + "" + "" + "" + "" + "" "" "" "" - "" - "" + "" + "" "" "" - "" - "" - "" + "" + "" + "" "" "" "" - "" - "" + "" + "" "" "" + "" "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" + "" "" "
OptionArgumentNotesOption
------
Argument
-------
Notes
-----
deathmarqueeenabledeathmarqueeenable | disablemarquee message on death
disablenull(toggles)
statsupdateenable | disablereport stats on update
null(toggles)null(toggles)
statsupdateenablespawnmessagesay | tell | silentspawn message into channel
disableclass | defaultspawn with class-based message
altcombatenable | disableuse alternate ai combat behavior
null(toggles)null(toggles)
spawnmessagesayautodefendenable | disablebots defend owner when aggroed
tellnull(toggles)
currentsilent
class
defaultshow current settings
"; @@ -3680,6 +3751,64 @@ void bot_command_owner_option(Client *c, const Seperator *sep) c->Message(m_action, "Bot 'spawn message' is now %s.", argument.c_str()); } + else if (!owner_option.compare("altcombat")) { + + if (!argument.compare("enable")) { + c->SetBotOption(Client::booAltCombat, true); + } + else if (!argument.compare("disable")) { + c->SetBotOption(Client::booAltCombat, false); + } + else { + c->SetBotOption(Client::booAltCombat, !c->GetBotOption(Client::booAltCombat)); + } + + database.botdb.SaveOwnerOption(c->CharacterID(), Client::booAltCombat, c->GetBotOption(Client::booAltCombat)); + + c->Message(m_action, "Bot 'alt combat' is now %s.", (c->GetBotOption(Client::booAltCombat) == true ? "enabled" : "disabled")); + } + else if (!owner_option.compare("autodefend")) { + + if (!argument.compare("enable")) { + c->SetBotOption(Client::booAutoDefend, true); + } + else if (!argument.compare("disable")) { + c->SetBotOption(Client::booAutoDefend, false); + } + else { + c->SetBotOption(Client::booAutoDefend, !c->GetBotOption(Client::booAutoDefend)); + } + + database.botdb.SaveOwnerOption(c->CharacterID(), Client::booAutoDefend, c->GetBotOption(Client::booAutoDefend)); + + c->Message(m_action, "Bot 'auto defend' is now %s.", (c->GetBotOption(Client::booAutoDefend) == true ? "enabled" : "disabled")); + } + else if (!owner_option.compare("current")) { + + std::string window_title = "Current Bot Owner Options Settings"; + std::string window_text = fmt::format( + "" + "" + "" + "" + "" + "" "" "" "" + "" "" "" "" + "" "" "" "" + "" "" "" "" + "" "" "" "" + "" "" "" "" + "
Option
------
Argument
-------
deathmarquee{}
statsupdate{}
spawnmessage{}
spawnmessage{}
altcombat{}
autodefend{}
", + (c->GetBotOption(Client::booDeathMarquee) ? "enabled" : "disabled"), + (c->GetBotOption(Client::booStatsUpdate) ? "enabled" : "disabled"), + (c->GetBotOption(Client::booSpawnMessageSay) ? "say" : (c->GetBotOption(Client::booSpawnMessageTell) ? "tell" : "silent")), + (c->GetBotOption(Client::booSpawnMessageClassSpecific) ? "class" : "default"), + (c->GetBotOption(Client::booAltCombat) ? "enabled" : "disabled"), + (c->GetBotOption(Client::booAutoDefend) ? "enabled" : "disabled") + ); + + c->SendPopupToClient(window_title.c_str(), window_text.c_str()); + } else { c->Message(m_fail, "Owner option '%s' is not recognized.", owner_option.c_str()); } @@ -3761,38 +3890,113 @@ void bot_command_pick_lock(Client *c, const Seperator *sep) c->Message(m_action, "%i door%s attempted - %i door%s successful", door_count, ((door_count != 1) ? ("s") : ("")), open_count, ((open_count != 1) ? ("s") : (""))); } +// TODO: Rework to allow owner specificed criteria for puller void bot_command_pull(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_pull", sep->arg[0], "pull")) + if (helper_command_alias_fail(c, "bot_command_pull", sep->arg[0], "pull")) { return; + } if (helper_is_help_or_usage(sep->arg[1])) { + c->Message(m_usage, "usage: %s", sep->arg[0]); return; } int ab_mask = ActionableBots::ABM_OwnerGroup; // existing behavior - need to add c->IsGrouped() check and modify code if different behavior is desired std::list sbl; - if (ActionableBots::PopulateSBL(c, "ownergroup", sbl, ab_mask) == ActionableBots::ABT_None) + if (ActionableBots::PopulateSBL(c, "ownergroup", sbl, ab_mask) == ActionableBots::ABT_None) { return; + } sbl.remove(nullptr); auto target_mob = ActionableTarget::VerifyEnemy(c, BCEnum::TT_Single); if (!target_mob) { - c->Message(m_fail, "Your current target is not attackable"); + + c->Message(m_fail, "Your current target is not attackable!"); return; } Bot* bot_puller = nullptr; for (auto bot_iter : sbl) { - if (!bot_iter->IsArcheryRange(target_mob)) + + if (bot_iter->GetAppearance() == eaDead || bot_iter->GetBotStance() == EQEmu::constants::stancePassive) { continue; + } - Bot::BotGroupSay(bot_iter, "Attempting to pull %s..", target_mob->GetCleanName()); - bot_iter->InterruptSpell(); - bot_iter->BotRangedAttack(target_mob); + switch (bot_iter->GetClass()) { + case ROGUE: + case MONK: + case BARD: + case RANGER: + bot_puller = bot_iter; + break; + case WARRIOR: + case SHADOWKNIGHT: + case PALADIN: + case BERSERKER: + case BEASTLORD: + if (!bot_puller) { + + bot_puller = bot_iter; + continue; + } + + switch (bot_puller->GetClass()) { + case DRUID: + case SHAMAN: + case CLERIC: + case WIZARD: + case NECROMANCER: + case MAGICIAN: + case ENCHANTER: + bot_puller = bot_iter; + default: + continue; + } + + continue; + case DRUID: + case SHAMAN: + case CLERIC: + if (!bot_puller) { + + bot_puller = bot_iter; + continue; + } + + switch (bot_puller->GetClass()) { + case WIZARD: + case NECROMANCER: + case MAGICIAN: + case ENCHANTER: + bot_puller = bot_iter; + default: + continue; + } + + continue; + case WIZARD: + case NECROMANCER: + case MAGICIAN: + case ENCHANTER: + if (!bot_puller) { + bot_puller = bot_iter; + } + + continue; + default: + continue; + } + + bot_puller = bot_iter; + break; } + + if (bot_puller) { + bot_puller->SetPullFlag(); + } helper_no_available_bots(c, bot_puller); } @@ -3817,7 +4021,7 @@ void bot_command_release(Client *c, const Seperator *sep) bot_iter->SetPauseAI(false); } - c->Message(m_action, "%i of your bots %s unsuspended", sbl.size(), ((sbl.size() != 1) ? ("are") : ("is"))); + c->Message(m_action, "%i of your bots %s released.", sbl.size(), ((sbl.size() != 1) ? ("are") : ("is"))); } void bot_command_resistance(Client *c, const Seperator *sep) @@ -4126,6 +4330,30 @@ void bot_command_summon_corpse(Client *c, const Seperator *sep) helper_no_available_bots(c, my_bot); } +void bot_command_suspend(Client *c, const Seperator *sep) +{ + if (helper_command_alias_fail(c, "bot_command_suspend", sep->arg[0], "suspend")) { + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { + c->Message(m_usage, "usage: %s ([actionable: ] ([actionable_name]))", sep->arg[0]); + return; + } + const int ab_mask = ActionableBots::ABM_NoFilter; + + std::list sbl; + if (ActionableBots::PopulateSBL(c, sep->arg[1], sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) { + return; + } + + sbl.remove(nullptr); + for (auto bot_iter : sbl) { + bot_iter->SetPauseAI(true); + } + + c->Message(m_action, "%i of your bots %s suspended.", sbl.size(), ((sbl.size() != 1) ? ("are") : ("is"))); +} + void bot_command_taunt(Client *c, const Seperator *sep) { if (helper_command_alias_fail(c, "bot_command_taunt", sep->arg[0], "taunt")) diff --git a/zone/bot_command.h b/zone/bot_command.h index c8bad1e42..0f0fab0ef 100644 --- a/zone/bot_command.h +++ b/zone/bot_command.h @@ -586,6 +586,7 @@ void bot_command_rune(Client *c, const Seperator *sep); void bot_command_send_home(Client *c, const Seperator *sep); void bot_command_size(Client *c, const Seperator *sep); void bot_command_summon_corpse(Client *c, const Seperator *sep); +void bot_command_suspend(Client *c, const Seperator *sep); void bot_command_taunt(Client *c, const Seperator *sep); void bot_command_track(Client *c, const Seperator *sep); void bot_command_water_breathing(Client *c, const Seperator *sep); diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index 974bbf565..8496dfcd8 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -580,7 +580,7 @@ bool BotDatabase::SaveNewBot(Bot* bot_inst, uint32& bot_id) bot_inst->GetPR(), bot_inst->GetDR(), bot_inst->GetCorrup(), - BOT_FOLLOW_DISTANCE_DEFAULT, + (uint32)BOT_FOLLOW_DISTANCE_DEFAULT, (IsCasterClass(bot_inst->GetClass()) ? (uint8)RuleI(Bots, CasterStopMeleeLevel) : 255) ); auto results = database.QueryDatabase(query); @@ -2253,8 +2253,10 @@ bool BotDatabase::SaveOwnerOption(const uint32 owner_id, size_t type, const bool switch (static_cast(type)) { case Client::booDeathMarquee: case Client::booStatsUpdate: - case Client::booSpawnMessageClassSpecific: { - + case Client::booSpawnMessageClassSpecific: + case Client::booAltCombat: + case Client::booAutoDefend: + { query = fmt::format( "REPLACE INTO `bot_owner_options`(`owner_id`, `option_type`, `option_value`) VALUES ('{}', '{}', '{}')", owner_id, @@ -2282,11 +2284,12 @@ bool BotDatabase::SaveOwnerOption(const uint32 owner_id, const std::pair(type.first)) { case Client::booSpawnMessageSay: - case Client::booSpawnMessageTell: { + case Client::booSpawnMessageTell: + { switch (static_cast(type.second)) { case Client::booSpawnMessageSay: - case Client::booSpawnMessageTell: { - + case Client::booSpawnMessageTell: + { query = fmt::format( "REPLACE INTO `bot_owner_options`(`owner_id`, `option_type`, `option_value`) VALUES ('{}', '{}', '{}'), ('{}', '{}', '{}')", owner_id, diff --git a/zone/client.cpp b/zone/client.cpp index 6a0a10f5d..251b23ad1 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -352,6 +352,10 @@ Client::Client(EQStreamInterface* ieqs) bot_owner_options[booSpawnMessageSay] = false; bot_owner_options[booSpawnMessageTell] = true; bot_owner_options[booSpawnMessageClassSpecific] = true; + bot_owner_options[booAltCombat] = false; + bot_owner_options[booAutoDefend] = true; + + SetBotPulling(false); #endif AI_Init(); diff --git a/zone/client.h b/zone/client.h index cbbf3c40e..3ade2e6fe 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1628,9 +1628,6 @@ private: #ifdef BOTS - - - public: enum BotOwnerOption : size_t { booDeathMarquee, @@ -1638,14 +1635,20 @@ public: booSpawnMessageSay, booSpawnMessageTell, booSpawnMessageClassSpecific, + booAltCombat, + booAutoDefend, _booCount }; bool GetBotOption(BotOwnerOption boo) const; void SetBotOption(BotOwnerOption boo, bool flag = true); + bool GetBotPulling() { return m_bot_pulling; } + void SetBotPulling(bool flag = true) { m_bot_pulling = flag; } + private: bool bot_owner_options[_booCount]; + bool m_bot_pulling; #endif }; diff --git a/zone/mob.cpp b/zone/mob.cpp index 4e6b6bf5c..68571b709 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -1493,17 +1493,23 @@ void Mob::SendHPUpdate(bool skip_self /*= false*/, bool force_update_all /*= fal } } -void Mob::StopMoving() { +void Mob::StopMoving() +{ StopNavigation(); - if (moved) + + if (moved) { moved = false; + } } -void Mob::StopMoving(float new_heading) { +void Mob::StopMoving(float new_heading) +{ StopNavigation(); RotateTo(new_heading); - if (moved) + + if (moved) { moved = false; + } } void Mob::SentPositionPacket(float dx, float dy, float dz, float dh, int anim, bool send_to_self) @@ -2684,6 +2690,27 @@ bool Mob::PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, fl return Result; } +bool Mob::PlotPositionOnArcInFrontOfTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float distance, float min_deg, float max_deg) +{ + + + return false; +} + +bool Mob::PlotPositionOnArcBehindTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float distance) +{ + + + return false; +} + +bool Mob::PlotPositionBehindMeFacingTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_dist, float max_dist) +{ + + + return false; +} + bool Mob::HateSummon() { // check if mob has ability to summon // 97% is the offical % that summoning starts on live, not 94 diff --git a/zone/mob.h b/zone/mob.h index 4f07e10e6..56cdfbf39 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -593,8 +593,8 @@ public: void MakeSpawnUpdateNoDelta(PlayerPositionUpdateServer_Struct* spu); void MakeSpawnUpdate(PlayerPositionUpdateServer_Struct* spu); void SentPositionPacket(float dx, float dy, float dz, float dh, int anim, bool send_to_self = false); - void StopMoving(); - void StopMoving(float new_heading); + virtual void StopMoving(); + virtual void StopMoving(float new_heading); void SetSpawned() { spawned = true; }; bool Spawned() { return spawned; }; virtual bool ShouldISpawnFor(Client *c) { return true; } @@ -676,8 +676,10 @@ public: void ShowStats(Client* client); void ShowBuffs(Client* client); void ShowBuffList(Client* client); - bool PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, float &z_dest, - bool lookForAftArc = true); + bool PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, float &z_dest, bool lookForAftArc = true); + bool PlotPositionOnArcInFrontOfTarget(Mob *target, float &x_dest, float &y_dest, float &z_dest, float distance, float min_deg = 5.0f, float max_deg = 150.0f); + bool PlotPositionOnArcBehindTarget(Mob *target, float &x_dest, float &y_dest, float &z_dest, float distance); + bool PlotPositionBehindMeFacingTarget(Mob *target, float &x_dest, float &y_dest, float &z_dest, float min_dist = 1.0f, float max_dist = 5.0f); // aura functions void MakeAura(uint16 spell_id);