diff --git a/sql/Bots/updates/world/2024_08_31_00_command.sql b/sql/Bots/updates/world/2024_08_31_00_command.sql new file mode 100644 index 0000000000000..4a19d83e557c0 --- /dev/null +++ b/sql/Bots/updates/world/2024_08_31_00_command.sql @@ -0,0 +1,3 @@ +-- +INSERT IGNORE INTO `command` (`name`) VALUES +('npcbot order pull'); diff --git a/src/server/game/AI/NpcBots/bot_ai.cpp b/src/server/game/AI/NpcBots/bot_ai.cpp index f3786a5262f9f..475b722fa762b 100644 --- a/src/server/game/AI/NpcBots/bot_ai.cpp +++ b/src/server/game/AI/NpcBots/bot_ai.cpp @@ -3708,6 +3708,7 @@ bool bot_ai::CanBotAttack(Unit const* target, int8 byspell, bool secondary) cons } } + bool pulling = IsLastOrder(BOT_ORDER_PULL, 0, target->GetGUID()); uint8 followdist = IAmFree() ? BotMgr::GetBotFollowDistDefault() : master->GetBotMgr()->GetBotFollowDist(); float foldist = _getAttackDistance(float(followdist)); if (!IAmFree() && IsRanged() && me->IsWithinLOSInMap(target, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::M2)) @@ -3737,9 +3738,9 @@ bool bot_ai::CanBotAttack(Unit const* target, int8 byspell, bool secondary) cons } return - ((master->IsInCombat() || target->IsInCombat() || IsWanderer() || (IAmFree() && me->GetFaction() == 14)) && + ((master->IsInCombat() || target->IsInCombat() || IsWanderer() || (IAmFree() && me->GetFaction() == 14) || pulling) && target->IsVisible() && target->isTargetableForAttack(false) && me->IsValidAttackTarget(target) && - (!master->IsAlive() || target->IsControlledByPlayer() || + (!master->IsAlive() || target->IsControlledByPlayer() || pulling || (followdist > 0 && (master->GetDistance(target) <= foldist || HasBotCommandState(BOT_COMMAND_STAY)))) &&//if master is killed pursue to the end !IsInBotParty(target) && (target->InSamePhase(me) || CanSeeEveryone()) && (!HasBotCommandState(BOT_COMMAND_STAY) || @@ -3902,6 +3903,20 @@ std::tuple bot_ai::_getTargets(bool byspell, bool ranged, bool &re return { mytar, mytar }; //Immediate targets + //orders + if (!IAmFree() && HasOrders() && HasRole(BOT_ROLE_DPS) && !me->IsInCombat() && me->getAttackers().empty()) + { + if (_orders.front()._type == BOT_ORDER_PULL) + { + ObjectGuid orderTargetGuid = ObjectGuid(_orders.front().params.pullParams.targetGuid); + if (Unit* orderTarget = mytar && mytar->GetGUID() == orderTargetGuid ? mytar : ObjectAccessor::GetUnit(*me, orderTargetGuid)) + { + if (CanBotAttack(orderTarget)) + return { orderTarget, nullptr }; + } + } + } + //maps if (!IAmFree() && me->GetMap()->GetEntry() && !me->GetMap()->GetEntry()->IsWorldMap()) { static const std::array WMOAreaGroupLashlayer = { 29476u }; // Halls of Strife @@ -15601,6 +15616,9 @@ void bot_ai::JustEnteredCombat(Unit* u) ResetChase(u); + if (IsLastOrder(BOT_ORDER_PULL, 0, u->GetGUID())) + CompleteOrder(_orders.front()); + if (IAmFree() && me->GetVictim() && me->GetVictim() != u && (me->getAttackers().empty() || (me->getAttackers().size() == 1u && *me->getAttackers().begin() == u)) && me->GetVictim()->GetVictim() != me && !(me->GetVictim()->IsInCombat() || me->GetVictim()->IsInCombatWith(me))) @@ -16224,6 +16242,23 @@ void bot_ai::CancelAllOrders() } void bot_ai::_ProcessOrders() { + ordersTimer = 500; + + while (!_orders.empty()) + { + BotOrder const& order = _orders.front(); + if (order._timeout <= time(0)) + { + if (DEBUG_BOT_ORDERS) + TC_LOG_DEBUG("npcbots", "bot_ai::_ProcessOrders: {} front order (type {}) expired...", me->GetName(), uint32(order._type)); + CancelOrder(order); + } + else if (order._type == BOT_ORDER_PULL && (!HasRole(BOT_ROLE_DPS) || me->IsInCombat() || !me->getAttackers().empty())) + CompleteOrder(order); + else + break; + } + if (HasBotCommandState(BOT_COMMAND_ISSUED_ORDER)) return; @@ -16233,8 +16268,6 @@ void bot_ai::_ProcessOrders() if (_orders.empty()) return; - ordersTimer = 500; - BotOrder const& order = _orders.front(); Unit* target = nullptr; switch (order._type) @@ -16260,14 +16293,14 @@ void bot_ai::_ProcessOrders() } else { - TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: invalid spellCastParams.targetGuid " UI64FMTD "!", order.params.spellCastParams.targetGuid); + TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: invalid spellCastParams.targetGuid {}!", order.params.spellCastParams.targetGuid); CancelOrder(order); return; } if (!target || !target->IsInWorld()) { - TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: target " UI64FMTD " not found!", order.params.spellCastParams.targetGuid); + TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: target {} not found!", order.params.spellCastParams.targetGuid); CancelOrder(order); return; } @@ -16278,13 +16311,45 @@ void bot_ai::_ProcessOrders() doCast(target, _spells[order.params.spellCastParams.baseSpell]->spellId); break; } + case BOT_ORDER_PULL: + { + if (me->GetVictim()) + break; + if (CCed(me)) + break; + + SetBotCommandState(BOT_COMMAND_ISSUED_ORDER); + + if (order.params.pullParams.targetGuid) + target = ObjectAccessor::GetUnit(*me, ObjectGuid(order.params.pullParams.targetGuid)); + else + { + TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: invalid pullParams.targetGuid {}!", order.params.pullParams.targetGuid); + CancelOrder(order); + return; + } + + if (!target || !target->IsInWorld()) + { + TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: target {} not found!", order.params.pullParams.targetGuid); + CancelOrder(order); + return; + } + if (!target->IsAlive() || target->IsInCombat() || !CanBotAttack(target)) + { + TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: target {} cannot be pulled!", order.params.pullParams.targetGuid); + CancelOrder(order); + return; + } + break; + } default: TC_LOG_ERROR("scripts", "bot_ai:_ProcessOrders: invalid order type {}!", uint32(order._type)); CancelOrder(order); return; } } -bool bot_ai::IsLastOrder(BotOrderTypes order_type, uint32 param1) const +bool bot_ai::IsLastOrder(BotOrderTypes order_type, uint32 param1, ObjectGuid guidparam1) const { if (!_orders.empty()) { @@ -16294,7 +16359,11 @@ bool bot_ai::IsLastOrder(BotOrderTypes order_type, uint32 param1) const switch (order_type) { case BOT_ORDER_SPELLCAST: - if (order.params.spellCastParams.baseSpell == param1) + if (!param1 || order.params.spellCastParams.baseSpell == param1) + return true; + break; + case BOT_ORDER_PULL: + if (!guidparam1 || order.params.pullParams.targetGuid == guidparam1.GetRawValue()) return true; break; default: diff --git a/src/server/game/AI/NpcBots/bot_ai.h b/src/server/game/AI/NpcBots/bot_ai.h index 29c47bce7466c..07cc65dc6372e 100644 --- a/src/server/game/AI/NpcBots/bot_ai.h +++ b/src/server/game/AI/NpcBots/bot_ai.h @@ -788,9 +788,14 @@ class bot_ai : public CreatureAI uint32 baseSpell; } spellCastParams; + struct + { + uint64 targetGuid; + } pullParams; + } params; - explicit BotOrder(BotOrderTypes order_type) : _type(order_type) + explicit BotOrder(BotOrderTypes order_type, uint32 timeout_sec = 10) : _type(order_type), _timeout(time(0) + timeout_sec) { memset((char*)(¶ms), 0, sizeof(params)); } @@ -802,10 +807,11 @@ class bot_ai : public CreatureAI private: BotOrderTypes _type; + time_t _timeout; }; bool HasOrders() const { return !_orders.empty(); } - bool IsLastOrder(BotOrderTypes order_type, uint32 param1) const; + bool IsLastOrder(BotOrderTypes order_type, uint32 param1 = 0, ObjectGuid guidparam1 = ObjectGuid::Empty) const; std::size_t GetOrdersCount() const { return _orders.size(); } bool AddOrder(BotOrder&& order); void CancelOrder(BotOrder const& order); diff --git a/src/server/game/AI/NpcBots/botcommands.cpp b/src/server/game/AI/NpcBots/botcommands.cpp index ec25289f702fe..ced98f03f5439 100644 --- a/src/server/game/AI/NpcBots/botcommands.cpp +++ b/src/server/game/AI/NpcBots/botcommands.cpp @@ -516,6 +516,7 @@ class script_bot_commands : public CommandScript static ChatCommandTable npcbotOrderCommandTable = { { "cast", HandleNpcBotOrderCastCommand, rbac::RBAC_PERM_COMMAND_NPCBOT_ORDER_CAST, Console::No }, + { "pull", HandleNpcBotOrderPullCommand, rbac::RBAC_PERM_COMMAND_NPCBOT_ORDER_CAST, Console::No }, }; static ChatCommandTable npcbotVehicleCommandTable = @@ -1703,6 +1704,155 @@ class script_bot_commands : public CommandScript return true; } + static bool HandleNpcBotOrderPullCommand(ChatHandler* handler, Optional bot_name, Optional target_token) + { + Player* owner = handler->GetSession()->GetPlayer(); + if (!owner->HaveBot() || !bot_name) + { + handler->SendSysMessage(".npcbot order pull #bot_name #[target_token]"); + handler->SendSysMessage("Orders bot to pull target immediately"); + return true; + } + + if (owner->GetBotMgr()->IsPartyInCombat()) + { + handler->SendSysMessage("Can't do that while in combat!"); + return true; + } + + for (std::decay_t::size_type i = 0u; i < bot_name->size(); ++i) + if ((*bot_name)[i] == '_') + (*bot_name)[i] = ' '; + + Creature* bot = owner->GetBotMgr()->GetBotByName(*bot_name); + if (bot) + { + if (!bot->IsInWorld()) + { + handler->PSendSysMessage("Bot %s is not found!", bot_name->c_str()); + return true; + } + if (!bot->IsAlive()) + { + handler->PSendSysMessage("%s is dead!", bot->GetName().c_str()); + return true; + } + if (!bot->GetBotAI()->HasRole(BOT_ROLE_DPS) || bot->GetVictim() || bot->IsInCombat() || !bot->getAttackers().empty()) + { + handler->PSendSysMessage("%s cannot pull target! Must be idle and have DPS role", bot->GetName().c_str()); + return true; + } + } + else + { + auto const& class_name = *bot_name; + for (auto const c : class_name) + { + if (!std::islower(c)) + { + handler->SendSysMessage("Bot class name must be in lower case!"); + return true; + } + } + + uint8 bot_class = BotMgr::BotClassByClassName(class_name); + if (bot_class == BOT_CLASS_NONE) + { + handler->PSendSysMessage("Unknown bot name or class %s!", class_name.c_str()); + return true; + } + + std::list cBots = owner->GetBotMgr()->GetAllBotsByClass(bot_class); + + if (cBots.empty()) + { + handler->PSendSysMessage("No bots of class %u found!", bot_class); + return true; + } + + bot = cBots.size() == 1 ? cBots.front() : Trinity::Containers::SelectRandomContainerElement(cBots); + + if (!bot) + { + handler->SendSysMessage("None of %u found bots can use pull yet!", cBots.size()); + return true; + } + } + + ObjectGuid target_guid = ObjectGuid::Empty; + bool token_valid = true; + if (!target_token || target_token == "mytarget") + target_guid = owner->GetTarget(); + else if (Group const* group = owner->GetGroup()) + { + if (target_token == "star") + target_guid = group->GetTargetIcons()[0]; + else if (target_token == "circle") + target_guid = group->GetTargetIcons()[1]; + else if (target_token == "diamond") + target_guid = group->GetTargetIcons()[2]; + else if (target_token == "triangle") + target_guid = group->GetTargetIcons()[3]; + else if (target_token == "moon") + target_guid = group->GetTargetIcons()[4]; + else if (target_token == "square") + target_guid = group->GetTargetIcons()[5]; + else if (target_token == "cross") + target_guid = group->GetTargetIcons()[6]; + else if (target_token == "skull") + target_guid = group->GetTargetIcons()[7]; + else if (target_token->size() == 1u && std::isdigit(target_token->front())) + { + uint8 digit = static_cast(std::stoi(std::string(*target_token))); + switch (digit) + { + case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: + target_guid = group->GetTargetIcons()[digit - 1]; + break; + default: + token_valid = false; + break; + } + } + else + token_valid = false; + } + else + token_valid = false; + + if (!token_valid) + { + handler->PSendSysMessage("Invalid target token '%s'!", *target_token); + handler->SendSysMessage("Valid target tokens:\n '','mytarget', " + "'star','1', 'circle','2', 'diamond','3', 'triangle','4', 'moon','5', 'square','6', 'cross','7', 'skull','8'" + "\nNote that target icons tokens are only available while in group"); + return true; + } + + Unit* target = target_guid ? ObjectAccessor::GetUnit(*owner, target_guid) : nullptr; + if (!target || !bot->FindMap() || target->FindMap() != bot->FindMap()) + { + handler->PSendSysMessage("Invalid target '%s'!", target ? target->GetName().c_str() : "unknown"); + return true; + } + + bot_ai::BotOrder order(BOT_ORDER_PULL); + order.params.pullParams.targetGuid = target_guid.GetRawValue(); + + if (bot->GetBotAI()->AddOrder(std::move(order))) + { + if (DEBUG_BOT_ORDERS) + handler->PSendSysMessage("Order given: %s: pull %s", bot->GetName().c_str(), target ? target->GetName().c_str() : "unknown"); + } + else + { + if (DEBUG_BOT_ORDERS) + handler->PSendSysMessage("Order failed: %s: pull %s", bot->GetName().c_str(), target ? target->GetName().c_str() : "unknown"); + } + + return true; + } + static bool HandleNpcBotOrderCastCommand(ChatHandler* handler, Optional bot_name, Optional spell_name, Optional target_token) { Player* owner = handler->GetSession()->GetPlayer(); diff --git a/src/server/game/AI/NpcBots/botcommon.h b/src/server/game/AI/NpcBots/botcommon.h index 9088aad65a6ea..4e3248156b3de 100644 --- a/src/server/game/AI/NpcBots/botcommon.h +++ b/src/server/game/AI/NpcBots/botcommon.h @@ -565,7 +565,10 @@ constexpr size_t MAX_SEND_POINTS = 5u; enum BotOrderTypes { BOT_ORDER_NONE = 0, - BOT_ORDER_SPELLCAST = 1 + BOT_ORDER_SPELLCAST = 1, + BOT_ORDER_PULL = 2, + + BOT_ORDER_END }; constexpr bool DEBUG_BOT_ORDERS = false; constexpr size_t MAX_BOT_ORDERS_QUEUE_SIZE = 3u; diff --git a/src/server/game/AI/NpcBots/bpet_ai.cpp b/src/server/game/AI/NpcBots/bpet_ai.cpp index 6deb1b4ca208f..7ec4a93f39f5f 100644 --- a/src/server/game/AI/NpcBots/bpet_ai.cpp +++ b/src/server/game/AI/NpcBots/bpet_ai.cpp @@ -1589,6 +1589,8 @@ bool bot_pet_ai::CheckAttackTarget() return false; } + if (petOwner->GetBotAI()->IsLastOrder(BOT_ORDER_PULL, 0, opponent->GetGUID())) + return false; if (reset) SetBotCommandState(BOT_COMMAND_COMBATRESET);//reset AttackStart()