diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index ccb392e1aef..88920a05041 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1038,6 +1038,7 @@ AiPlayerbot.EnableNewRpgStrategy = 1 # DoQuest (Default: 60 Select quest from the quest log and head to the location to attempt completion​) # TravelFlight (Default: 15 Go to the nearest flightmaster and fly to a level-appropriate area) # Rest (Default: 5 Take a break for a while and do nothing) +# OutdoorPvp (Default: 10 Participate in outdoor PvP capture points if already in an outdoor PvP zone) AiPlayerbot.RpgStatusProbWeight.WanderRandom = 15 AiPlayerbot.RpgStatusProbWeight.WanderNpc = 20 AiPlayerbot.RpgStatusProbWeight.GoGrind = 15 @@ -1045,6 +1046,7 @@ AiPlayerbot.RpgStatusProbWeight.GoCamp = 10 AiPlayerbot.RpgStatusProbWeight.DoQuest = 60 AiPlayerbot.RpgStatusProbWeight.TravelFlight = 15 AiPlayerbot.RpgStatusProbWeight.Rest = 5 +AiPlayerbot.RpgStatusProbWeight.OutdoorPvp = 10 # Bots' minimum and maximum level when teleporting in and out of a zone, according to the new RPG strategy # Format: AiPlayerbot.ZoneBracket.zoneID = minLevel,maxLevel diff --git a/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql b/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql new file mode 100644 index 00000000000..3d356a2a2ba --- /dev/null +++ b/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql @@ -0,0 +1,240 @@ +-- ######################################################### +-- Playerbots - Add focus heal command texts +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'focus_heal_not_healer', + 'focus_heal_provide_names', + 'focus_heal_no_targets', + 'focus_heal_current_targets', + 'focus_heal_cleared', + 'focus_heal_add_remove_syntax', + 'focus_heal_not_in_group', + 'focus_heal_not_in_group_with', + 'focus_heal_added', + 'focus_heal_removed' +); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'focus_heal_not_healer', + 'focus_heal_provide_names', + 'focus_heal_no_targets', + 'focus_heal_current_targets', + 'focus_heal_cleared', + 'focus_heal_add_remove_syntax', + 'focus_heal_not_in_group', + 'focus_heal_not_in_group_with', + 'focus_heal_added', + 'focus_heal_removed' +); + +-- focus_heal_not_healer +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1745, + 'focus_heal_not_healer', + 'I''m not a healer or offhealer (please change my strats to heal or offheal)', + 0, 0, + '저는 힐러나 오프힐러가 아닙니다 (전략을 heal 또는 offheal로 변경해주세요)', + 'Je ne suis pas un soigneur ou un soigneur secondaire (veuillez changer mes strats en heal ou offheal)', + 'Ich bin kein Heiler oder Nebenheiler (bitte ändere meine Strategien auf heal oder offheal)', + '我不是治疗者或副治疗者(请将我的策略更改为 heal 或 offheal)', + '我不是治療者或副治療者(請將我的策略更改為 heal 或 offheal)', + 'No soy un sanador ni un sanador secundario (por favor cambia mis estrategias a heal o offheal)', + 'No soy un sanador ni un sanador secundario (por favor cambia mis estrategias a heal o offheal)', + 'Я не лекарь и не побочный лекарь (пожалуйста, измените мои стратегии на heal или offheal)'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_healer', 100); + +-- focus_heal_provide_names +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1746, + 'focus_heal_provide_names', + 'Please provide one or more player names', + 0, 0, + '하나 이상의 플레이어 이름을 제공해주세요', + 'Veuillez fournir un ou plusieurs noms de joueurs', + 'Bitte geben Sie einen oder mehrere Spielernamen an', + '请提供一个或多个玩家名称', + '請提供一個或多個玩家名稱', + 'Por favor proporciona uno o más nombres de jugadores', + 'Por favor proporciona uno o más nombres de jugadores', + 'Пожалуйста, укажите одно или несколько имён игроков'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_provide_names', 100); + +-- focus_heal_no_targets +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1747, + 'focus_heal_no_targets', + 'I don''t have any focus heal targets', + 0, 0, + '지정된 집중 치유 대상이 없습니다', + 'Je n''ai aucune cible de soin prioritaire', + 'Ich habe keine fokussierten Heilziele', + '我没有任何集中治疗目标', + '我沒有任何集中治療目標', + 'No tengo ningún objetivo de sanación prioritario', + 'No tengo ningún objetivo de sanación prioritario', + 'У меня нет целей приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_no_targets', 100); + +-- focus_heal_current_targets: %targets is replaced with comma-separated player names +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1748, + 'focus_heal_current_targets', + 'My focus heal targets are %targets', + 0, 0, + '나의 집중 치유 대상: %targets', + 'Mes cibles de soin prioritaire sont %targets', + 'Meine fokussierten Heilziele sind %targets', + '我的集中治疗目标是 %targets', + '我的集中治療目標是 %targets', + 'Mis objetivos de sanación prioritarios son %targets', + 'Mis objetivos de sanación prioritarios son %targets', + 'Мои цели приоритетного лечения: %targets'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_current_targets', 100); + +-- focus_heal_cleared +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1749, + 'focus_heal_cleared', + 'Removed focus heal targets', + 0, 0, + '집중 치유 대상을 제거했습니다', + 'Cibles de soin prioritaire supprimées', + 'Fokussierte Heilziele entfernt', + '已移除集中治疗目标', + '已移除集中治療目標', + 'Objetivos de sanación prioritarios eliminados', + 'Objetivos de sanación prioritarios eliminados', + 'Цели приоритетного лечения удалены'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_cleared', 100); + +-- focus_heal_add_remove_syntax +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1750, + 'focus_heal_add_remove_syntax', + 'Please specify a + for add or - to remove a target', + 0, 0, + '대상을 추가하려면 +, 제거하려면 -를 지정해주세요', + 'Veuillez spécifier + pour ajouter ou - pour retirer une cible', + 'Bitte geben Sie + zum Hinzufügen oder - zum Entfernen eines Ziels an', + '请指定 + 添加或 - 移除目标', + '請指定 + 添加或 - 移除目標', + 'Por favor especifica + para agregar o - para eliminar un objetivo', + 'Por favor especifica + para agregar o - para eliminar un objetivo', + 'Пожалуйста, укажите + для добавления или - для удаления цели'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_add_remove_syntax', 100); + +-- focus_heal_not_in_group +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1751, + 'focus_heal_not_in_group', + 'I''m not in a group', + 0, 0, + '저는 파티에 속해있지 않습니다', + 'Je ne suis pas dans un groupe', + 'Ich bin in keiner Gruppe', + '我不在队伍中', + '我不在隊伍中', + 'No estoy en un grupo', + 'No estoy en un grupo', + 'Я не в группе'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_in_group', 100); + +-- focus_heal_not_in_group_with: %player_name is replaced with the target player's name +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1752, + 'focus_heal_not_in_group_with', + 'I''m not in a group with %player_name', + 0, 0, + '%player_name 와(과) 같은 파티에 없습니다', + 'Je ne suis pas dans un groupe avec %player_name', + 'Ich bin nicht in einer Gruppe mit %player_name', + '我与 %player_name 不在同一队伍中', + '我與 %player_name 不在同一隊伍中', + 'No estoy en un grupo con %player_name', + 'No estoy en un grupo con %player_name', + 'Я не в группе с %player_name'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_in_group_with', 100); + +-- focus_heal_added: %player_name is replaced with the added player's name +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1753, + 'focus_heal_added', + 'Added %player_name to focus heal targets', + 0, 0, + '%player_name 을(를) 집중 치유 대상에 추가했습니다', + '%player_name ajouté aux cibles de soin prioritaire', + '%player_name zu den fokussierten Heilzielen hinzugefügt', + '已将 %player_name 添加到集中治疗目标', + '已將 %player_name 添加到集中治療目標', + '%player_name agregado a los objetivos de sanación prioritarios', + '%player_name agregado a los objetivos de sanación prioritarios', + '%player_name добавлен в цели приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_added', 100); + +-- focus_heal_removed: %player_name is replaced with the removed player's name +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1754, + 'focus_heal_removed', + 'Removed %player_name from focus heal targets', + 0, 0, + '%player_name 을(를) 집중 치유 대상에서 제거했습니다', + '%player_name retiré des cibles de soin prioritaire', + '%player_name aus den fokussierten Heilzielen entfernt', + '已将 %player_name 从集中治疗目标中移除', + '已將 %player_name 從集中治療目標中移除', + '%player_name eliminado de los objetivos de sanación prioritarios', + '%player_name eliminado de los objetivos de sanación prioritarios', + '%player_name удалён из целей приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_removed', 100); diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 41763be90f2..79b5b498581 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -63,6 +63,7 @@ #include "WorldBuffAction.h" #include "XpGainAction.h" #include "NewRpgAction.h" +#include "NewRpgOutdoorPvP.h" #include "FishingAction.h" #include "CancelChannelAction.h" #include "WaitForAttackAction.h" @@ -265,6 +266,7 @@ class ActionContext : public NamedObjectContext creators["new rpg wander npc"] = &ActionContext::new_rpg_wander_npc; creators["new rpg do quest"] = &ActionContext::new_rpg_do_quest; creators["new rpg travel flight"] = &ActionContext::new_rpg_travel_flight; + creators["new rpg outdoor pvp"] = &ActionContext::new_rpg_outdoor_pvp; creators["wait for attack keep safe distance"] = &ActionContext::wait_for_attack_keep_safe_distance; } @@ -462,6 +464,7 @@ class ActionContext : public NamedObjectContext static Action* new_rpg_wander_npc(PlayerbotAI* ai) { return new NewRpgWanderNpcAction(ai); } static Action* new_rpg_do_quest(PlayerbotAI* ai) { return new NewRpgDoQuestAction(ai); } static Action* new_rpg_travel_flight(PlayerbotAI* ai) { return new NewRpgTravelFlightAction(ai); } + static Action* new_rpg_outdoor_pvp(PlayerbotAI* ai) { return new NewRpgOutdoorPvpAction(ai); } static Action* wait_for_attack_keep_safe_distance(PlayerbotAI* ai) { return new WaitForAttackKeepSafeDistanceAction(ai); } }; diff --git a/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp b/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp new file mode 100644 index 00000000000..d987bdd7385 --- /dev/null +++ b/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "SetFocusHealTargetsAction.h" + +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "PlayerbotTextMgr.h" + +#include +#include + +static std::string LowercaseString(std::string const& str) +{ + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +static Player* FindGroupPlayerByName(Player* player, std::string const& playerName) +{ + if (!player) + return nullptr; + + Group* group = player->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (member) + { + std::string memberName = member->GetName(); + if (LowercaseString(memberName) == playerName) + return member; + } + } + + return nullptr; +} + +bool SetFocusHealTargetsAction::Execute(Event event) +{ + if (!botAI->IsHeal(bot) && !botAI->HasStrategy("offheal", BOT_STATE_COMBAT)) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_healer", + "I'm not a healer or offhealer (please change my strats to heal or offheal)", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + std::string const param = LowercaseString(event.getParam()); + if (param.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_provide_names", + "Please provide one or more player names", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + std::list focusHealTargets = + AI_VALUE(std::list, "focus heal targets"); + + // Query current focus targets + if (param.find('?') != std::string::npos) + { + if (focusHealTargets.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_no_targets", + "I don't have any focus heal targets", + {}); + botAI->TellMasterNoFacing(text); + } + else + { + std::stringstream targetNames; + for (auto it = focusHealTargets.begin(); it != focusHealTargets.end(); ++it) + { + Unit* target = botAI->GetUnit(*it); + if (target) + { + if (it != focusHealTargets.begin()) + targetNames << ", "; + targetNames << target->GetName(); + } + } + + std::map placeholders; + placeholders["%targets"] = targetNames.str(); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_current_targets", + "My focus heal targets are %targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + + return true; + } + + // Clear all targets + if (param == "none" || param == "unset" || param == "clear") + { + focusHealTargets.clear(); + SET_AI_VALUE(std::list, "focus heal targets", focusHealTargets); + botAI->ChangeStrategy("-focus heal targets", BOT_STATE_COMBAT); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_cleared", + "Removed focus heal targets", + {}); + botAI->TellMasterNoFacing(text); + return true; + } + + // Parse multiple targets separated by commas + std::vector targetNames; + if (param.find(',') != std::string::npos) + { + std::string targetName; + std::stringstream ss(param); + while (std::getline(ss, targetName, ',')) + targetNames.push_back(targetName); + } + else + targetNames.push_back(param); + + if (targetNames.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_provide_names", + "Please provide one or more player names", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + if (!bot->GetGroup()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_in_group", + "I'm not in a group", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + for (std::string const& targetName : targetNames) + { + bool const add = targetName.find("+") != std::string::npos; + bool const remove = targetName.find("-") != std::string::npos; + if (!add && !remove) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_add_remove_syntax", + "Please specify a + for add or - to remove a target", + {}); + botAI->TellMasterNoFacing(text); + continue; + } + + std::string const playerName = targetName.substr(1); + Player* target = FindGroupPlayerByName(bot, playerName); + if (!target) + { + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_in_group_with", + "I'm not in a group with %player_name", + placeholders); + botAI->TellMasterNoFacing(text); + continue; + } + + ObjectGuid const& targetGuid = target->GetGUID(); + if (add) + { + if (std::find(focusHealTargets.begin(), focusHealTargets.end(), targetGuid) == + focusHealTargets.end()) + focusHealTargets.push_back(targetGuid); + + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_added", + "Added %player_name to focus heal targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + else + { + focusHealTargets.remove(targetGuid); + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_removed", + "Removed %player_name from focus heal targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + } + + SET_AI_VALUE(std::list, "focus heal targets", focusHealTargets); + + if (focusHealTargets.empty()) + botAI->ChangeStrategy("-focus heal targets", BOT_STATE_COMBAT); + else + botAI->ChangeStrategy("+focus heal targets", BOT_STATE_COMBAT); + + return true; +} diff --git a/src/Ai/Base/Actions/SetFocusHealTargetsAction.h b/src/Ai/Base/Actions/SetFocusHealTargetsAction.h new file mode 100644 index 00000000000..92d5c0b1983 --- /dev/null +++ b/src/Ai/Base/Actions/SetFocusHealTargetsAction.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_SETFOCUSHEALTARGETSACTION_H +#define _PLAYERBOT_SETFOCUSHEALTARGETSACTION_H + +#include "Action.h" + +class PlayerbotAI; + +class SetFocusHealTargetsAction : public Action +{ +public: + SetFocusHealTargetsAction(PlayerbotAI* botAI) : Action(botAI, "focus heal targets") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index c3a6b3d0219..6f11fb33c64 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -37,6 +37,7 @@ #include "LogLevelAction.h" #include "LootStrategyAction.h" #include "LootRollAction.h" +#include "SetFocusHealTargetsAction.h" #include "MailAction.h" #include "NamedObjectContext.h" #include "NewRpgAction.h" @@ -201,6 +202,7 @@ class ChatActionContext : public NamedObjectContext creators["pet attack"] = &ChatActionContext::pet_attack; creators["roll"] = &ChatActionContext::roll_action; creators["wait for attack time"] = &ChatActionContext::wait_for_attack_time; + creators["focus heal targets"] = &ChatActionContext::focus_heal_targets; } private: @@ -314,6 +316,7 @@ class ChatActionContext : public NamedObjectContext static Action* pet_attack(PlayerbotAI* botAI) { return new PetsAction(botAI, "attack"); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } static Action* wait_for_attack_time(PlayerbotAI* botAI) { return new SetWaitForAttackTimeAction(botAI); } + static Action* focus_heal_targets(PlayerbotAI* botAI) { return new SetFocusHealTargetsAction(botAI); } }; #endif diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 6bdfcdc4134..40316bd62a3 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -146,6 +146,7 @@ class ChatTriggerContext : public NamedObjectContext creators["pet attack"] = &ChatTriggerContext::pet_attack; creators["roll"] = &ChatTriggerContext::roll_action; creators["wait for attack time"] = &ChatTriggerContext::wait_for_attack_time; + creators["focus heal"] = &ChatTriggerContext::focus_heal; } private: @@ -271,6 +272,7 @@ class ChatTriggerContext : public NamedObjectContext static Trigger* pet_attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet attack"); } static Trigger* roll_action(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "roll"); } static Trigger* wait_for_attack_time(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "wait for attack time"); } + static Trigger* focus_heal(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "focus heal"); } }; #endif diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index e13b57dd10c..77a8d9d0b2d 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -107,6 +107,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back(new TriggerNode("pet", { NextAction("pet", relevance) })); triggers.push_back(new TriggerNode("pet attack", { NextAction("pet attack", relevance) })); triggers.push_back(new TriggerNode("roll", { NextAction("roll", relevance) })); + triggers.push_back(new TriggerNode("focus heal", { NextAction("focus heal targets", relevance) })); } ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) @@ -200,4 +201,5 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("pet"); supported.push_back("pet attack"); supported.push_back("wait for attack time"); + supported.push_back("focus heal"); } diff --git a/src/Ai/Base/Strategy/FocusTargetStrategy.h b/src/Ai/Base/Strategy/FocusTargetStrategy.h new file mode 100644 index 00000000000..9af35c66653 --- /dev/null +++ b/src/Ai/Base/Strategy/FocusTargetStrategy.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_FOCUSTARGETSTRATEGY_H +#define _PLAYERBOT_FOCUSTARGETSTRATEGY_H + +#include "Strategy.h" + +class PlayerbotAI; + +class FocusHealTargetsStrategy : public Strategy +{ +public: + FocusHealTargetsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + std::string const getName() override { return "focus heal targets"; } +}; + +#endif diff --git a/src/Ai/Base/StrategyContext.h b/src/Ai/Base/StrategyContext.h index c16573fa071..5386e872bfd 100644 --- a/src/Ai/Base/StrategyContext.h +++ b/src/Ai/Base/StrategyContext.h @@ -19,6 +19,7 @@ #include "DuelStrategy.h" #include "EmoteStrategy.h" #include "FleeStrategy.h" +#include "FocusTargetStrategy.h" #include "FollowMasterStrategy.h" #include "GrindingStrategy.h" #include "GroupStrategy.h" @@ -126,6 +127,7 @@ class StrategyContext : public NamedObjectContext creators["use bobber"] = &StrategyContext::bobber_strategy; creators["master fishing"] = &StrategyContext::master_fishing; creators["wait for attack"] = &StrategyContext::wait_for_attack; + creators["focus heal targets"] = &StrategyContext::focus_heal_targets; } private: @@ -198,6 +200,7 @@ class StrategyContext : public NamedObjectContext static Strategy* bobber_strategy(PlayerbotAI* botAI) { return new UseBobberStrategy(botAI); } static Strategy* master_fishing(PlayerbotAI* botAI) { return new MasterFishingStrategy(botAI); } static Strategy* wait_for_attack(PlayerbotAI* botAI) { return new WaitForAttackStrategy(botAI); } + static Strategy* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetsStrategy(botAI); } }; class MovementStrategyContext : public NamedObjectContext diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index c77df3a31ba..bfdddecf7a3 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -230,6 +230,7 @@ class TriggerContext : public NamedObjectContext creators["wander npc status"] = &TriggerContext::wander_npc_status; creators["do quest status"] = &TriggerContext::do_quest_status; creators["travel flight status"] = &TriggerContext::travel_flight_status; + creators["outdoor pvp status"] = &TriggerContext::outdoor_pvp_status; creators["can self resurrect"] = &TriggerContext::can_self_resurrect; creators["can fish"] = &TriggerContext::can_fish; creators["can use fishing bobber"] = &TriggerContext::can_use_fishing_bobber; @@ -436,6 +437,7 @@ class TriggerContext : public NamedObjectContext static Trigger* wander_npc_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_WANDER_NPC); } static Trigger* do_quest_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_DO_QUEST); } static Trigger* travel_flight_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_TRAVEL_FLIGHT); } + static Trigger* outdoor_pvp_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_OUTDOOR_PVP); } static Trigger* can_self_resurrect(PlayerbotAI* ai) { return new SelfResurrectTrigger(ai); } static Trigger* can_fish(PlayerbotAI* ai) { return new CanFishTrigger(ai); } static Trigger* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberTrigger(ai); } diff --git a/src/Ai/Base/Value/PartyMemberToHeal.cpp b/src/Ai/Base/Value/PartyMemberToHeal.cpp index b49c1cd4196..9845bc11912 100644 --- a/src/Ai/Base/Value/PartyMemberToHeal.cpp +++ b/src/Ai/Base/Value/PartyMemberToHeal.cpp @@ -38,6 +38,36 @@ Unit* PartyMemberToHeal::Calculate() bool isRaid = bot->GetGroup()->isRaidGroup(); MinValueCalculator calc(100); + // If focus heal targets strategy is active, only heal those targets + if (botAI->HasStrategy("focus heal targets", BOT_STATE_COMBAT)) + { + std::list const focusHealTargets = + AI_VALUE(std::list, "focus heal targets"); + + for (ObjectGuid const& focusHealTarget : focusHealTargets) + { + Player* player = ObjectAccessor::FindPlayer(focusHealTarget); + if (!player || !player->IsInWorld() || !player->IsAlive() || !player->IsInSameGroupWith(bot)) + continue; + + float health = player->GetHealthPct(); + if (isRaid || health < sPlayerbotAIConfig.mediumHealth || + !IsTargetOfSpellCast(player, predicate)) + { + float probeValue = 100.0f; + if (player->GetDistance2d(bot) > sPlayerbotAIConfig.healDistance) + probeValue = health + 30.0f; + else + probeValue = health + player->GetDistance2d(bot) / 10.0f; + + if (probeValue < calc.minValue && Check(player)) + calc.probe(probeValue, player); + } + } + + return (Unit*)calc.param; + } + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* player = gref->GetSource(); @@ -45,17 +75,17 @@ Unit* PartyMemberToHeal::Calculate() continue; if (player && player->IsAlive()) { - uint8 health = player->GetHealthPct(); + float health = player->GetHealthPct(); if (isRaid || health < sPlayerbotAIConfig.mediumHealth || !IsTargetOfSpellCast(player, predicate)) { - uint32 probeValue = 100; + float probeValue = 100.0f; if (player->GetDistance2d(bot) > sPlayerbotAIConfig.healDistance) { - probeValue = health + 30; + probeValue = health + 30.0f; } else { - probeValue = health + player->GetDistance2d(bot) / 10; + probeValue = health + player->GetDistance2d(bot) / 10.0f; } // delay Check player to here for better performance if (probeValue < calc.minValue && Check(player)) @@ -68,10 +98,10 @@ Unit* PartyMemberToHeal::Calculate() Pet* pet = player->GetPet(); if (pet && pet->IsAlive()) { - uint8 health = ((Unit*)pet)->GetHealthPct(); - uint32 probeValue = 100; + float health = ((Unit*)pet)->GetHealthPct(); + float probeValue = 100.0f; if (isRaid || health < sPlayerbotAIConfig.mediumHealth) - probeValue = health + 30; + probeValue = health + 30.0f; // delay Check pet to here for better performance if (probeValue < calc.minValue && Check(pet)) { @@ -82,10 +112,10 @@ Unit* PartyMemberToHeal::Calculate() Unit* charm = player->GetCharm(); if (charm && charm->IsAlive()) { - uint8 health = charm->GetHealthPct(); - uint32 probeValue = 100; + float health = charm->GetHealthPct(); + float probeValue = 100.0f; if (isRaid || health < sPlayerbotAIConfig.mediumHealth) - probeValue = health + 30; + probeValue = health + 30.0f; // delay Check charm to here for better performance if (probeValue < calc.minValue && Check(charm)) { diff --git a/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp b/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp index f2b6aef105d..bf9e9d68950 100644 --- a/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp +++ b/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp @@ -13,6 +13,7 @@ #include "ServerFacade.h" #include "SharedDefines.h" #include "NearestGameObjects.h" +#include std::vector PossibleRpgTargetsValue::allowedNpcFlags; @@ -88,8 +89,11 @@ bool PossibleRpgTargetsValue::AcceptUnit(Unit* unit) std::vector PossibleNewRpgTargetsValue::allowedNpcFlags; +// Sparse starting zones where the default scan range is insufficient for WANDER_NPC (requires >= 3 NPCs) +static const std::unordered_set rpgRangeOverrideAreaIds = { 3526 /* Ammen Vale */, 2117 /* Deathknell */ }; + PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float range) - : NearestUnitsValue(botAI, "possible new rpg targets", range, true) + : NearestUnitsValue(botAI, "possible new rpg targets", range, true), defaultRange(range) { if (allowedNpcFlags.empty()) { @@ -119,6 +123,11 @@ PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float GuidVector PossibleNewRpgTargetsValue::Calculate() { + if (rpgRangeOverrideAreaIds.count(bot->GetAreaId()) && defaultRange < 200.0f) + range = 200.0f; + else + range = defaultRange; + std::list targets; FindUnits(targets); diff --git a/src/Ai/Base/Value/PossibleRpgTargetsValue.h b/src/Ai/Base/Value/PossibleRpgTargetsValue.h index ae5b596a0d8..00e5f801293 100644 --- a/src/Ai/Base/Value/PossibleRpgTargetsValue.h +++ b/src/Ai/Base/Value/PossibleRpgTargetsValue.h @@ -35,6 +35,8 @@ class PossibleNewRpgTargetsValue : public NearestUnitsValue protected: void FindUnits(std::list& targets) override; bool AcceptUnit(Unit* unit) override; +private: + float defaultRange; }; class PossibleNewRpgGameObjectsValue : public ObjectGuidListCalculatedValue diff --git a/src/Ai/Base/Value/TargetValue.h b/src/Ai/Base/Value/TargetValue.h index e9e6cdea4db..7d766578a53 100644 --- a/src/Ai/Base/Value/TargetValue.h +++ b/src/Ai/Base/Value/TargetValue.h @@ -140,4 +140,11 @@ class BossTargetValue : public TargetValue, public Qualified public: Unit* Calculate(); }; + +class FocusHealTargetValue : public ManualSetValue> +{ +public: + FocusHealTargetValue(PlayerbotAI* botAI) : ManualSetValue>(botAI, {}, "focus heal targets") {} +}; + #endif diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index 14cbd185c24..77d25e060ff 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -241,6 +241,7 @@ class ValueContext : public NamedObjectContext creators["travel target"] = &ValueContext::travel_target; creators["talk target"] = &ValueContext::talk_target; creators["pull target"] = &ValueContext::pull_target; + creators["focus heal targets"] = &ValueContext::focus_heal_targets; creators["group"] = &ValueContext::group; creators["range"] = &ValueContext::range; creators["inside target"] = &ValueContext::inside_target; @@ -497,6 +498,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* next_rpg_action(PlayerbotAI* botAI) { return new NextRpgActionValue(botAI); } static UntypedValue* travel_target(PlayerbotAI* botAI) { return new TravelTargetValue(botAI); } static UntypedValue* pull_target(PlayerbotAI* botAI) { return new PullTargetValue(botAI); } + static UntypedValue* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetValue(botAI); } static UntypedValue* bg_master(PlayerbotAI* botAI) { return new BgMasterValue(botAI); } static UntypedValue* bg_role(PlayerbotAI* botAI) { return new BgRoleValue(botAI); } diff --git a/src/Ai/Class/Druid/Action/DruidActions.cpp b/src/Ai/Class/Druid/Action/DruidActions.cpp index d6a950b175f..2eb809480f4 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.cpp +++ b/src/Ai/Class/Druid/Action/DruidActions.cpp @@ -23,6 +23,16 @@ std::vector CastAbolishPoisonOnPartyAction::getAlternatives() CastSpellAction::getPrerequisites()); } +bool CastLifebloomOnMainTankAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target || !target->IsAlive() || !CastSpellAction::isUseful()) + return false; + + Aura* lifebloom = botAI->GetAura("lifebloom", target, true, true); + return !lifebloom || lifebloom->GetStackAmount() < 3 || lifebloom->GetDuration() < 2000; +} + Value* CastEntanglingRootsCcAction::GetTargetValue() { return context->GetValue("cc target", "entangling roots"); diff --git a/src/Ai/Class/Druid/Action/DruidActions.h b/src/Ai/Class/Druid/Action/DruidActions.h index e3e177590d4..016c3dfc433 100644 --- a/src/Ai/Class/Druid/Action/DruidActions.h +++ b/src/Ai/Class/Druid/Action/DruidActions.h @@ -128,6 +128,14 @@ class CastThornsOnMainTankAction : public BuffOnMainTankAction CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {} }; +class CastLifebloomOnMainTankAction : public BuffOnMainTankAction +{ +public: + CastLifebloomOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "lifebloom", true) {} + + bool isUseful() override; +}; + class CastOmenOfClarityAction : public CastBuffSpellAction { public: diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 17b561d19f4..3d9086cffc9 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -79,6 +79,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext DruidTriggerFactoryInternal() { creators["omen of clarity"] = &DruidTriggerFactoryInternal::omen_of_clarity; + creators["clearcasting"] = &DruidTriggerFactoryInternal::clearcasting; creators["thorns"] = &DruidTriggerFactoryInternal::thorns; creators["thorns on party"] = &DruidTriggerFactoryInternal::thorns_on_party; creators["thorns on main tank"] = &DruidTriggerFactoryInternal::thorns_on_main_tank; @@ -117,6 +118,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext private: static Trigger* natures_swiftness(PlayerbotAI* botAI) { return new NaturesSwiftnessTrigger(botAI); } + static Trigger* clearcasting(PlayerbotAI* botAI) { return new ClearcastingTrigger(botAI); } static Trigger* eclipse_solar(PlayerbotAI* botAI) { return new EclipseSolarTrigger(botAI); } static Trigger* eclipse_lunar(PlayerbotAI* botAI) { return new EclipseLunarTrigger(botAI); } static Trigger* thorns(PlayerbotAI* botAI) { return new ThornsTrigger(botAI); } @@ -209,6 +211,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext creators["thorns"] = &DruidAiObjectContextInternal::thorns; creators["thorns on party"] = &DruidAiObjectContextInternal::thorns_on_party; creators["thorns on main tank"] = &DruidAiObjectContextInternal::thorns_on_main_tank; + creators["lifebloom on main tank"] = &DruidAiObjectContextInternal::lifebloom_on_main_tank; creators["cure poison"] = &DruidAiObjectContextInternal::cure_poison; creators["cure poison on party"] = &DruidAiObjectContextInternal::cure_poison_on_party; creators["abolish poison"] = &DruidAiObjectContextInternal::abolish_poison; @@ -304,6 +307,7 @@ class DruidAiObjectContextInternal : public NamedObjectContext static Action* thorns(PlayerbotAI* botAI) { return new CastThornsAction(botAI); } static Action* thorns_on_party(PlayerbotAI* botAI) { return new CastThornsOnPartyAction(botAI); } static Action* thorns_on_main_tank(PlayerbotAI* botAI) { return new CastThornsOnMainTankAction(botAI); } + static Action* lifebloom_on_main_tank(PlayerbotAI* botAI) { return new CastLifebloomOnMainTankAction(botAI); } static Action* cure_poison(PlayerbotAI* botAI) { return new CastCurePoisonAction(botAI); } static Action* cure_poison_on_party(PlayerbotAI* botAI) { return new CastCurePoisonOnPartyAction(botAI); } static Action* abolish_poison(PlayerbotAI* botAI) { return new CastAbolishPoisonAction(botAI); } diff --git a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp index 7529cbb8ea7..0710e0fdc1e 100644 --- a/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp @@ -56,6 +56,9 @@ void HealDruidStrategy::InitTriggers(std::vector& triggers) new TriggerNode("party member critical health", { NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4) })); + triggers.push_back(new TriggerNode("clearcasting", + { NextAction("lifebloom on main tank", ACTION_CRITICAL_HEAL - 1) })); + triggers.push_back(new TriggerNode( "group heal setting", { diff --git a/src/Ai/Class/Druid/Trigger/DruidTriggers.h b/src/Ai/Class/Druid/Trigger/DruidTriggers.h index adad0d2a3cf..01daaeba65a 100644 --- a/src/Ai/Class/Druid/Trigger/DruidTriggers.h +++ b/src/Ai/Class/Druid/Trigger/DruidTriggers.h @@ -61,6 +61,12 @@ class OmenOfClarityTrigger : public BuffTrigger OmenOfClarityTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "omen of clarity") {} }; +class ClearcastingTrigger : public HasAuraTrigger +{ +public: + ClearcastingTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "clearcasting") {} +}; + class RakeTrigger : public DebuffTrigger { public: diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index ddd1240da22..ca0ca243360 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -62,7 +62,7 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) { case RPG_IDLE: return RandomChangeStatus({RPG_GO_CAMP, RPG_GO_GRIND, RPG_WANDER_RANDOM, RPG_WANDER_NPC, RPG_DO_QUEST, - RPG_TRAVEL_FLIGHT, RPG_REST}); + RPG_TRAVEL_FLIGHT, RPG_REST, RPG_OUTDOOR_PVP}); case RPG_GO_GRIND: { @@ -140,6 +140,15 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) } break; } + case RPG_OUTDOOR_PVP: + { + if (info.HasStatusPersisted(statusOutDoorPvPDuration)) + { + info.ChangeToIdle(); + return true; + } + break; + } default: break; } diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.h b/src/Ai/World/Rpg/Action/NewRpgAction.h index a8cb7a2bcef..83594204b64 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgAction.h @@ -47,10 +47,11 @@ class NewRpgStatusUpdateAction : public NewRpgBaseAction protected: // static NewRpgStatusTransitionProb transitionMat; - const int32 statusWanderNpcDuration = 5 * 60 * 1000; - const int32 statusWanderRandomDuration = 5 * 60 * 1000; - const int32 statusRestDuration = 30 * 1000; - const int32 statusDoQuestDuration = 30 * 60 * 1000; + const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ; + const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ; + const int32 statusRestDuration = 30 * IN_MILLISECONDS ; + const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ; + const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ; }; class NewRpgGoGrindAction : public NewRpgBaseAction diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 986b2f0f543..092b115387f 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -12,6 +12,7 @@ #include "NewRpgStrategy.h" #include "Object.h" #include "ObjectAccessor.h" +#include "OutdoorPvPMgr.h" #include "ObjectDefines.h" #include "ObjectGuid.h" #include "ObjectMgr.h" @@ -222,12 +223,10 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) return MoveTo(mapId, x, y, z, false, false, false, true); } -bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority) +bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, WorldObject* center) { if (IsWaitingForLastMove(priority)) - { return false; - } Map* map = bot->GetMap(); const float x = bot->GetPositionX(); @@ -1160,6 +1159,11 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta bot->SetStandState(UNIT_STAND_STATE_SIT); return true; } + case RPG_OUTDOOR_PVP: + { + botAI->rpgInfo.ChangeToOutdoorPvp(); + return true; + } default: { botAI->rpgInfo.ChangeToRest(); @@ -1220,6 +1224,17 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) std::vector path; return SelectRandomFlightTaxiNode(flightMaster, path); } + case RPG_OUTDOOR_PVP: + { + if (!bot->IsPvP()) + return false; + uint32 zoneId = bot->GetZoneId(); + if (zoneId == AREA_NAGRAND) + return false; + + OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId); + return outdoorPvP != nullptr; + } default: return false; } diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index f17891ffc65..eaba7244628 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -31,7 +31,7 @@ class NewRpgBaseAction : public MovementAction /* MOVEMENT RELATED */ bool MoveFarTo(WorldPosition dest); bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); - bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); + bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr); bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); /* QUEST RELATED CHECK */ diff --git a/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp new file mode 100644 index 00000000000..042246cfb4c --- /dev/null +++ b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp @@ -0,0 +1,128 @@ +#include "NewRpgOutdoorPvP.h" +#include "OutdoorPvP.h" +#include "OutdoorPvPMgr.h" + +bool NewRpgOutdoorPvpAction::Execute(Event event) +{ + if (!bot->IsPvP()) + { + botAI->rpgInfo.ChangeToIdle(); + return false; + } + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive()) + return false; + + uint32 zoneId = bot->GetZoneId(); + OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId); + if (!outdoorPvP || zoneId == AREA_NAGRAND) + { + botAI->rpgInfo.ChangeToIdle(); + return false; + } + + OutdoorPvP::OPvPCapturePointMap const& capturePointMap = outdoorPvP->GetCapturePoints(); + + NewRpgInfo& info = botAI->rpgInfo; + auto* dataPtr = std::get_if(&info.data); + if (!dataPtr) + return false; + auto& data = *dataPtr; + // Re-resolve stored spawn ID from the capture point map each tick (avoids dangling pointers) + OPvPCapturePoint* objective = nullptr; + if (data.capturePointSpawnId && !capturePointMap.empty()) + { + auto it = capturePointMap.find(data.capturePointSpawnId); + if (it != capturePointMap.end()) + { + OPvPCapturePoint* capturePoint = it->second; + if (capturePoint && capturePoint->_capturePoint) + { + float threshold = capturePoint->GetMinValue(); + float slider = capturePoint->GetSlider(); + uint8 faction = bot->GetTeamId(); + LOG_DEBUG("playerbots", "[NEW RPG] Bot {} with faction {} is evaluating existing RPG objective {} with threshold {} and slider value {}", bot->GetName(), faction, capturePoint->_capturePoint->GetName(), threshold, slider); + if ((faction == TEAM_HORDE && slider >= -threshold) || + (faction == TEAM_ALLIANCE && slider <= threshold)) + objective = capturePoint; + } + } + if (!objective) + data.capturePointSpawnId = 0; + } + + if (!objective) + { + objective = SelectNewObjective(capturePointMap); + if (!objective) + { + botAI->rpgInfo.ChangeToIdle(); + return true; + } + data.capturePointSpawnId = objective->m_capturePointSpawnId; + LOG_DEBUG("playerbots","[NEW RPG] Bot {} selected OutDoorPvP target capturePointSpawnId {}", bot->GetName(), data.capturePointSpawnId); + } + + GameObject* objectiveGO = objective->_capturePoint; + if (!objectiveGO) + return false; + + if (objectiveGO->GetGoType() != GAMEOBJECT_TYPE_CAPTURE_POINT) + return false; + + float radius = objectiveGO->GetGOInfo()->capturePoint.radius / 2.0f; + if (!objectiveGO->IsWithinDistInMap(bot, radius)) + return MoveFarTo(WorldPosition(objectiveGO)); + + return PatrolCapturePoint(objectiveGO, radius); +} + +OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCapturePointMap const& capturePointMap) +{ + OPvPCapturePoint* objective = nullptr; + uint8 faction = bot->GetTeamId(); + std::vector candidateObjectives; + + if (capturePointMap.empty()) + { + botAI->rpgInfo.ChangeToIdle(); + return objective; + } + for (auto const& [guid, point] : capturePointMap) + { + GameObject* capturePointObject = point->_capturePoint; + if (!capturePointObject) + continue; + + float threshold = point->GetMinValue(); + float slider = point->GetSlider(); + if (faction == TEAM_HORDE && slider > -threshold) + candidateObjectives.push_back(point); + else if (faction == TEAM_ALLIANCE && slider < threshold) + candidateObjectives.push_back(point); + } + if (candidateObjectives.empty()) + { + LOG_DEBUG("playerbots", "[New RPG] Bot {} found no valid outdoor PVP objectives to capture", bot->GetName()); + botAI->rpgInfo.ChangeToIdle(); + return objective; + } + int randomIndex = urand(0, candidateObjectives.size() - 1); + objective = candidateObjectives[randomIndex]; + return objective; +} + +bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius) +{ + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) + return false; + + // Randomly pause at the current spot before picking a new patrol point + if (urand(0, 2) == 0) + return ForceToWait(urand(3000, 6000)); + + float patrolRadius = radius * 0.8f; + if (MoveRandomNear(patrolRadius, MovementPriority::MOVEMENT_NORMAL, objectiveGO)) + return true; + + return ForceToWait(urand(3000, 6000)); +} diff --git a/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h new file mode 100644 index 00000000000..0487b9891f1 --- /dev/null +++ b/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h @@ -0,0 +1,19 @@ +#ifndef PLAYERBOT_NEWRPGOUTDOORPVP_H +#define PLAYERBOT_NEWRPGOUTDOORPVP_H + +#include "NewRpgBaseAction.h" +#include "OutdoorPvP.h" + +class NewRpgOutdoorPvpAction : public NewRpgBaseAction +{ +public: + NewRpgOutdoorPvpAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg outdoor pvp") {} + + virtual bool Execute(Event event) override; + OPvPCapturePoint* SelectNewObjective(OutdoorPvP::OPvPCapturePointMap const& capturePointMap); + +private: + bool PatrolCapturePoint(GameObject* objectiveGO, float radius); +}; + +#endif diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 4e04ab08674..4935503fc25 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -47,6 +47,14 @@ void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector) return RPG_REST; if constexpr (std::is_same_v) return RPG_DO_QUEST; if constexpr (std::is_same_v) return RPG_TRAVEL_FLIGHT; + if constexpr (std::is_same_v) return RPG_OUTDOOR_PVP; return RPG_IDLE; }, data); } @@ -153,6 +162,14 @@ std::string NewRpgInfo::ToString() out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; } + else if constexpr (std::is_same_v) + { + out << "OUTDOOR_PVP"; + if (!arg.capturePointSpawnId) + out << "\nNo capture point assigned."; + else + out << "\ncapturePointSpawnId: " << arg.capturePointSpawnId; + } else out << "UNKNOWN"; }, data); diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index c2349c14b2f..9e6abdda4c7 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -58,6 +58,11 @@ struct NewRpgInfo { Rest() = default; }; + // RPG_OUTDOOR_PVP + struct OutdoorPvP + { + ObjectGuid::LowType capturePointSpawnId{0}; + }; struct Idle { }; @@ -79,7 +84,8 @@ struct NewRpgInfo WanderRandom, DoQuest, Rest, - TravelFlight + TravelFlight, + OutdoorPvP >; RpgData data; @@ -91,6 +97,7 @@ struct NewRpgInfo void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); + void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0); void ChangeToRest(); void ChangeToIdle(); bool CanChangeTo(NewRpgStatus status); diff --git a/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp b/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp index f0d0ce5a923..521b15c34ad 100644 --- a/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp +++ b/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp @@ -65,6 +65,14 @@ void NewRpgStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back( + new TriggerNode( + "outdoor pvp status", + { + NextAction("new rpg outdoor pvp", 3.0f) + } + ) + ); } void NewRpgStrategy::InitMultipliers(std::vector&) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 1a74b8b2f7d..e9de581dead 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -54,6 +54,7 @@ #include "Unit.h" #include "UpdateTime.h" #include "Vehicle.h" +#include "../../../../src/server/scripts/Spells/spell_dk.cpp" const int SPELL_TITAN_GRIP = 49152; @@ -2163,7 +2164,7 @@ bool PlayerbotAI::IsTank(Player* player, bool bySpec) switch (player->getClass()) { case CLASS_DEATH_KNIGHT: - if (tab == DEATH_KNIGHT_TAB_BLOOD) + if (tab == DEATH_KNIGHT_TAB_BLOOD || player->HasAura(SPELL_DK_FROST_PRESENCE)) { return true; } diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index a772136e771..ee06c58f899 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -2861,10 +2861,10 @@ void RandomPlayerbotMgr::PrintStats() LOG_INFO("playerbots", "Bots rpg status:"); LOG_INFO("playerbots", " Idle: {}, Rest: {}, GoGrind: {}, GoCamp: {}, MoveRandom: {}, MoveNpc: {}, DoQuest: {}, " - "TravelFlight: {}", + "TravelFlight: {}, OutdoorPvP: {}", rpgStatusCount[RPG_IDLE], rpgStatusCount[RPG_REST], rpgStatusCount[RPG_GO_GRIND], rpgStatusCount[RPG_GO_CAMP], rpgStatusCount[RPG_WANDER_RANDOM], rpgStatusCount[RPG_WANDER_NPC], - rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT]); + rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT], rpgStatusCount[RPG_OUTDOOR_PVP]); LOG_INFO("playerbots", "Bots total quests:"); LOG_INFO("playerbots", " Accepted: {}, Rewarded: {}, Dropped: {}", rpgStasticTotal.questAccepted, diff --git a/src/Mgr/Item/StatsWeightCalculator.cpp b/src/Mgr/Item/StatsWeightCalculator.cpp index 1b3f3dcfcb5..361e4a5f722 100644 --- a/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/src/Mgr/Item/StatsWeightCalculator.cpp @@ -20,6 +20,13 @@ #include "StatsCollector.h" #include "Unit.h" +namespace +{ +constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_1 = 30482; +constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_2 = 43045; +constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_3 = 43046; +} + StatsWeightCalculator::StatsWeightCalculator(Player* player) : player_(player) { if (PlayerbotAI::IsHeal(player)) @@ -454,6 +461,16 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player) if (player->HasAura(51885)) stats_weights_[STATS_TYPE_INTELLECT] += 1.1f; } + else if (cls == CLASS_MAGE) + { + if (!player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_1) + && !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_2) + && !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_3)) + { + stats_weights_[STATS_TYPE_INTELLECT] += 0.2f; + stats_weights_[STATS_TYPE_SPIRIT] -= 0.0f; + } + } } void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto) diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index f150f7af198..8c8343db2b6 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -652,6 +652,7 @@ bool PlayerbotAIConfig::Initialize() RpgStatusProbWeight[RPG_DO_QUEST] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.DoQuest", 60); RpgStatusProbWeight[RPG_TRAVEL_FLIGHT] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.TravelFlight", 15); RpgStatusProbWeight[RPG_REST] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.Rest", 5); + RpgStatusProbWeight[RPG_OUTDOOR_PVP] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.OutdoorPvp", 10); syncLevelWithPlayers = sConfigMgr->GetOption("AiPlayerbot.SyncLevelWithPlayers", false); randomBotGroupNearby = sConfigMgr->GetOption("AiPlayerbot.RandomBotGroupNearby", false); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 7b6c1eb6f57..4e758e79e93 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -56,7 +56,8 @@ enum NewRpgStatus : int RPG_TRAVEL_FLIGHT = 6, // Taking a break RPG_REST = 7, - RPG_STATUS_END = 8 + RPG_OUTDOOR_PVP = 8, + RPG_STATUS_END = 9 }; #define MAX_SPECNO 20 diff --git a/src/Util/ServerFacade.cpp b/src/Util/ServerFacade.cpp index 12a77de6359..1ba1eb5ea5b 100644 --- a/src/Util/ServerFacade.cpp +++ b/src/Util/ServerFacade.cpp @@ -53,8 +53,9 @@ void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool force) bot->SetOrientation(angle); if (!bot->IsRooted()) - bot->SendMovementFlagUpdate(); - // } + // enforce (bool self) true otherwhise when using real-client with self-bot wont + // recieve update; e.g. will not face the target when using (mostly ranged) attack + bot->SendMovementFlagUpdate(true); } Unit* ServerFacade::GetChaseTarget(Unit* target)