diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index a782479550..cc06208909 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -630,7 +630,7 @@ AiPlayerbot.RandomBotHordeRatio = 50 AiPlayerbot.DisableDeathKnightLogin = 0 # Enable simulated expansion limitation for talents and glyphs -# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 +# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 # and 7 rows plus the middle talent of the 8th row for bots from level 61 until level 71 # Default: 0 (disabled) AiPlayerbot.LimitTalentsExpansion = 0 diff --git a/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql b/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql new file mode 100644 index 0000000000..b7c6c96826 --- /dev/null +++ b/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql @@ -0,0 +1,255 @@ +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'pet_usage_error', + 'pet_no_pet_error', + 'pet_stance_report', + 'pet_no_target_error', + 'pet_target_dead_error', + 'pet_invalid_target_error', + 'pet_pvp_prohibited_error', + 'pet_attack_success', + 'pet_attack_failed', + 'pet_follow_success', + 'pet_stay_success', + 'pet_unknown_command_error', + 'pet_stance_set_success', + 'pet_type_pet', + 'pet_type_guardian', + 'pet_stance_aggressive', + 'pet_stance_defensive', + 'pet_stance_passive', + 'pet_stance_unknown' +); + +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'pet_usage_error', + 'pet_no_pet_error', + 'pet_stance_report', + 'pet_no_target_error', + 'pet_target_dead_error', + 'pet_invalid_target_error', + 'pet_pvp_prohibited_error', + 'pet_attack_success', + 'pet_attack_failed', + 'pet_follow_success', + 'pet_stay_success', + 'pet_unknown_command_error', + 'pet_stance_set_success', + 'pet_type_pet', + 'pet_type_guardian', + 'pet_stance_aggressive', + 'pet_stance_defensive', + 'pet_stance_passive', + 'pet_stance_unknown' +); + +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 +(1717, 'pet_usage_error', "Usage: pet ", 0, 0, +"사용법: pet ", +"Utilisation: pet ", +"Verwendung: pet ", +"用法: pet ", +"用法: pet ", +"Uso: pet ", +"Uso: pet ", +"Использование: pet "), + +(1718, 'pet_no_pet_error', "You have no pet or guardian pet.", 0, 0, +"펫이나 수호자 펫이 없습니다.", +"Vous n'avez pas de familier ou gardien.", +"Du hast kein Tier oder Wächter.", +"你没有宠物或守护者宠物。", +"你沒有寵物或守護者寵物。", +"No tienes mascota o mascota guardián.", +"No tienes mascota o mascota guardián.", +"У вас нет питомца или защитника."), + +(1719, 'pet_stance_report', "Current stance of %type \"%name\": %stance.", 0, 0, +"%type \"%name\"의 현재 태세: %stance.", +"Position actuelle du %type \"%name\": %stance.", +"Aktuelle Haltung des %type \"%name\": %stance.", +"%type \"%name\" 的当前姿态: %stance。", +"%type \"%name\" 的當前姿態: %stance。", +"Postura actual del %type \"%name\": %stance.", +"Postura actual del %type \"%name\": %stance.", +"Текущая позиция %type \"%name\": %stance."), + +(1720, 'pet_no_target_error', "No valid target selected by master.", 0, 0, +"주인이 유효한 대상을 선택하지 않았습니다.", +"Aucune cible valide sélectionnée par le maître.", +"Kein gültiges Ziel vom Meister ausgewählt.", +"主人未选择有效目标。", +"主人未選擇有效目標。", +"No hay objetivo válido seleccionado por el maestro.", +"No hay objetivo válido seleccionado por el maestro.", +"Хозяин не выбрал действительную цель."), + +(1721, 'pet_target_dead_error', "Target is not alive.", 0, 0, +"대상이 살아있지 않습니다.", +"La cible n'est pas vivante.", +"Das Ziel ist nicht am Leben.", +"目标未存活。", +"目標未存活。", +"El objetivo no está vivo.", +"El objetivo no está vivo.", +"Цель не жива."), + +(1722, 'pet_invalid_target_error', "Target is not a valid attack target for the bot.", 0, 0, +"대상이 봇에게 유효한 공격 대상이 아닙니다.", +"La cible n'est pas une cible d'attaque valide pour le bot.", +"Das Ziel ist kein gültiges Angriffsziel für den Bot.", +"目标不是机器人的有效攻击目标。", +"目標不是機器人的有效攻擊目標。", +"El objetivo no es un objetivo de ataque válido para el bot.", +"El objetivo no es un objetivo de ataque válido para el bot.", +"Цель не является допустимой целью атаки для бота."), + +(1723, 'pet_pvp_prohibited_error', "I cannot command my pet to attack players in PvP prohibited areas.", 0, 0, +"PvP 금지 지역에서는 펫에게 플레이어 공격 명령을 내릴 수 없습니다.", +"Je ne peux pas commander à mon familier d'attaquer des joueurs dans les zones où le PvP est interdit.", +"Ich kann meinem Tier nicht befehlen, Spieler in PvP-verbotenen Gebieten anzugreifen.", +"我不能命令我的宠物在禁止PvP的区域攻击玩家。", +"我不能命令我的寵物在禁止PvP的區域攻擊玩家。", +"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.", +"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.", +"Я не могу приказать своему питомцу атаковать игроков в зонах, где PvP запрещено."), + +(1724, 'pet_attack_success', "Pet commanded to attack your target.", 0, 0, +"펫이 당신의 대상을 공격하도록 명령했습니다.", +"Le familier a reçu l'ordre d'attaquer votre cible.", +"Tier wurde befohlen, dein Ziel anzugreifen.", +"宠物已命令攻击你的目标。", +"寵物已命令攻擊你的目標。", +"Mascota ordenada a atacar tu objetivo.", +"Mascota ordenada a atacar tu objetivo.", +"Питомцу приказано атаковать вашу цель."), + +(1725, 'pet_attack_failed', "Pet did not attack. (Already attacking or unable to attack target)", 0, 0, +"펫이 공격하지 않았습니다. (이미 공격 중이거나 대상 공격 불가)", +"Le familier n'a pas attaqué. (Attaque déjà en cours ou impossible d'attaquer la cible)", +"Tier hat nicht angegriffen. (Greift bereits an oder kann Ziel nicht angreifen)", +"宠物未攻击。(已在攻击或无法攻击目标)", +"寵物未攻擊。(已在攻擊或無法攻擊目標)", +"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)", +"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)", +"Питомец не атаковал. (Уже атакует или не может атаковать цель)"), + +(1726, 'pet_follow_success', "Pet commanded to follow.", 0, 0, +"펫이 따라오도록 명령했습니다.", +"Le familier a reçu l'ordre de suivre.", +"Tier wurde befohlen zu folgen.", +"宠物已命令跟随。", +"寵物已命令跟隨。", +"Mascota ordenada a seguir.", +"Mascota ordenada a seguir.", +"Питомцу приказано следовать."), + +(1727, 'pet_stay_success', "Pet commanded to stay.", 0, 0, +"펫이 머물도록 명령했습니다.", +"Le familier a reçu l'ordre de rester.", +"Tier wurde befohlen zu bleiben.", +"宠物已命令停留。", +"寵物已命令停留。", +"Mascota ordenada a quedarse.", +"Mascota ordenada a quedarse.", +"Питомцу приказано остаться."), + +(1728, 'pet_unknown_command_error', "Unknown pet command: %param. Use: pet ", 0, 0, +"알 수 없는 펫 명령: %param. 사용법: pet ", +"Commande de familier inconnue: %param. Utilisation: pet ", +"Unbekannter Tierbefehl: %param. Verwendung: pet ", +"未知宠物命令: %param。用法: pet ", +"未知寵物命令: %param。用法: pet ", +"Comando de mascota desconocido: %param. Uso: pet ", +"Comando de mascota desconocido: %param. Uso: pet ", +"Неизвестная команда питомца: %param. Использование: pet "), + +(1729, 'pet_stance_set_success', "Pet stance set to %stance.", 0, 0, +"펫 태세가 %stance(으)로 설정되었습니다.", +"Position du familier définie sur %stance.", +"Tierhaltung auf %stance gesetzt.", +"宠物姿态设置为 %stance。", +"寵物姿態設置為 %stance。", +"Postura de mascota establecida en %stance.", +"Postura de mascota establecida en %stance.", +"Позиция питомца установлена на %stance."), + +(1730, 'pet_type_pet', "pet", 0, 0, +"펫", +"familier", +"Tier", +"宠物", +"寵物", +"mascota", +"mascota", +"питомец"), + +(1731, 'pet_type_guardian', "guardian", 0, 0, +"수호자", +"gardien", +"Wächter", +"守护者", +"守護者", +"guardián", +"guardián", +"защитник"), + +(1732, 'pet_stance_aggressive', "aggressive", 0, 0, +"공격적", +"agressif", +"aggressiv", +"进攻", +"進攻", +"agresivo", +"agresivo", +"агрессивная"), + +(1733, 'pet_stance_defensive', "defensive", 0, 0, +"방어적", +"défensif", +"defensiv", +"防御", +"防禦", +"defensivo", +"defensivo", +"защитная"), + +(1734, 'pet_stance_passive', "passive", 0, 0, +"수동적", +"passif", +"passiv", +"被动", +"被動", +"pasivo", +"pasivo", +"пассивная"), + +(1735, 'pet_stance_unknown', "unknown", 0, 0, +"알 수 없음", +"inconnu", +"unbekannt", +"未知", +"未知", +"desconocido", +"desconocido", +"неизвестная"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES +('pet_usage_error', 100), +('pet_no_pet_error', 100), +('pet_stance_report', 100), +('pet_no_target_error', 100), +('pet_target_dead_error', 100), +('pet_invalid_target_error', 100), +('pet_pvp_prohibited_error', 100), +('pet_attack_success', 100), +('pet_attack_failed', 100), +('pet_follow_success', 100), +('pet_stay_success', 100), +('pet_unknown_command_error', 100), +('pet_stance_set_success', 100), +('pet_type_pet', 100), +('pet_type_guardian', 100), +('pet_stance_aggressive', 100), +('pet_stance_defensive', 100), +('pet_stance_passive', 100), +('pet_stance_unknown', 100); \ No newline at end of file diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 23d073f54c..f176607482 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -43,6 +43,7 @@ #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "PlayerbotMgr.h" +#include "PlayerbotGuildMgr.h" #include "Playerbots.h" #include "PointMovementGenerator.h" #include "PositionValue.h" @@ -365,7 +366,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Update the bot's group status (moved to helper function) - UpdateAIGroupAndMaster(); + UpdateAIGroupMaster(); // Update internal AI UpdateAIInternal(elapsed, minimal); @@ -373,7 +374,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Helper function for UpdateAI to check group membership and handle removal if necessary -void PlayerbotAI::UpdateAIGroupAndMaster() +void PlayerbotAI::UpdateAIGroupMaster() { if (!bot) return; @@ -420,7 +421,7 @@ void PlayerbotAI::UpdateAIGroupAndMaster() { botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); - if (botAI->GetMaster() == botAI->GetGroupMaster()) + if (botAI->GetMaster() == botAI->GetGroupLeader()) botAI->TellMaster("Hello, I follow you!"); else botAI->TellMaster(!urand(0, 2) ? "Hello!" : "Hi!"); @@ -431,8 +432,6 @@ void PlayerbotAI::UpdateAIGroupAndMaster() botAI->ChangeStrategy("-follow", BOT_STATE_NON_COMBAT); } } - else if (!newMaster && !bot->InBattleground()) - LeaveOrDisbandGroup(); } } @@ -1386,9 +1385,6 @@ void PlayerbotAI::DoNextAction(bool min) else if (bot->isAFK()) bot->ToggleAFK(); - Group* group = bot->GetGroup(); - PlayerbotAI* masterBotAI = nullptr; - if (master && master->IsInWorld()) { float distance = sServerFacade->GetDistance2d(bot, master); @@ -1461,7 +1457,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "onyxia"; // Onyxia's Lair break; case 409: - strategyName = "mc"; // Molten Core + strategyName = "moltencore"; // Molten Core break; case 469: strategyName = "bwl"; // Blackwing Lair @@ -1477,6 +1473,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) break; case 544: strategyName = "magtheridon"; // Magtheridon's Lair + break; case 565: strategyName = "gruulslair"; // Gruul's Lair break; @@ -2248,7 +2245,7 @@ uint32 PlayerbotAI::GetGroupTankNum(Player* player) bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } -bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) +bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers) { Group* group = player->GetGroup(); if (!group) @@ -2265,6 +2262,9 @@ bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) continue; } + if (ignoreDeadPlayers && !member->IsAlive()) + continue; + if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) @@ -2284,6 +2284,9 @@ bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) continue; } + if (ignoreDeadPlayers && !member->IsAlive()) + continue; + if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) @@ -4092,7 +4095,7 @@ Player* PlayerbotAI::FindNewMaster() if (!group) return nullptr; - Player* groupLeader = GetGroupMaster(); + Player* groupLeader = GetGroupLeader(); PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(groupLeader); if (!leaderBotAI || leaderBotAI->IsRealPlayer()) return groupLeader; @@ -4143,7 +4146,7 @@ bool PlayerbotAI::HasActivePlayerMaster() { return master && !GET_PLAYERBOT_AI(m bool PlayerbotAI::IsAlt() { return HasRealPlayerMaster() && !sRandomPlayerbotMgr->IsRandomBot(bot); } -Player* PlayerbotAI::GetGroupMaster() +Player* PlayerbotAI::GetGroupLeader() { if (!bot->InBattleground()) if (Group* group = bot->GetGroup()) @@ -5812,19 +5815,10 @@ bool PlayerbotAI::CanMove() return bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FLIGHT_MOTION_TYPE; } - +//TODO Verify where this is actually called and the implementation of it. bool PlayerbotAI::IsRealGuild(uint32 guildId) { - Guild* guild = sGuildMgr->GetGuildById(guildId); - if (!guild) - { - return false; - } - uint32 leaderAccount = sCharacterCache->GetCharacterAccountIdByGuid(guild->GetLeaderGUID()); - if (!leaderAccount) - return false; - - return !(sPlayerbotAIConfig->IsInRandomAccountList(leaderAccount)); + return sPlayerbotGuildMgr->IsRealGuild(guildId); } bool PlayerbotAI::IsInRealGuild() @@ -5832,7 +5826,7 @@ bool PlayerbotAI::IsInRealGuild() if (!bot->GetGuildId()) return false; - return IsRealGuild(bot->GetGuildId()); + return sPlayerbotGuildMgr->IsRealGuild(bot->GetGuildId()); } void PlayerbotAI::QueueChatResponse(const ChatQueuedReply chatReply) { chatReplies.push_back(std::move(chatReply)); } diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index 8c8cff14f2..3b9eaab1ea 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -428,7 +428,7 @@ class PlayerbotAI : public PlayerbotAIBase static bool IsMainTank(Player* player); static uint32 GetGroupTankNum(Player* player); static bool IsAssistTank(Player* player); - static bool IsAssistTankOfIndex(Player* player, int index); + static bool IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); static bool IsHealAssistantOfIndex(Player* player, int index); static bool IsRangedDpsAssistantOfIndex(Player* player, int index); bool HasAggro(Unit* unit); @@ -540,7 +540,7 @@ class PlayerbotAI : public PlayerbotAIBase // Get the group leader or the master of the bot. // Checks if the bot is summoned as alt of a player bool IsAlt(); - Player* GetGroupMaster(); + Player* GetGroupLeader(); // Returns a semi-random (cycling) number that is fixed for each bot. uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); GrouperType GetGrouperType(); @@ -612,7 +612,7 @@ class PlayerbotAI : public PlayerbotAIBase static void _fillGearScoreData(Player* player, Item* item, std::vector* gearScore, uint32& twoHandScore, bool mixed = false); bool IsTellAllowed(PlayerbotSecurityLevel securityLevel = PLAYERBOT_SECURITY_ALLOW_ALL); - void UpdateAIGroupAndMaster(); + void UpdateAIGroupMaster(); Item* FindItemInInventory(std::function checkItem) const; void HandleCommands(); void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index bdafc10c63..45f880d213 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -10,6 +10,7 @@ #include "PlayerbotDungeonSuggestionMgr.h" #include "PlayerbotFactory.h" #include "Playerbots.h" +#include "PlayerbotGuildMgr.h" #include "RandomItemMgr.h" #include "RandomPlayerbotFactory.h" #include "RandomPlayerbotMgr.h" @@ -661,6 +662,7 @@ bool PlayerbotAIConfig::Initialize() sRandomPlayerbotMgr->Init(); } + sPlayerbotGuildMgr->Init(); sRandomItemMgr->Init(); sRandomItemMgr->InitAfterAhBot(); sPlayerbotTextMgr->LoadBotTexts(); @@ -725,8 +727,8 @@ std::string const PlayerbotAIConfig::GetTimestampStr() // HH hour (2 digits 00-23) // MM minutes (2 digits 00-59) // SS seconds (2 digits 00-59) - char buf[20]; - snprintf(buf, 20, "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour, + char buf[32]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour, aTm->tm_min, aTm->tm_sec); return std::string(buf); } diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 83a6a20b9a..76177395f6 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -269,7 +269,6 @@ class PlayerbotAIConfig bool deleteRandomBotAccounts; uint32 randomBotGuildCount, randomBotGuildSizeMax; bool deleteRandomBotGuilds; - std::vector randomBotGuilds; std::vector pvpProhibitedZoneIds; std::vector pvpProhibitedAreaIds; bool fastReactInBG; diff --git a/src/PlayerbotGuildMgr.cpp b/src/PlayerbotGuildMgr.cpp new file mode 100644 index 0000000000..13068d7205 --- /dev/null +++ b/src/PlayerbotGuildMgr.cpp @@ -0,0 +1,324 @@ +#include "PlayerbotGuildMgr.h" +#include "Player.h" +#include "PlayerbotAIConfig.h" +#include "DatabaseEnv.h" +#include "Guild.h" +#include "GuildMgr.h" +#include "RandomPlayerbotMgr.h" +#include "ScriptMgr.h" + +PlayerbotGuildMgr::PlayerbotGuildMgr() +{ + _randomBotGuildCount = sPlayerbotAIConfig->randomBotGuildCount; + _randomBotGuildSizeMax = sPlayerbotAIConfig->randomBotGuildSizeMax; +} + +void PlayerbotGuildMgr::Init() +{ + _guildCache.clear(); + if (sPlayerbotAIConfig->deleteRandomBotGuilds) + DeleteBotGuilds(); + + LoadGuildNames(); + ValidateGuildCache(); +} + +bool PlayerbotGuildMgr::CreateGuild(Player* player, std::string guildName) +{ + Guild* guild = new Guild(); + if (!guild->Create(player, guildName)) + { + LOG_ERROR("playerbots", "Error creating guild [ {} ] with leader [ {} ]", guildName, + player->GetName()); + delete guild; + return false; + } + sGuildMgr->AddGuild(guild); + + LOG_DEBUG("playerbots", "Guild created: id={} name='{}'", guild->GetId(), guildName); + SetGuildEmblem(guild->GetId()); + + GuildCache entry; + entry.name = guildName; + entry.memberCount = 1; + entry.status = 1; + entry.maxMembers = _randomBotGuildSizeMax; + entry.faction = player->GetTeamId(); + + _guildCache[guild->GetId()] = entry; + return true; +} + +bool PlayerbotGuildMgr::SetGuildEmblem(uint32 guildId) +{ + Guild* guild = sGuildMgr->GetGuildById(guildId); + if (!guild) + return false; + + // create random emblem + uint32 st, cl, br, bc, bg; + bg = urand(0, 51); + bc = urand(0, 17); + cl = urand(0, 17); + br = urand(0, 7); + st = urand(0, 180); + + LOG_DEBUG("playerbots", + "[TABARD] new guild id={} random -> style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", + guild->GetId(), st, cl, br, bc, bg); + + // populate guild table with a random tabard design + CharacterDatabase.Execute( + "UPDATE guild SET EmblemStyle={}, EmblemColor={}, BorderStyle={}, BorderColor={}, BackgroundColor={} " + "WHERE guildid={}", + st, cl, br, bc, bg, guild->GetId()); + LOG_DEBUG("playerbots", "[TABARD] UPDATE done for guild id={}", guild->GetId()); + + // Immediate reading for log + if (QueryResult qr = CharacterDatabase.Query( + "SELECT EmblemStyle,EmblemColor,BorderStyle,BorderColor,BackgroundColor FROM guild WHERE guildid={}", + guild->GetId())) + { + Field* f = qr->Fetch(); + LOG_DEBUG("playerbots", + "[TABARD] DB check guild id={} => style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", + guild->GetId(), f[0].Get(), f[1].Get(), f[2].Get(), f[3].Get(), f[4].Get()); + } + return true; +} + +std::string PlayerbotGuildMgr::AssignToGuild(Player* player) +{ + if (!player) + return ""; + + LOG_DEBUG("playerbots", "Assigning player [{}] to a guild", player->GetName()); + + int playerFaction = player->GetTeamId(); + std::vector partiallyfilledguilds; + partiallyfilledguilds.reserve(_guildCache.size()); + + for (auto& keyValue : _guildCache) + { + GuildCache& cached = keyValue.second; + if (cached.status == 1 && cached.faction == playerFaction) + partiallyfilledguilds.push_back(&cached); + } + + if (!partiallyfilledguilds.empty()) + { + size_t idx = static_cast(urand(0, static_cast(partiallyfilledguilds.size()) - 1)); + return (partiallyfilledguilds[idx]->name); + } + + for (auto key : _shuffled_guild_keys) + { + if (_guildNames[key]) + { + LOG_INFO("playerbots","Assigning player [{}] to guild [{}]", player->GetName(), key); + return key; + } + } + LOG_ERROR("playerbots","No available guild names left."); + return ""; +} + +void PlayerbotGuildMgr::OnGuildUpdate(Guild* guild) +{ + auto it = _guildCache.find(guild->GetId()); + if (it == _guildCache.end()) + return; + + GuildCache& entry = it->second; + entry.memberCount++; + if (entry.memberCount >= entry.maxMembers) + entry.status = 2; // Full + std::string guildName = guild->GetName(); + for (auto it : _guildNames) + { + if (it.first == guildName) + { + it.second = false; + break; + } + } +} + +void PlayerbotGuildMgr::ResetGuildCache() +{ + for (auto it = _guildCache.begin(); it != _guildCache.end();) + { + GuildCache& cached = it->second; + cached.memberCount = 0; + cached.faction = 2; + cached.status = 0; + } +} + +void PlayerbotGuildMgr::LoadGuildNames() +{ + LOG_INFO("playerbots", "Loading guild names from playerbots_guild_names..."); + + QueryResult result = CharacterDatabase.Query("SELECT name_id, name FROM playerbots_guild_names"); + + if (!result) + { + LOG_ERROR("playerbots", "No entries found in playerbots_guild_names. List is empty."); + return; + } + + do + { + Field* fields = result->Fetch(); + _guildNames[fields[1].Get()] = true; + } while (result->NextRow()); + + for (auto& pair : _guildNames) + _shuffled_guild_keys.push_back(pair.first); + + std::random_device rd; + std::mt19937 g(rd()); + + std::shuffle(_shuffled_guild_keys.begin(), _shuffled_guild_keys.end(), g); + LOG_INFO("playerbots", "Loaded {} guild entries from playerbots_guild_names table.", _guildNames.size()); +} + +void PlayerbotGuildMgr::ValidateGuildCache() +{ + QueryResult result = CharacterDatabase.Query("SELECT guildid, name FROM guild"); + if (!result) + { + LOG_ERROR("playerbots", "No guilds found in database, resetting guild cache"); + ResetGuildCache(); + return; + } + + std::unordered_map dbGuilds; + do + { + Field* fields = result->Fetch(); + uint32 guildId = fields[0].Get(); + std::string guildName = fields[1].Get(); + dbGuilds[guildId] = guildName; + } while (result->NextRow()); + + for (auto it = dbGuilds.begin(); it != dbGuilds.end(); it++) + { + uint32 guildId = it->first; + GuildCache cache; + cache.name = it->second; + cache.maxMembers = _randomBotGuildSizeMax; + + Guild* guild = sGuildMgr ->GetGuildById(guildId); + if (!guild) + continue; + + cache.memberCount = guild->GetMemberCount(); + ObjectGuid leaderGuid = guild->GetLeaderGUID(); + CharacterCacheEntry const* leaderEntry = sCharacterCache->GetCharacterCacheByGuid(leaderGuid); + uint32 leaderAccount = leaderEntry->AccountId; + cache.hasRealPlayer = !(sPlayerbotAIConfig->IsInRandomAccountList(leaderAccount)); + cache.faction = Player::TeamIdForRace(leaderEntry->Race); + if (cache.memberCount == 0) + cache.status = 0; // empty + else if (cache.memberCount < cache.maxMembers) + cache.status = 1; // partially filled + else + cache.status = 2; // full + + _guildCache.insert_or_assign(guildId, cache); + for (auto it : _guildNames) + { + if (it.first == cache.name) + { + it.second = false; + break; + } + } + } +} + +void PlayerbotGuildMgr::DeleteBotGuilds() +{ + LOG_INFO("playerbots", "Deleting random bot guilds..."); + std::vector randomBots; + + PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BOT); + stmt->SetData(0, "add"); + if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt)) + { + do + { + Field* fields = result->Fetch(); + uint32 bot = fields[0].Get(); + randomBots.push_back(bot); + } while (result->NextRow()); + } + + for (std::vector::iterator i = randomBots.begin(); i != randomBots.end(); ++i) + { + if (Guild* guild = sGuildMgr->GetGuildByLeader(ObjectGuid::Create(*i))) + guild->Disband(); + } + LOG_INFO("playerbots", "Random bot guilds deleted"); +} + +bool PlayerbotGuildMgr::IsRealGuild(Player* bot) +{ + if (!bot) + return false; + uint32 guildId = bot->GetGuildId(); + if (!guildId) + return false; + + return IsRealGuild(guildId); +} + +bool PlayerbotGuildMgr::IsRealGuild(uint32 guildId) +{ + if (!guildId) + return false; + + auto it = _guildCache.find(guildId); + if (it == _guildCache.end()) + return false; + + return it->second.hasRealPlayer; +} + +class BotGuildCacheWorldScript : public WorldScript +{ + public: + + BotGuildCacheWorldScript() : WorldScript("BotGuildCacheWorldScript"), _validateTimer(0){} + + void OnStartup() override + { + sPlayerbotGuildMgr->LoadGuildNames(); + LOG_INFO("server.loading", "Bot guild cache initialized"); + sPlayerbotGuildMgr->ValidateGuildCache(); + LOG_INFO("server.loading", "Bot guild cache validated"); + } + + void OnUpdate(uint32 diff) override + { + _validateTimer += diff; + + if (_validateTimer >= _validateInterval) // Validate every hour + { + _validateTimer = 0; + sPlayerbotGuildMgr->ValidateGuildCache(); + LOG_INFO("playerbots", "Schedueled guild cache validation"); + } + return; + } + + private: + uint32 _validateInterval = HOUR*IN_MILLISECONDS; + uint32 _validateTimer; +}; + +void PlayerBotsGuildValidationScript() +{ + new BotGuildCacheWorldScript(); +} \ No newline at end of file diff --git a/src/PlayerbotGuildMgr.h b/src/PlayerbotGuildMgr.h new file mode 100644 index 0000000000..d05c6845c2 --- /dev/null +++ b/src/PlayerbotGuildMgr.h @@ -0,0 +1,65 @@ +#ifndef _PLAYERBOT_PLAYERBOTGUILDMGR_H +#define _PLAYERBOT_PLAYERBOTGUILDMGR_H + +#include "Guild.h" +#include "Player.h" +#include "PlayerbotAI.h" + +constexpr std::array GuilderMap = +{ + GuilderType::SOLO, + GuilderType::TINY, + GuilderType::SMALL, + GuilderType::MEDIUM, + GuilderType::LARGE, + GuilderType::VERY_LARGE +}; + +class PlayerbotAI; + +class PlayerbotGuildMgr +{ +public: + static PlayerbotGuildMgr* instance() + { + static PlayerbotGuildMgr instance; + return &instance; + } + + void Init(); + std::string AssignToGuild(Player* player); + void LoadGuildNames(); + void ValidateGuildCache(); + void ResetGuildCache(); + bool CreateGuild(Player* player, std::string guildName); + void OnGuildUpdate (Guild* guild); + bool SetGuildEmblem(uint32 guildId); + void DeleteBotGuilds(); + bool IsRealGuild(uint32 guildId); + bool IsRealGuild(Player* bot); + +private: + PlayerbotGuildMgr(); + int _maxIndex; + int _randomBotGuildCount; + int _randomBotGuildSizeMax; + std::unordered_map _guildNames; + + struct GuildCache + { + std::string name; + uint8 status; + uint32 maxMembers = 0; + uint32 memberCount = 0; + uint8 faction = 0; + bool hasRealPlayer = false; + }; + std::unordered_map _guildCache; + std::vector _shuffled_guild_keys; +}; + +void PlayerBotsGuildValidationScript(); + +#define sPlayerbotGuildMgr PlayerbotGuildMgr::instance() + +#endif \ No newline at end of file diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index 6bdbf9b659..fc78461797 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -27,8 +27,11 @@ #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "PlayerbotFactory.h" +#include "PlayerbotOperations.h" #include "PlayerbotSecurity.h" +#include "PlayerbotWorldThreadProcessor.h" #include "Playerbots.h" +#include "PlayerbotGuildMgr.h" #include "RandomPlayerbotMgr.h" #include "SharedDefines.h" #include "WorldSession.h" @@ -85,7 +88,6 @@ class PlayerbotLoginQueryHolder : public LoginQueryHolder void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId) { - // bot is loading if (botLoading.find(playerGuid) != botLoading.end()) return; @@ -195,7 +197,9 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con } sRandomPlayerbotMgr->OnPlayerLogin(bot); - OnBotLogin(bot); + + auto op = std::make_unique(bot->GetGUID(), this); + sPlayerbotWorldProcessor->QueueOperation(std::move(op)); botLoading.erase(holder.GetGuid()); } @@ -316,11 +320,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) if (!botAI) return; - Group* group = bot->GetGroup(); - if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster()) - { - sPlayerbotDbStore->Save(botAI); - } + // Queue group cleanup operation for world thread + auto cleanupOp = std::make_unique(guid); + sPlayerbotWorldProcessor->QueueOperation(std::move(cleanupOp)); LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str()); bot->SaveToDB(false, false); @@ -549,6 +551,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK); + // Queue group operations for world thread if (master && master->GetGroup() && !group) { Group* mgroup = master->GetGroup(); @@ -556,24 +559,29 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) { if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup()) { - mgroup->ConvertToRaid(); + // Queue ConvertToRaid operation + auto convertOp = std::make_unique(master->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); } if (mgroup->isRaidGroup()) { - mgroup->AddMember(bot); + // Queue AddMember operation + auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(addOp)); } } else { - mgroup->AddMember(bot); + // Queue AddMember operation + auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(addOp)); } } else if (master && !group) { - Group* newGroup = new Group(); - newGroup->Create(master); - sGroupMgr->AddGroup(newGroup); - newGroup->AddMember(bot); + // Queue group creation and AddMember operation + auto inviteOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(inviteOp)); } // if (master) // { @@ -1167,7 +1175,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (ObjectAccessor::FindConnectedPlayer(guid)) continue; uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(guid); - if (guildId && PlayerbotAI::IsRealGuild(guildId)) + if (guildId && sPlayerbotGuildMgr->IsRealGuild(guildId)) continue; AddPlayerBot(guid, master->GetSession()->GetAccountId()); messages.push_back("Add class " + std::string(charname)); @@ -1602,8 +1610,26 @@ void PlayerbotMgr::OnBotLoginInternal(Player* const bot) void PlayerbotMgr::OnPlayerLogin(Player* player) { + if (!player) + return; + + WorldSession* session = player->GetSession(); + if (!session) + { + LOG_WARN("playerbots", "Unable to register locale priority for player {} because the session is missing", player->GetName()); + return; + } + + // DB locale (source of bot text translation) + LocaleConstant const databaseLocale = session->GetSessionDbLocaleIndex(); + + // For bot texts (DB-driven), prefer the database locale with a safe fallback. + LocaleConstant usedLocale = databaseLocale; + if (usedLocale >= MAX_LOCALES) + usedLocale = LOCALE_enUS; // fallback + // set locale priority for bot texts - sPlayerbotTextMgr->AddLocalePriority(player->GetSession()->GetSessionDbcLocale()); + sPlayerbotTextMgr->AddLocalePriority(usedLocale); if (sPlayerbotAIConfig->selfBotLevel > 2) HandlePlayerbotCommand("self", player); @@ -1611,7 +1637,7 @@ void PlayerbotMgr::OnPlayerLogin(Player* player) if (!sPlayerbotAIConfig->botAutologin) return; - uint32 accountId = player->GetSession()->GetAccountId(); + uint32 accountId = session->GetAccountId(); QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId); if (results) { diff --git a/src/PlayerbotOperation.h b/src/PlayerbotOperation.h new file mode 100644 index 0000000000..6ac303d323 --- /dev/null +++ b/src/PlayerbotOperation.h @@ -0,0 +1,93 @@ +/* + * 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_OPERATION_H +#define _PLAYERBOT_OPERATION_H + +#include "Common.h" +#include "ObjectGuid.h" +#include + +/** + * @brief Base class for thread-unsafe operations that must be executed in the world thread + * + * PlayerbotOperation represents an operation that needs to be deferred from a map thread + * to the world thread for safe execution. Examples include group modifications, LFG operations, + * guild operations, etc. + * + * Thread Safety: + * - The constructor and data members must be thread-safe (use copies, not pointers) + * - Execute() is called in the world thread and can safely perform thread-unsafe operations + * - Subclasses must not store raw pointers to (core/world thread) game object (use ObjectGuid instead) + */ +class PlayerbotOperation +{ +public: + virtual ~PlayerbotOperation() = default; + + /** + * @brief Execute this operation in the world thread + * + * This method is called by PlayerbotWorldThreadProcessor::Update() which runs in the world thread. + * It's safe to perform any thread-unsafe operation here (Group, LFG, Guild, etc.) + * + * @return true if operation succeeded, false if it failed + */ + virtual bool Execute() = 0; + + /** + * @brief Get the bot GUID this operation is for (optional) + * + * Used for logging and debugging purposes. + * + * @return ObjectGuid of the bot, or ObjectGuid::Empty if not applicable + */ + virtual ObjectGuid GetBotGuid() const { return ObjectGuid::Empty; } + + /** + * @brief Get the operation priority (higher = more urgent) + * + * Priority levels: + * - 100: Critical (crash prevention, cleanup operations) + * - 50: High (player-facing operations like group invites) + * - 10: Normal (background operations) + * - 0: Low (statistics, logging) + * + * @return Priority value (0-100) + */ + virtual uint32 GetPriority() const { return 10; } + + /** + * @brief Get a human-readable name for this operation + * + * Used for logging and debugging. + * + * @return Operation name + */ + virtual std::string GetName() const { return "Unknown Operation"; } + + /** + * @brief Check if this operation is still valid + * + * Called before Execute() to check if the operation should still be executed. + * For example, if a bot logged out, group invite operations for that bot can be skipped. + * + * @return true if operation should be executed, false to skip + */ + virtual bool IsValid() const { return true; } +}; + +/** + * @brief Comparison operator for priority queue (higher priority first) + */ +struct PlayerbotOperationComparator +{ + bool operator()(const std::unique_ptr& a, const std::unique_ptr& b) const + { + return a->GetPriority() < b->GetPriority(); // Lower priority goes to back of queue + } +}; + +#endif diff --git a/src/PlayerbotOperations.h b/src/PlayerbotOperations.h new file mode 100644 index 0000000000..d7c2b47bb8 --- /dev/null +++ b/src/PlayerbotOperations.h @@ -0,0 +1,500 @@ +/* + * 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_OPERATIONS_H +#define _PLAYERBOT_OPERATIONS_H + +#include "Group.h" +#include "GroupMgr.h" +#include "GuildMgr.h" +#include "ObjectAccessor.h" +#include "PlayerbotOperation.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "PlayerbotMgr.h" +#include "PlayerbotDbStore.h" +#include "RandomPlayerbotMgr.h" + +// Group invite operation +class GroupInviteOperation : public PlayerbotOperation +{ +public: + GroupInviteOperation(ObjectGuid botGuid, ObjectGuid targetGuid) + : m_botGuid(botGuid), m_targetGuid(targetGuid) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* target = ObjectAccessor::FindPlayer(m_targetGuid); + + if (!bot || !target) + { + LOG_DEBUG("playerbots", "GroupInviteOperation: Bot or target not found"); + return false; + } + + // Check if target is already in a group + if (target->GetGroup()) + { + LOG_DEBUG("playerbots", "GroupInviteOperation: Target {} is already in a group", target->GetName()); + return false; + } + + Group* group = bot->GetGroup(); + + // Create group if bot doesn't have one + if (!group) + { + group = new Group; + if (!group->Create(bot)) + { + delete group; + LOG_ERROR("playerbots", "GroupInviteOperation: Failed to create group for bot {}", bot->GetName()); + return false; + } + sGroupMgr->AddGroup(group); + LOG_DEBUG("playerbots", "GroupInviteOperation: Created new group for bot {}", bot->GetName()); + } + + // Convert to raid if needed (more than 5 members) + if (!group->isRaidGroup() && group->GetMembersCount() >= 5) + { + group->ConvertToRaid(); + LOG_DEBUG("playerbots", "GroupInviteOperation: Converted group to raid"); + } + + // Add member to group + if (group->AddMember(target)) + { + LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName()); + return true; + } + else + { + LOG_ERROR("playerbots", "GroupInviteOperation: Failed to add {} to group", target->GetName()); + return false; + } + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } // High priority (player-facing) + + std::string GetName() const override { return "GroupInvite"; } + + bool IsValid() const override + { + // Check if bot still exists and is online + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* target = ObjectAccessor::FindPlayer(m_targetGuid); + return bot && target; + } + +private: + ObjectGuid m_botGuid; + ObjectGuid m_targetGuid; +}; + +// Remove member from group +class GroupRemoveMemberOperation : public PlayerbotOperation +{ +public: + GroupRemoveMemberOperation(ObjectGuid botGuid, ObjectGuid targetGuid) + : m_botGuid(botGuid), m_targetGuid(targetGuid) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* target = ObjectAccessor::FindPlayer(m_targetGuid); + + if (!bot || !target) + return false; + + Group* group = bot->GetGroup(); + if (!group) + { + LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Bot is not in a group"); + return false; + } + + if (!group->IsMember(target->GetGUID())) + { + LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Target is not in bot's group"); + return false; + } + + group->RemoveMember(target->GetGUID()); + LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Removed {} from group", target->GetName()); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } + + std::string GetName() const override { return "GroupRemoveMember"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + return bot != nullptr; + } + +private: + ObjectGuid m_botGuid; + ObjectGuid m_targetGuid; +}; + +// Convert group to raid +class GroupConvertToRaidOperation : public PlayerbotOperation +{ +public: + GroupConvertToRaidOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {} + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + if (!bot) + return false; + + Group* group = bot->GetGroup(); + if (!group) + { + LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Bot is not in a group"); + return false; + } + + if (group->isRaidGroup()) + { + LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Group is already a raid"); + return true; // Success - already in desired state + } + + group->ConvertToRaid(); + LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Converted group to raid"); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } + + std::string GetName() const override { return "GroupConvertToRaid"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + return bot != nullptr; + } + +private: + ObjectGuid m_botGuid; +}; + +// Set group leader +class GroupSetLeaderOperation : public PlayerbotOperation +{ +public: + GroupSetLeaderOperation(ObjectGuid botGuid, ObjectGuid newLeaderGuid) + : m_botGuid(botGuid), m_newLeaderGuid(newLeaderGuid) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid); + + if (!bot || !newLeader) + return false; + + Group* group = bot->GetGroup(); + if (!group) + { + LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Bot is not in a group"); + return false; + } + + if (!group->IsMember(newLeader->GetGUID())) + { + LOG_DEBUG("playerbots", "GroupSetLeaderOperation: New leader is not in the group"); + return false; + } + + group->ChangeLeader(newLeader->GetGUID()); + LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Changed leader to {}", newLeader->GetName()); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } + + std::string GetName() const override { return "GroupSetLeader"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid); + return bot && newLeader; + } + +private: + ObjectGuid m_botGuid; + ObjectGuid m_newLeaderGuid; +}; + +// Form arena group +class ArenaGroupFormationOperation : public PlayerbotOperation +{ +public: + ArenaGroupFormationOperation(ObjectGuid leaderGuid, std::vector memberGuids, + uint32 requiredSize, uint32 arenaTeamId, std::string arenaTeamName) + : m_leaderGuid(leaderGuid), m_memberGuids(memberGuids), + m_requiredSize(requiredSize), m_arenaTeamId(arenaTeamId), m_arenaTeamName(arenaTeamName) + { + } + + bool Execute() override + { + Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid); + if (!leader) + { + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Leader not found"); + return false; + } + + // Step 1: Remove all members from their existing groups + for (const ObjectGuid& memberGuid : m_memberGuids) + { + Player* member = ObjectAccessor::FindPlayer(memberGuid); + if (!member) + continue; + + Group* memberGroup = member->GetGroup(); + if (memberGroup) + { + memberGroup->RemoveMember(memberGuid); + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Removed {} from their existing group", + member->GetName()); + } + } + + // Step 2: Disband leader's existing group + Group* leaderGroup = leader->GetGroup(); + if (leaderGroup) + { + leaderGroup->Disband(true); + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Disbanded leader's existing group"); + } + + // Step 3: Create new group with leader + Group* newGroup = new Group(); + if (!newGroup->Create(leader)) + { + delete newGroup; + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to create arena group for leader {}", + leader->GetName()); + return false; + } + + sGroupMgr->AddGroup(newGroup); + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Created new arena group with leader {}", + leader->GetName()); + + // Step 4: Add members to the new group + uint32 addedMembers = 0; + for (const ObjectGuid& memberGuid : m_memberGuids) + { + Player* member = ObjectAccessor::FindPlayer(memberGuid); + if (!member) + { + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} not found, skipping", + memberGuid.ToString()); + continue; + } + + if (member->GetLevel() < 70) + { + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} is below level 70, skipping", + member->GetName()); + continue; + } + + if (newGroup->AddMember(member)) + { + addedMembers++; + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Added {} to arena group", + member->GetName()); + } + else + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to add {} to arena group", + member->GetName()); + } + + if (addedMembers == 0) + { + LOG_ERROR("playerbots", "ArenaGroupFormationOperation: No members were added to the arena group"); + newGroup->Disband(); + return false; + } + + // Step 5: Teleport members to leader and reset AI + for (const ObjectGuid& memberGuid : m_memberGuids) + { + Player* member = ObjectAccessor::FindPlayer(memberGuid); + if (!member || !newGroup->IsMember(memberGuid)) + continue; + + PlayerbotAI* memberBotAI = sPlayerbotsMgr->GetPlayerbotAI(member); + if (memberBotAI) + memberBotAI->Reset(); + + member->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); + member->TeleportTo(leader->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), + leader->GetPositionZ(), 0); + + LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Teleported {} to leader", member->GetName()); + } + + // Check if we have enough members + if (newGroup->GetMembersCount() < m_requiredSize) + { + LOG_INFO("playerbots", "Team #{} <{}> Group is not ready for match (not enough members: {}/{})", + m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount(), m_requiredSize); + newGroup->Disband(); + return false; + } + + LOG_INFO("playerbots", "Team #{} <{}> Group is ready for match with {} members", + m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount()); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_leaderGuid; } + + uint32 GetPriority() const override { return 60; } // Very high priority (arena/BG operations) + + std::string GetName() const override { return "ArenaGroupFormation"; } + + bool IsValid() const override + { + Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid); + return leader != nullptr; + } + +private: + ObjectGuid m_leaderGuid; + std::vector m_memberGuids; + uint32 m_requiredSize; + uint32 m_arenaTeamId; + std::string m_arenaTeamName; +}; + +// Bot logout group cleanup operation +class BotLogoutGroupCleanupOperation : public PlayerbotOperation +{ +public: + BotLogoutGroupCleanupOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {} + + bool Execute() override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + if (!bot) + return false; + + PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot); + if (!botAI) + return false; + + Group* group = bot->GetGroup(); + if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster()) + sPlayerbotDbStore->Save(botAI); + + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + uint32 GetPriority() const override { return 70; } + std::string GetName() const override { return "BotLogoutGroupCleanup"; } + + bool IsValid() const override + { + Player* bot = ObjectAccessor::FindPlayer(m_botGuid); + return bot != nullptr; + } + +private: + ObjectGuid m_botGuid; +}; + +// Add player bot operation (for logging in bots from map threads) +class AddPlayerBotOperation : public PlayerbotOperation +{ +public: + AddPlayerBotOperation(ObjectGuid botGuid, uint32 masterAccountId) + : m_botGuid(botGuid), m_masterAccountId(masterAccountId) + { + } + + bool Execute() override + { + sRandomPlayerbotMgr->AddPlayerBot(m_botGuid, m_masterAccountId); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + + uint32 GetPriority() const override { return 50; } // High priority + + std::string GetName() const override { return "AddPlayerBot"; } + + bool IsValid() const override + { + return !ObjectAccessor::FindConnectedPlayer(m_botGuid); + } + +private: + ObjectGuid m_botGuid; + uint32 m_masterAccountId; +}; + +class OnBotLoginOperation : public PlayerbotOperation +{ +public: + OnBotLoginOperation(ObjectGuid botGuid, PlayerbotHolder* holder) + : m_botGuid(botGuid), m_holder(holder) + { + } + + bool Execute() override + { + Player* bot = ObjectAccessor::FindConnectedPlayer(m_botGuid); + if (!bot || !m_holder) + return false; + + m_holder->OnBotLogin(bot); + return true; + } + + ObjectGuid GetBotGuid() const override { return m_botGuid; } + uint32 GetPriority() const override { return 100; } + std::string GetName() const override { return "OnBotLogin"; } + + bool IsValid() const override + { + return ObjectAccessor::FindConnectedPlayer(m_botGuid) != nullptr; + } + +private: + ObjectGuid m_botGuid; + PlayerbotHolder* m_holder; +}; + +#endif diff --git a/src/PlayerbotSecurity.cpp b/src/PlayerbotSecurity.cpp index 45e8598989..822f40e6d4 100644 --- a/src/PlayerbotSecurity.cpp +++ b/src/PlayerbotSecurity.cpp @@ -251,9 +251,9 @@ bool PlayerbotSecurity::CheckLevelFor(PlayerbotSecurityLevel level, bool silent, out << "I am currently leading a group. I can invite you if you want."; break; case PLAYERBOT_DENY_NOT_LEADER: - if (botAI->GetGroupMaster()) + if (botAI->GetGroupLeader()) { - out << "I am in a group with " << botAI->GetGroupMaster()->GetName() + out << "I am in a group with " << botAI->GetGroupLeader()->GetName() << ". You can ask him for invite."; } else diff --git a/src/PlayerbotTextMgr.cpp b/src/PlayerbotTextMgr.cpp index 3caa96a738..1dce9a29a5 100644 --- a/src/PlayerbotTextMgr.cpp +++ b/src/PlayerbotTextMgr.cpp @@ -190,26 +190,29 @@ bool PlayerbotTextMgr::GetBotText(std::string name, std::string& text, std::map< void PlayerbotTextMgr::AddLocalePriority(uint32 locale) { - if (!locale) + if (locale >= MAX_LOCALES) + { + LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds MAX_LOCALES ({})", locale, MAX_LOCALES - 1); return; + } botTextLocalePriority[locale]++; } uint32 PlayerbotTextMgr::GetLocalePriority() { - uint32 topLocale = 0; - // if no real players online, reset top locale - if (!sWorldSessionMgr->GetActiveSessionCount()) + uint32 const activeSessions = sWorldSessionMgr->GetActiveSessionCount(); + if (!activeSessions) { ResetLocalePriority(); return 0; } + uint32 topLocale = 0; for (uint8 i = 0; i < MAX_LOCALES; ++i) { - if (botTextLocalePriority[i] > topLocale) + if (botTextLocalePriority[i] > botTextLocalePriority[topLocale]) topLocale = i; } diff --git a/src/PlayerbotWorldThreadProcessor.cpp b/src/PlayerbotWorldThreadProcessor.cpp new file mode 100644 index 0000000000..c776eb1207 --- /dev/null +++ b/src/PlayerbotWorldThreadProcessor.cpp @@ -0,0 +1,217 @@ +/* + * 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 "PlayerbotWorldThreadProcessor.h" + +#include "Log.h" +#include "PlayerbotAIConfig.h" + +#include + +PlayerbotWorldThreadProcessor::PlayerbotWorldThreadProcessor() + : m_enabled(true), m_maxQueueSize(10000), m_batchSize(100), m_queueWarningThreshold(80), + m_timeSinceLastUpdate(0), m_updateInterval(50) // Process at least every 50ms +{ + LOG_INFO("playerbots", "PlayerbotWorldThreadProcessor initialized"); +} + +PlayerbotWorldThreadProcessor::~PlayerbotWorldThreadProcessor() { ClearQueue(); } + +PlayerbotWorldThreadProcessor* PlayerbotWorldThreadProcessor::instance() +{ + static PlayerbotWorldThreadProcessor instance; + return &instance; +} + +void PlayerbotWorldThreadProcessor::Update(uint32 diff) +{ + if (!m_enabled) + return; + + // Accumulate time + m_timeSinceLastUpdate += diff; + + // Don't process too frequently to reduce overhead + if (m_timeSinceLastUpdate < m_updateInterval) + return; + + m_timeSinceLastUpdate = 0; + + // Check queue health (warn if getting full) + CheckQueueHealth(); + + // Process a batch of operations + ProcessBatch(); +} + +bool PlayerbotWorldThreadProcessor::QueueOperation(std::unique_ptr operation) +{ + if (!operation) + { + LOG_ERROR("playerbots", "Attempted to queue null operation"); + return false; + } + + std::lock_guard lock(m_queueMutex); + + // Check if queue is full + if (m_operationQueue.size() >= m_maxQueueSize) + { + LOG_ERROR("playerbots", + "PlayerbotWorldThreadProcessor queue is full ({} operations). Dropping operation: {}", + m_maxQueueSize, operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsSkipped++; + return false; + } + + // Queue the operation + m_operationQueue.push(std::move(operation)); + + // Update statistics + { + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = static_cast(m_operationQueue.size()); + m_stats.maxQueueSize = std::max(m_stats.maxQueueSize, m_stats.currentQueueSize); + } + + return true; +} + +void PlayerbotWorldThreadProcessor::ProcessBatch() +{ + // Extract a batch of operations from the queue + std::vector> batch; + batch.reserve(m_batchSize); + + { + std::lock_guard lock(m_queueMutex); + + // Extract up to batchSize operations + while (!m_operationQueue.empty() && batch.size() < m_batchSize) + { + batch.push_back(std::move(m_operationQueue.front())); + m_operationQueue.pop(); + } + + // Update current queue size stat + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = static_cast(m_operationQueue.size()); + } + + // Execute operations outside of lock to avoid blocking queue + uint32 totalExecutionTime = 0; + for (auto& operation : batch) + { + if (!operation) + continue; + + try + { + // Check if operation is still valid + if (!operation->IsValid()) + { + LOG_DEBUG("playerbots", "Skipping invalid operation: {}", operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsSkipped++; + continue; + } + + // Time the execution + uint32 startTime = getMSTime(); + + // Execute the operation + bool success = operation->Execute(); + + uint32 executionTime = GetMSTimeDiffToNow(startTime); + totalExecutionTime += executionTime; + + // Log slow operations + if (executionTime > 100) + LOG_WARN("playerbots", "Slow operation: {} took {}ms", operation->GetName(), executionTime); + + // Update statistics + std::lock_guard statsLock(m_statsMutex); + if (success) + m_stats.totalOperationsProcessed++; + else + { + m_stats.totalOperationsFailed++; + LOG_DEBUG("playerbots", "Operation failed: {}", operation->GetName()); + } + } + catch (std::exception const& e) + { + LOG_ERROR("playerbots", "Exception in operation {}: {}", operation->GetName(), e.what()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsFailed++; + } + catch (...) + { + LOG_ERROR("playerbots", "Unknown exception in operation {}", operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsFailed++; + } + } + + // Update average execution time + if (!batch.empty()) + { + std::lock_guard statsLock(m_statsMutex); + uint32 avgTime = totalExecutionTime / static_cast(batch.size()); + // Exponential moving average + m_stats.averageExecutionTimeMs = + (m_stats.averageExecutionTimeMs * 9 + avgTime) / 10; // 90% old, 10% new + } +} + +void PlayerbotWorldThreadProcessor::CheckQueueHealth() +{ + uint32 queueSize = GetQueueSize(); + uint32 threshold = (m_maxQueueSize * m_queueWarningThreshold) / 100; + + if (queueSize >= threshold) + { + LOG_WARN("playerbots", + "PlayerbotWorldThreadProcessor queue is {}% full ({}/{}). " + "Consider increasing update frequency or batch size.", + (queueSize * 100) / m_maxQueueSize, queueSize, m_maxQueueSize); + } +} + +uint32 PlayerbotWorldThreadProcessor::GetQueueSize() const +{ + std::lock_guard lock(m_queueMutex); + return static_cast(m_operationQueue.size()); +} + +void PlayerbotWorldThreadProcessor::ClearQueue() +{ + std::lock_guard lock(m_queueMutex); + + uint32 cleared = static_cast(m_operationQueue.size()); + if (cleared > 0) + LOG_INFO("playerbots", "Clearing {} queued operations", cleared); + + // Clear the queue + while (!m_operationQueue.empty()) + { + m_operationQueue.pop(); + } + + // Reset queue size stat + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = 0; +} + +PlayerbotWorldThreadProcessor::Statistics PlayerbotWorldThreadProcessor::GetStatistics() const +{ + std::lock_guard statsLock(m_statsMutex); + return m_stats; // Return a copy +} diff --git a/src/PlayerbotWorldThreadProcessor.h b/src/PlayerbotWorldThreadProcessor.h new file mode 100644 index 0000000000..e37d2b5ba5 --- /dev/null +++ b/src/PlayerbotWorldThreadProcessor.h @@ -0,0 +1,142 @@ +/* + * 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_WORLD_THREAD_PROCESSOR_H +#define _PLAYERBOT_WORLD_THREAD_PROCESSOR_H + +#include "Common.h" +#include "PlayerbotOperation.h" + +#include +#include +#include + +/** + * @brief Processes thread-unsafe bot operations in the world thread + * + * The PlayerbotWorldThreadProcessor manages a queue of operations that must be executed + * in the world thread rather than map threads. This ensures thread safety for operations + * like group modifications, LFG, guilds, battlegrounds, etc. + * + * Architecture: + * - Map threads queue operations via QueueOperation() + * - World thread processes operations via Update() (called from WorldScript::OnUpdate) + * - Operations are processed in priority order + * - Thread-safe queue protected by mutex + * + * Usage: + * auto op = std::make_unique(botGuid, params); + * sPlayerbotWorldProcessor->QueueOperation(std::move(op)); + */ +class PlayerbotWorldThreadProcessor +{ +public: + PlayerbotWorldThreadProcessor(); + ~PlayerbotWorldThreadProcessor(); + + static PlayerbotWorldThreadProcessor* instance(); + + /** + * @brief Update and process queued operations (called from world thread) + * + * This method should be called from WorldScript::OnUpdate hook, which runs in the world thread. + * It processes a batch of queued operations. + * + * @param diff Time since last update in milliseconds + */ + void Update(uint32 diff); + + /** + * @brief Queue an operation for execution in the world thread + * + * Thread-safe method that can be called from any thread (typically map threads). + * The operation will be executed later during Update(). + * + * @param operation Unique pointer to the operation (ownership is transferred) + * @return true if operation was queued, false if queue is full + */ + bool QueueOperation(std::unique_ptr operation); + + /** + * @brief Get current queue size + * + * Thread-safe method for monitoring queue size. + * + * @return Number of operations waiting to be processed + */ + uint32 GetQueueSize() const; + + /** + * @brief Clear all queued operations + * + * Used during shutdown or emergency situations. + */ + void ClearQueue(); + + /** + * @brief Get statistics about operation processing + */ + struct Statistics + { + uint64 totalOperationsProcessed = 0; + uint64 totalOperationsFailed = 0; + uint64 totalOperationsSkipped = 0; + uint32 currentQueueSize = 0; + uint32 maxQueueSize = 0; + uint32 averageExecutionTimeMs = 0; + }; + + Statistics GetStatistics() const; + + /** + * @brief Enable/disable operation processing + * + * When disabled, operations are still queued but not processed. + * Useful for testing or temporary suspension. + * + * @param enabled true to enable processing, false to disable + */ + void SetEnabled(bool enabled) { m_enabled = enabled; } + + bool IsEnabled() const { return m_enabled; } + +private: + /** + * @brief Process a single batch of operations + * + * Extracts operations from queue and executes them. + * Called internally by Update(). + */ + void ProcessBatch(); + + /** + * @brief Check if queue is approaching capacity + * + * Logs warning if queue is getting full. + */ + void CheckQueueHealth(); + + // Thread-safe queue + mutable std::mutex m_queueMutex; + std::queue> m_operationQueue; + + // Configuration + bool m_enabled; + uint32 m_maxQueueSize; // Maximum operations in queue + uint32 m_batchSize; // Operations to process per Update() + uint32 m_queueWarningThreshold; // Warn when queue reaches this percentage + + // Statistics + mutable std::mutex m_statsMutex; + Statistics m_stats; + + // Timing + uint32 m_timeSinceLastUpdate; + uint32 m_updateInterval; // Minimum ms between updates +}; + +#define sPlayerbotWorldProcessor PlayerbotWorldThreadProcessor::instance() + +#endif diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index a7217d7bc4..3f39d10de6 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -25,6 +25,8 @@ #include "Metric.h" #include "PlayerScript.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotGuildMgr.h" +#include "PlayerbotWorldThreadProcessor.h" #include "RandomPlayerbotMgr.h" #include "ScriptMgr.h" #include "cs_playerbots.h" @@ -81,12 +83,12 @@ class PlayerbotsPlayerScript : public PlayerScript PlayerbotsPlayerScript() : PlayerScript("PlayerbotsPlayerScript", { PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_AFTER_UPDATE, - PLAYERHOOK_ON_CHAT, - PLAYERHOOK_ON_CHAT_WITH_CHANNEL, - PLAYERHOOK_ON_CHAT_WITH_GROUP, PLAYERHOOK_ON_BEFORE_CRITERIA_PROGRESS, PLAYERHOOK_ON_BEFORE_ACHI_COMPLETE, PLAYERHOOK_CAN_PLAYER_USE_PRIVATE_CHAT, + PLAYERHOOK_CAN_PLAYER_USE_GROUP_CHAT, + PLAYERHOOK_CAN_PLAYER_USE_GUILD_CHAT, + PLAYERHOOK_CAN_PLAYER_USE_CHANNEL_CHAT, PLAYERHOOK_ON_GIVE_EXP, PLAYERHOOK_ON_BEFORE_TELEPORT }) {} @@ -163,14 +165,17 @@ class PlayerbotsPlayerScript : public PlayerScript { botAI->HandleCommand(type, msg, player); - return false; + // hotfix; otherwise the server will crash when whispering logout + // https://github.com/mod-playerbots/mod-playerbots/pull/1838 + // TODO: find the root cause and solve it. (does not happen in party chat) + if (msg == "logout") + return false; } } - return true; } - void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Group* group) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Group* group) override { for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { @@ -182,9 +187,10 @@ class PlayerbotsPlayerScript : public PlayerScript } } } + return true; } - void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* guild) override { if (type == CHAT_MSG_GUILD) { @@ -203,9 +209,10 @@ class PlayerbotsPlayerScript : public PlayerScript } } } + return true; } - void OnPlayerChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override { if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player)) { @@ -216,6 +223,7 @@ class PlayerbotsPlayerScript : public PlayerScript } sRandomPlayerbotMgr->HandleCommand(type, msg, player); + return true; } bool OnPlayerBeforeAchievementComplete(Player* player, AchievementEntry const* achievement) override @@ -300,7 +308,8 @@ class PlayerbotsWorldScript : public WorldScript { public: PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", { - WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED + WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED, + WORLDHOOK_ON_UPDATE }) {} void OnBeforeWorldInitialized() override @@ -329,6 +338,13 @@ class PlayerbotsWorldScript : public WorldScript LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime)); LOG_INFO("server.loading", " "); + LOG_INFO("server.loading", "Playerbots World Thread Processor initialized"); + } + + void OnUpdate(uint32 diff) override + { + sPlayerbotWorldProcessor->Update(diff); + sRandomPlayerbotMgr->UpdateAI(diff); // World thread only } }; @@ -390,8 +406,7 @@ class PlayerbotsScript : public PlayerbotScript void OnPlayerbotUpdate(uint32 diff) override { - sRandomPlayerbotMgr->UpdateAI(diff); - sRandomPlayerbotMgr->UpdateSessions(); + sRandomPlayerbotMgr->UpdateSessions(); // Per-bot updates only } void OnPlayerbotUpdateSessions(Player* player) override @@ -468,6 +483,6 @@ void AddPlayerbotsScripts() new PlayerbotsWorldScript(); new PlayerbotsScript(); new PlayerBotsBGScript(); - AddSC_playerbots_commandscript(); + PlayerBotsGuildValidationScript(); } diff --git a/src/RandomPlayerbotFactory.cpp b/src/RandomPlayerbotFactory.cpp index 4be2d0edfc..5a4672f5eb 100644 --- a/src/RandomPlayerbotFactory.cpp +++ b/src/RandomPlayerbotFactory.cpp @@ -11,6 +11,7 @@ #include "GuildMgr.h" #include "PlayerbotFactory.h" #include "Playerbots.h" +#include "PlayerbotGuildMgr.h" #include "ScriptMgr.h" #include "SharedDefines.h" #include "SocialMgr.h" @@ -754,187 +755,6 @@ void RandomPlayerbotFactory::CreateRandomBots() sPlayerbotAIConfig->randomBotAccounts.size(), totalRandomBotChars); } -void RandomPlayerbotFactory::CreateRandomGuilds() -{ - std::vector randomBots; - - PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BOT); - stmt->SetData(0, "add"); - if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt)) - { - do - { - Field* fields = result->Fetch(); - uint32 bot = fields[0].Get(); - randomBots.push_back(bot); - } while (result->NextRow()); - } - - if (sPlayerbotAIConfig->deleteRandomBotGuilds) - { - LOG_INFO("playerbots", "Deleting random bot guilds..."); - for (std::vector::iterator i = randomBots.begin(); i != randomBots.end(); ++i) - { - if (Guild* guild = sGuildMgr->GetGuildByLeader(ObjectGuid::Create(*i))) - guild->Disband(); - } - - LOG_INFO("playerbots", "Random bot guilds deleted"); - } - - std::unordered_set botAccounts; - botAccounts.reserve(sPlayerbotAIConfig->randomBotAccounts.size()); - for (uint32 acc : sPlayerbotAIConfig->randomBotAccounts) - botAccounts.insert(acc); - - // Recount bot guilds directly from the database (does not depend on connected bots) - uint32 guildNumber = 0; - sPlayerbotAIConfig->randomBotGuilds.clear(); - sPlayerbotAIConfig->randomBotGuilds.shrink_to_fit(); // avoids accumulating old capacity - - if (!botAccounts.empty()) - { - if (QueryResult res = CharacterDatabase.Query( - // We only retrieve what is necessary (guildid, leader account) - "SELECT g.guildid, c.account " - "FROM guild g JOIN characters c ON g.leaderguid = c.guid")) - { - do - { - Field* f = res->Fetch(); - const uint32 guildId = f[0].Get(); - const uint32 accountId = f[1].Get(); - - // Determine if guild leader's account is a bot account. - if (botAccounts.find(accountId) != botAccounts.end()) - { - ++guildNumber; - sPlayerbotAIConfig->randomBotGuilds.push_back(guildId); - } - } while (res->NextRow()); - } - } - - LOG_INFO("playerbots", "{}/{} random bot guilds exist in guild table",guildNumber, sPlayerbotAIConfig->randomBotGuildCount); - if (guildNumber >= sPlayerbotAIConfig->randomBotGuildCount) - { - LOG_DEBUG("playerbots", "No new random guilds required"); - return; - } - - // We list the available leaders (online bots, not in guilds) - GuidVector availableLeaders; - availableLeaders.reserve(randomBots.size()); // limit reallocs - for (const uint32 botLowGuid : randomBots) - { - ObjectGuid leader = ObjectGuid::Create(botLowGuid); - if (sGuildMgr->GetGuildByLeader(leader)) - { - // already GuildLeader -> ignored - continue; - } - else - { - if (Player* player = ObjectAccessor::FindPlayer(leader)) - { - if (!player->GetGuildId()) - availableLeaders.push_back(leader); - } - } - } - LOG_DEBUG("playerbots", "{} available leaders for new guilds found", availableLeaders.size()); - - // Create up to randomBotGuildCount by counting only EFFECTIVE creations - uint32 createdThisRun = 0; - for (; guildNumber < sPlayerbotAIConfig->randomBotGuildCount; /* ++guildNumber -> done only if creation */) - { - std::string const guildName = CreateRandomGuildName(); - if (guildName.empty()) - break; // no more names available in playerbots_guild_names - - if (sGuildMgr->GetGuildByName(guildName)) - continue; // name already taken, skip - - if (availableLeaders.empty()) - { - LOG_ERROR("playerbots", "No leaders for random guilds available"); - break; // no more leaders: we can no longer progress without distorting the counter - } - - uint32 index = urand(0, availableLeaders.size() - 1); - ObjectGuid leader = availableLeaders[index]; - availableLeaders.erase(availableLeaders.begin() + index); // Removes the chosen leader to avoid re-selecting it repeatedly - - Player* player = ObjectAccessor::FindPlayer(leader); - if (!player) - { - LOG_ERROR("playerbots", "ObjectAccessor Cannot find player to set leader for guild {} . Skipped...", - guildName.c_str()); - // we will try with other leaders in the next round (guildNumber is not incremented) - continue; - } - - if (player->GetGuildId()) - { - // leader already in guild -> we don't advance the counter, we move on to the next one - continue; - } - - LOG_DEBUG("playerbots", "Creating guild name='{}' leader='{}'...", guildName.c_str(), player->GetName().c_str()); - - Guild* guild = new Guild(); - if (!guild->Create(player, guildName)) - { - LOG_ERROR("playerbots", "Error creating guild [ {} ] with leader [ {} ]", guildName.c_str(), - player->GetName().c_str()); - delete guild; - continue; - } - - sGuildMgr->AddGuild(guild); - - LOG_DEBUG("playerbots", "Guild created: id={} name='{}'", guild->GetId(), guildName.c_str()); - - // create random emblem - uint32 st, cl, br, bc, bg; - bg = urand(0, 51); - bc = urand(0, 17); - cl = urand(0, 17); - br = urand(0, 7); - st = urand(0, 180); - - LOG_DEBUG("playerbots", - "[TABARD] new guild id={} random -> style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", - guild->GetId(), st, cl, br, bc, bg); - - // populate guild table with a random tabard design - CharacterDatabase.Execute( - "UPDATE guild SET EmblemStyle={}, EmblemColor={}, BorderStyle={}, BorderColor={}, BackgroundColor={} " - "WHERE guildid={}", - st, cl, br, bc, bg, guild->GetId()); - LOG_DEBUG("playerbots", "[TABARD] UPDATE done for guild id={}", guild->GetId()); - - // Immediate reading for log - if (QueryResult qr = CharacterDatabase.Query( - "SELECT EmblemStyle,EmblemColor,BorderStyle,BorderColor,BackgroundColor FROM guild WHERE guildid={}", - guild->GetId())) - { - Field* f = qr->Fetch(); - LOG_DEBUG("playerbots", - "[TABARD] DB check guild id={} => style={}, color={}, borderStyle={}, borderColor={}, bgColor={}", - guild->GetId(), f[0].Get(), f[1].Get(), f[2].Get(), f[3].Get(), f[4].Get()); - } - - sPlayerbotAIConfig->randomBotGuilds.push_back(guild->GetId()); - // The guild is only counted if it is actually created - ++guildNumber; - ++createdThisRun; - } - - // Shows the true total and how many were created during this run - LOG_INFO("playerbots", "{} random bot guilds created this run)", createdThisRun); -} - std::string const RandomPlayerbotFactory::CreateRandomGuildName() { std::string guildName = ""; diff --git a/src/RandomPlayerbotFactory.h b/src/RandomPlayerbotFactory.h index d6c19e454b..92f2c9f33c 100644 --- a/src/RandomPlayerbotFactory.h +++ b/src/RandomPlayerbotFactory.h @@ -51,7 +51,6 @@ class RandomPlayerbotFactory Player* CreateRandomBot(WorldSession* session, uint8 cls, std::unordered_map>& names); static void CreateRandomBots(); - static void CreateRandomGuilds(); static void CreateRandomArenaTeams(ArenaType slot, uint32 count); static std::string const CreateRandomGuildName(); static uint32 CalculateTotalAccountCount(); diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index 89a960aecb..1f94e35e13 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -1480,10 +1480,10 @@ bool RandomPlayerbotMgr::ProcessBot(uint32 bot) if (!sRandomPlayerbotMgr->IsRandomBot(player)) update = false; - if (player->GetGroup() && botAI->GetGroupMaster()) + if (player->GetGroup() && botAI->GetGroupLeader()) { - PlayerbotAI* groupMasterBotAI = GET_PLAYERBOT_AI(botAI->GetGroupMaster()); - if (!groupMasterBotAI || groupMasterBotAI->IsRealPlayer()) + PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader()); + if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer()) { update = false; } diff --git a/src/factory/PlayerbotFactory.cpp b/src/factory/PlayerbotFactory.cpp index 65ffc444ee..37dd3906b2 100644 --- a/src/factory/PlayerbotFactory.cpp +++ b/src/factory/PlayerbotFactory.cpp @@ -30,6 +30,7 @@ #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" +#include "PlayerbotGuildMgr.h" #include "Playerbots.h" #include "QuestDef.h" #include "RandomItemMgr.h" @@ -3965,45 +3966,36 @@ void PlayerbotFactory::InitInventoryEquip() void PlayerbotFactory::InitGuild() { if (bot->GetGuildId()) - return; - - // bot->SaveToDB(false, false); - - // add guild tabard - if (bot->GetGuildId() && !bot->HasItemCount(5976, 1)) - StoreItem(5976, 1); - - if (sPlayerbotAIConfig->randomBotGuilds.empty()) - RandomPlayerbotFactory::CreateRandomGuilds(); - - std::vector guilds; - for (std::vector::iterator i = sPlayerbotAIConfig->randomBotGuilds.begin(); - i != sPlayerbotAIConfig->randomBotGuilds.end(); ++i) - guilds.push_back(*i); - - if (guilds.empty()) { - LOG_ERROR("playerbots", "No random guilds available"); + if (!bot->HasItemCount(5976, 1) && bot->GetLevel() > 9) + StoreItem(5976, 1); return; } - int index = urand(0, guilds.size() - 1); - uint32 guildId = guilds[index]; - Guild* guild = sGuildMgr->GetGuildById(guildId); + std::string guildName = sPlayerbotGuildMgr->AssignToGuild(bot); + if (guildName.empty()) + return; + + Guild* guild = sGuildMgr->GetGuildByName(guildName); if (!guild) { - LOG_ERROR("playerbots", "Invalid guild {}", guildId); + if (!sPlayerbotGuildMgr->CreateGuild(bot, guildName)) + LOG_ERROR("playerbots","Failed to create guild {} for bot {}", guildName, bot->GetName()); return; } - - if (guild->GetMemberSize() < urand(10, sPlayerbotAIConfig->randomBotGuildSizeMax)) - guild->AddMember(bot->GetGUID(), urand(GR_OFFICER, GR_INITIATE)); - + else + { + if (guild->AddMember(bot->GetGUID(),urand(GR_OFFICER, GR_INITIATE))) + { + LOG_DEBUG("playerbots","Bot {} joined guild {}.", bot->GetName(), guildName); + sPlayerbotGuildMgr->OnGuildUpdate(guild); + } + else + LOG_ERROR("playerbots","Bot {} failed to join guild {}.", bot->GetName(), guildName); + } // add guild tabard if (bot->GetGuildId() && bot->GetLevel() > 9 && urand(0, 4) && !bot->HasItemCount(5976, 1)) StoreItem(5976, 1); - - // bot->SaveToDB(false, false); } void PlayerbotFactory::InitImmersive() @@ -4099,6 +4091,7 @@ void PlayerbotFactory::InitImmersive() void PlayerbotFactory::InitArenaTeam() { + if (!sPlayerbotAIConfig->IsInRandomAccountList(bot->GetSession()->GetAccountId())) return; @@ -4185,10 +4178,34 @@ void PlayerbotFactory::InitArenaTeam() if (botcaptain && botcaptain->GetTeamId() == bot->GetTeamId()) // need? { + // Add bot to arena team arenateam->AddMember(bot->GetGUID()); - arenateam->SaveToDB(); + + // Only synchronize ratings once the team is full (avoid redundant work) + // The captain was added with incorrect ratings when the team was created, + // so we fix everyone's ratings once the roster is complete + if (arenateam->GetMembersSize() >= (uint32)arenateam->GetType()) + { + uint32 teamRating = arenateam->GetRating(); + + // Use SetRatingForAll to align all members with team rating + arenateam->SetRatingForAll(teamRating); + + // For bot-only teams, keep MMR synchronized with team rating + // This ensures matchmaking reflects the artificial team strength (1000-2000 range) + // instead of being influenced by the global CONFIG_ARENA_START_MATCHMAKER_RATING + for (auto& member : arenateam->GetMembers()) + { + // Set MMR to match personal rating (which already matches team rating) + member.MatchMakerRating = member.PersonalRating; + member.MaxMMR = std::max(member.MaxMMR, member.PersonalRating); + } + // Force save all member data to database + arenateam->SaveToDB(true); + } } } + arenateams.erase(arenateams.begin() + index); } diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h index cebd7615a7..09c9145f93 100644 --- a/src/strategy/actions/ActionContext.h +++ b/src/strategy/actions/ActionContext.h @@ -121,7 +121,7 @@ class ActionContext : public NamedObjectContext creators["shoot"] = &ActionContext::shoot; creators["follow"] = &ActionContext::follow; creators["move from group"] = &ActionContext::move_from_group; - creators["flee to master"] = &ActionContext::flee_to_master; + creators["flee to group leader"] = &ActionContext::flee_to_group_leader; creators["runaway"] = &ActionContext::runaway; creators["stay"] = &ActionContext::stay; creators["sit"] = &ActionContext::sit; @@ -318,7 +318,7 @@ class ActionContext : public NamedObjectContext static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); } static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); } static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); } - static Action* flee_to_master(PlayerbotAI* botAI) { return new FleeToMasterAction(botAI); } + static Action* flee_to_group_leader(PlayerbotAI* botAI) { return new FleeToGroupLeaderAction(botAI); } static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); } static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); } static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); } diff --git a/src/strategy/actions/AttackAction.cpp b/src/strategy/actions/AttackAction.cpp index 4ea8694378..38cb96835f 100644 --- a/src/strategy/actions/AttackAction.cpp +++ b/src/strategy/actions/AttackAction.cpp @@ -84,9 +84,10 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/) return false; } - if ((sPlayerbotAIConfig->IsInPvpProhibitedZone(bot->GetZoneId()) || - sPlayerbotAIConfig->IsInPvpProhibitedArea(bot->GetAreaId())) - && (target->IsPlayer() || target->IsPet())) + // Check if bot OR target is in prohibited zone/area + if ((target->IsPlayer() || target->IsPet()) && + (sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) || + sPlayerbotAIConfig->IsPvpProhibited(target->GetZoneId(), target->GetAreaId()))) { if (verbose) botAI->TellError("I cannot attack other players in PvP prohibited areas."); diff --git a/src/strategy/actions/ChooseRpgTargetAction.cpp b/src/strategy/actions/ChooseRpgTargetAction.cpp index 20151658cc..0d441e9b67 100644 --- a/src/strategy/actions/ChooseRpgTargetAction.cpp +++ b/src/strategy/actions/ChooseRpgTargetAction.cpp @@ -311,7 +311,7 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldObject* target) bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - Player* gmaster = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Player* realMaster = botAI->GetMaster(); AiObjectContext* context = botAI->GetAiObjectContext(); @@ -327,30 +327,30 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) return false; } - if (!gmaster || bot == gmaster) + if (!groupLeader || bot == groupLeader) return true; if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) return true; - if (bot->GetDistance(gmaster) > sPlayerbotAIConfig->rpgDistance * 2) + if (bot->GetDistance(groupLeader) > sPlayerbotAIConfig->rpgDistance * 2) return false; Formation* formation = AI_VALUE(Formation*, "formation"); - float distance = gmaster->GetDistance2d(pos.getX(), pos.getY()); + float distance = groupLeader->GetDistance2d(pos.getX(), pos.getY()); if (!botAI->HasActivePlayerMaster() && distance < 50.0f) { - Player* player = gmaster; - if (gmaster && !gmaster->isMoving() || + Player* player = groupLeader; + if (groupLeader && !groupLeader->isMoving() || PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance) return true; } - if ((inDungeon || !gmaster->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == gmaster && distance > 5.0f) + if ((inDungeon || !groupLeader->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == groupLeader && distance > 5.0f) return false; - if (!gmaster->isMoving() && distance < 25.0f) + if (!groupLeader->isMoving() && distance < 25.0f) return true; if (distance < formation->GetMaxDistance()) diff --git a/src/strategy/actions/ChooseTravelTargetAction.cpp b/src/strategy/actions/ChooseTravelTargetAction.cpp index 3843f35876..cf6dddd40c 100644 --- a/src/strategy/actions/ChooseTravelTargetAction.cpp +++ b/src/strategy/actions/ChooseTravelTargetAction.cpp @@ -180,7 +180,7 @@ void ChooseTravelTargetAction::getNewTarget(TravelTarget* newTarget, TravelTarge void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget) { // Tell the master where we are going. - if (!bot->GetGroup() || (botAI->GetGroupMaster() == bot)) + if (!bot->GetGroup() || (botAI->GetGroupLeader() == bot)) ReportTravelTarget(newTarget, oldTarget); // If we are heading to a creature/npc clear it from the ignore list. diff --git a/src/strategy/actions/FollowActions.cpp b/src/strategy/actions/FollowActions.cpp index f4a66bf9e7..d168e8cc93 100644 --- a/src/strategy/actions/FollowActions.cpp +++ b/src/strategy/actions/FollowActions.cpp @@ -70,7 +70,7 @@ bool FollowAction::isUseful() if (!target.empty()) fTarget = AI_VALUE(Unit*, target); else - fTarget = AI_VALUE(Unit*, "master target"); + fTarget = AI_VALUE(Unit*, "group leader"); if (fTarget) { @@ -114,9 +114,9 @@ bool FollowAction::CanDeadFollow(Unit* target) return true; } -bool FleeToMasterAction::Execute(Event event) +bool FleeToGroupLeaderAction::Execute(Event event) { - Unit* fTarget = AI_VALUE(Unit*, "master target"); + Unit* fTarget = AI_VALUE(Unit*, "group leader"); bool canFollow = Follow(fTarget); if (!canFollow) { @@ -146,22 +146,22 @@ bool FleeToMasterAction::Execute(Event event) return true; } -bool FleeToMasterAction::isUseful() +bool FleeToGroupLeaderAction::isUseful() { - if (!botAI->GetGroupMaster()) + if (!botAI->GetGroupLeader()) return false; - if (botAI->GetGroupMaster() == bot) + if (botAI->GetGroupLeader() == bot) return false; Unit* target = AI_VALUE(Unit*, "current target"); - if (target && botAI->GetGroupMaster()->GetTarget() == target->GetGUID()) + if (target && botAI->GetGroupLeader()->GetTarget() == target->GetGUID()) return false; if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) return false; - Unit* fTarget = AI_VALUE(Unit*, "master target"); + Unit* fTarget = AI_VALUE(Unit*, "group leader"); if (!CanDeadFollow(fTarget)) return false; diff --git a/src/strategy/actions/FollowActions.h b/src/strategy/actions/FollowActions.h index 5468e34131..9331bbf556 100644 --- a/src/strategy/actions/FollowActions.h +++ b/src/strategy/actions/FollowActions.h @@ -20,10 +20,10 @@ class FollowAction : public MovementAction bool CanDeadFollow(Unit* target); }; -class FleeToMasterAction : public FollowAction +class FleeToGroupLeaderAction : public FollowAction { public: - FleeToMasterAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to master") {} + FleeToGroupLeaderAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to group leader") {} bool Execute(Event event) override; bool isUseful() override; diff --git a/src/strategy/actions/GroupAction.cpp b/src/strategy/actions/GroupAction.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/strategy/actions/GroupAction.h b/src/strategy/actions/GroupAction.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/strategy/actions/GuildManagementActions.cpp b/src/strategy/actions/GuildManagementActions.cpp index 4ab6d72c59..f00a955e7c 100644 --- a/src/strategy/actions/GuildManagementActions.cpp +++ b/src/strategy/actions/GuildManagementActions.cpp @@ -58,6 +58,14 @@ Player* GuidManageAction::GetPlayer(Event event) return nullptr; } +void GuidManageAction::SendPacket(WorldPacket const& packet) +{ + // make a heap copy because QueuePacket takes ownership + WorldPacket* data = new WorldPacket(packet); + + bot->GetSession()->QueuePacket(data); +} + bool GuidManageAction::Execute(Event event) { Player* player = GetPlayer(event); @@ -84,12 +92,6 @@ bool GuildInviteAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_INVITE); } -void GuildInviteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildInviteByName data = WorldPacket(packet); - bot->GetSession()->HandleGuildInviteOpcode(data); -} - bool GuildInviteAction::PlayerIsValid(Player* member) { return !member->GetGuildId() && (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) || @@ -101,12 +103,6 @@ bool GuildPromoteAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_PROMOTE); } -void GuildPromoteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildPromoteMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildPromoteOpcode(data); -} - bool GuildPromoteAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member) - 1; @@ -117,12 +113,6 @@ bool GuildDemoteAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_DEMOTE); } -void GuildDemoteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildDemoteMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildDemoteOpcode(data); -} - bool GuildDemoteAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); @@ -133,12 +123,6 @@ bool GuildRemoveAction::isUseful() return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_REMOVE); } -void GuildRemoveAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildOfficerRemoveMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildRemoveOpcode(data); -} - bool GuildRemoveAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); diff --git a/src/strategy/actions/GuildManagementActions.h b/src/strategy/actions/GuildManagementActions.h index bf46d1b741..b1d363e89a 100644 --- a/src/strategy/actions/GuildManagementActions.h +++ b/src/strategy/actions/GuildManagementActions.h @@ -25,7 +25,7 @@ class GuidManageAction : public Action bool isUseful() override { return false; } protected: - virtual void SendPacket(WorldPacket data){}; + virtual void SendPacket(WorldPacket const& packet); virtual Player* GetPlayer(Event event); virtual bool PlayerIsValid(Player* member); virtual uint8 GetRankId(Player* member); @@ -44,7 +44,6 @@ class GuildInviteAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -59,7 +58,6 @@ class GuildPromoteAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -74,7 +72,6 @@ class GuildDemoteAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -89,7 +86,6 @@ class GuildRemoveAction : public GuidManageAction bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; diff --git a/src/strategy/actions/InviteToGroupAction.cpp b/src/strategy/actions/InviteToGroupAction.cpp index 7af26210c6..bec515fefb 100644 --- a/src/strategy/actions/InviteToGroupAction.cpp +++ b/src/strategy/actions/InviteToGroupAction.cpp @@ -9,7 +9,9 @@ #include "Event.h" #include "GuildMgr.h" #include "Log.h" +#include "PlayerbotOperations.h" #include "Playerbots.h" +#include "PlayerbotWorldThreadProcessor.h" #include "ServerFacade.h" bool InviteToGroupAction::Invite(Player* inviter, Player* player) @@ -27,7 +29,10 @@ bool InviteToGroupAction::Invite(Player* inviter, Player* player) { if (GET_PLAYERBOT_AI(player) && !GET_PLAYERBOT_AI(player)->IsRealPlayer()) if (!group->isRaidGroup() && group->GetMembersCount() > 4) - group->ConvertToRaid(); + { + auto convertOp = std::make_unique(inviter->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } } WorldPacket p; @@ -89,7 +94,10 @@ bool InviteNearbyToGroupAction::Execute(Event event) // When inviting the 5th member of the group convert to raid for future invites. if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && bot->GetGroup()->GetMembersCount() > 3) - group->ConvertToRaid(); + { + auto convertOp = std::make_unique(bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot)) { @@ -133,7 +141,7 @@ bool InviteNearbyToGroupAction::isUseful() if (group->isRaidGroup() && group->IsFull()) return false; - if (botAI->GetGroupMaster() != bot) + if (botAI->GetGroupLeader() != bot) return false; uint32 memberCount = group->GetMembersCount(); @@ -221,7 +229,8 @@ bool InviteGuildToGroupAction::Execute(Event event) if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && bot->GetGroup()->GetMembersCount() > 3) { - group->ConvertToRaid(); + auto convertOp = std::make_unique(bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); } if (sPlayerbotAIConfig->inviteChat && @@ -362,7 +371,10 @@ bool LfgAction::Execute(Event event) if (param.empty() || param == "5" || group->isRaidGroup()) return false; // Group or raid is full so stop trying. else - group->ConvertToRaid(); // We want a raid but are in a group so convert and continue. + { + auto convertOp = std::make_unique(requester->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } } Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); diff --git a/src/strategy/actions/LeaveGroupAction.cpp b/src/strategy/actions/LeaveGroupAction.cpp index 039c476bf5..a279c9426c 100644 --- a/src/strategy/actions/LeaveGroupAction.cpp +++ b/src/strategy/actions/LeaveGroupAction.cpp @@ -109,22 +109,22 @@ bool LeaveFarAwayAction::isUseful() if (!bot->GetGroup()) return false; - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Player* trueMaster = botAI->GetMaster(); - if (!master || (bot == master && !botAI->IsRealPlayer())) + if (!groupLeader || (bot == groupLeader && !botAI->IsRealPlayer())) return false; - PlayerbotAI* masterBotAI = nullptr; - if (master) - masterBotAI = GET_PLAYERBOT_AI(master); - if (master && !masterBotAI) + PlayerbotAI* groupLeaderBotAI = nullptr; + if (groupLeader) + groupLeaderBotAI = GET_PLAYERBOT_AI(groupLeader); + if (groupLeader && !groupLeaderBotAI) return false; if (trueMaster && !GET_PLAYERBOT_AI(trueMaster)) return false; if (botAI->IsAlt() && - (!masterBotAI || masterBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player master. + (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player groupLeader. return false; if (botAI->GetGrouperType() == GrouperType::SOLO) @@ -138,19 +138,19 @@ bool LeaveFarAwayAction::isUseful() if (dCount > 4 && !botAI->HasRealPlayerMaster()) return true; - if (bot->GetGuildId() == master->GetGuildId()) + if (bot->GetGuildId() == groupLeader->GetGuildId()) { - if (bot->GetLevel() > master->GetLevel() + 5) + if (bot->GetLevel() > groupLeader->GetLevel() + 5) { if (AI_VALUE(bool, "should get money")) return false; } } - if (abs(int32(master->GetLevel() - bot->GetLevel())) > 4) + if (abs(int32(groupLeader->GetLevel() - bot->GetLevel())) > 4) return true; - if (bot->GetMapId() != master->GetMapId() || bot->GetDistance2d(master) >= 2 * sPlayerbotAIConfig->rpgDistance) + if (bot->GetMapId() != groupLeader->GetMapId() || bot->GetDistance2d(groupLeader) >= 2 * sPlayerbotAIConfig->rpgDistance) { return true; } diff --git a/src/strategy/actions/MoveToTravelTargetAction.cpp b/src/strategy/actions/MoveToTravelTargetAction.cpp index a4dec2caac..ed60339b82 100644 --- a/src/strategy/actions/MoveToTravelTargetAction.cpp +++ b/src/strategy/actions/MoveToTravelTargetAction.cpp @@ -18,7 +18,7 @@ bool MoveToTravelTargetAction::Execute(Event event) WorldLocation location = *target->getPosition(); Group* group = bot->GetGroup(); - if (group && !urand(0, 1) && bot == botAI->GetGroupMaster() && !bot->IsInCombat()) + if (group && !urand(0, 1) && bot == botAI->GetGroupLeader() && !bot->IsInCombat()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { diff --git a/src/strategy/actions/MovementActions.cpp b/src/strategy/actions/MovementActions.cpp index 15e098a521..f66638a7a1 100644 --- a/src/strategy/actions/MovementActions.cpp +++ b/src/strategy/actions/MovementActions.cpp @@ -971,73 +971,86 @@ bool MovementAction::Follow(Unit* target, float distance) { return Follow(target void MovementAction::UpdateMovementState() { - // state flags - const float gLvlZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); - const bool onGround = bot->GetPositionZ() < gLvlZ + 1.f; - const bool wantsToFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); - const auto master = botAI ? botAI->GetMaster() : nullptr; // real or not - const bool masterIsFlying = master && master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); - const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); - const auto liquidState = bot->GetLiquidData().Status; // default LIQUID_MAP_NO_WATER - const bool isWaterArea = liquidState != LIQUID_MAP_NO_WATER; - const bool isUnderWater = liquidState == LIQUID_MAP_UNDER_WATER; - const bool isInWater = liquidState == LIQUID_MAP_IN_WATER; - const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - const bool wantsToWaterWalk = bot->HasWaterWalkAura(); - const bool wantsToSwim = isInWater || isUnderWater; - - // handle water state - if (isWaterArea) - { - // water walking - if (wantsToWaterWalk && !isWaterWalking && !isUnderWater && !isFlying) - { - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - bot->AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - bot->SendMovementFlagUpdate(); + const bool isCurrentlyRestricted = // see if the bot is currently slowed, rooted, or otherwise unable to move + bot->isFrozen() || + bot->IsPolymorphed() || + bot->HasRootAura() || + bot->HasStunAura() || + bot->HasConfuseAura() || + bot->HasUnitState(UNIT_STATE_LOST_CONTROL); + + // no update movement flags while movement is current restricted. + if (!isCurrentlyRestricted && bot->IsAlive()) + { + // state flags + const auto master = botAI ? botAI->GetMaster() : nullptr; // real player or not + const bool masterIsFlying = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) : true; + const bool masterIsSwimming = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING) : true; + const auto liquidState = bot->GetLiquidData().Status; // default LIQUID_MAP_NO_WATER + const float gZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + const bool wantsToFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); + const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); + const bool isWaterArea = liquidState != LIQUID_MAP_NO_WATER; + const bool isUnderWater = liquidState == LIQUID_MAP_UNDER_WATER; + const bool isInWater = liquidState == LIQUID_MAP_IN_WATER; + const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + const bool wantsToWaterWalk = bot->HasWaterWalkAura(); + const bool wantsToSwim = isInWater || isUnderWater; + const bool onGroundZ = (bot->GetPositionZ() < gZ + 1.f) && !isWaterArea; + bool movementFlagsUpdated = false; + + // handle water state + if (isWaterArea && !isFlying) + { + // water walking + if (wantsToWaterWalk && !isWaterWalking && !masterIsSwimming) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + bot->AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + movementFlagsUpdated = true; + } + // swimming + else if (wantsToSwim && !isSwimming && masterIsSwimming) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + bot->AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING); + movementFlagsUpdated = true; + } } - // swimming - else if (wantsToSwim && !isSwimming && !wantsToWaterWalk && !isFlying) + else if (isSwimming || isWaterWalking) { + // reset water flags + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - bot->AddUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - bot->SendMovementFlagUpdate(); + movementFlagsUpdated = true; } - } - else - { - // reset flags, if not will inherit incorrect walk speed here and there - // when transistions between land and water. - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_SWIMMING); - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - bot->SendMovementFlagUpdate(); - } - // handle flying state - if (wantsToFly && !isFlying && masterIsFlying) - { - bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); - bot->SendMovementFlagUpdate(); - } - else if ((!wantsToFly || onGround) && isFlying) - { - bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); - bot->SendMovementFlagUpdate(); - } + // handle flying state + if (wantsToFly && !isFlying && masterIsFlying) + { + bot->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + bot->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); + movementFlagsUpdated = true; + } + else if ((!wantsToFly || onGroundZ) && isFlying) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); + movementFlagsUpdated = true; + } - // See if the bot is currently slowed, rooted, or otherwise unable to move - bool isCurrentlyRestricted = bot->isFrozen() || bot->IsPolymorphed() || bot->HasRootAura() || bot->HasStunAura() || - bot->HasConfuseAura() || bot->HasUnitState(UNIT_STATE_LOST_CONTROL); + // detect if movement restrictions have been lifted, CC just ended. + if (wasMovementRestricted) + movementFlagsUpdated = true; // refresh movement state to ensure animations play correctly - // Detect if movement restrictions have been lifted - if (wasMovementRestricted && !isCurrentlyRestricted && bot->IsAlive()) - { - // CC just ended - refresh movement state to ensure animations play correctly - bot->SendMovementFlagUpdate(); + if (movementFlagsUpdated) + bot->SendMovementFlagUpdate(); } - // Save current state for the next check + // Save current state for the next check wasMovementRestricted = isCurrentlyRestricted; // Temporary speed increase in group @@ -1089,7 +1102,7 @@ void MovementAction::UpdateMovementState() // { // if (Unit* pTarget = sServerFacade->GetChaseTarget(bot)) // { - // if (pTarget != botAI->GetGroupMaster()) + // if (pTarget != botAI->GetGroupLeader()) // return; // if (!bot->IsWithinMeleeRange(pTarget)) @@ -1820,25 +1833,12 @@ void MovementAction::DoMovePoint(Unit* unit, float x, float y, float z, bool gen if (!mm) return; - // enable flying - if (unit->HasUnitMovementFlag(MOVEMENTFLAG_FLYING)) - { - unit->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); - unit->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); - } - else - { - unit->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); - unit->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); - } - // enable water walking if (unit->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING)) { - float gLvlZ = unit->GetMapWaterOrGroundLevel(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()); - unit->UpdatePosition(unit->GetPositionX(), unit->GetPositionY(), gLvlZ, false); - // z = gLvlZ; do not overwrite Z axex, otherwise you wont be able to steer the bots into swimming when water - // walking. + float gZ = unit->GetMapWaterOrGroundLevel(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()); + unit->UpdatePosition(unit->GetPositionX(), unit->GetPositionY(), gZ, false); + // z = gZ; no overwrite Z axe otherwise you cant steer the bots into swimming when water walking. } mm->Clear(); @@ -2663,7 +2663,7 @@ bool DisperseSetAction::Execute(Event event) return true; } -bool RunAwayAction::Execute(Event event) { return Flee(AI_VALUE(Unit*, "master target")); } +bool RunAwayAction::Execute(Event event) { return Flee(AI_VALUE(Unit*, "group leader")); } bool MoveToLootAction::Execute(Event event) { diff --git a/src/strategy/actions/PassLeadershipToMasterAction.cpp b/src/strategy/actions/PassLeadershipToMasterAction.cpp index ceb1fbbcf2..87890c1c57 100644 --- a/src/strategy/actions/PassLeadershipToMasterAction.cpp +++ b/src/strategy/actions/PassLeadershipToMasterAction.cpp @@ -6,16 +6,17 @@ #include "PassLeadershipToMasterAction.h" #include "Event.h" +#include "PlayerbotOperations.h" #include "Playerbots.h" +#include "PlayerbotWorldThreadProcessor.h" bool PassLeadershipToMasterAction::Execute(Event event) { if (Player* master = GetMaster()) if (master && master != bot && bot->GetGroup() && bot->GetGroup()->IsMember(master->GetGUID())) { - WorldPacket p(SMSG_GROUP_SET_LEADER, 8); - p << master->GetGUID(); - bot->GetSession()->HandleGroupSetLeaderOpcode(p); + auto setLeaderOp = std::make_unique(bot->GetGUID(), master->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(setLeaderOp)); if (!message.empty()) botAI->TellMasterNoFacing(message); diff --git a/src/strategy/actions/PetsAction.cpp b/src/strategy/actions/PetsAction.cpp index 945a883672..9e9e3172c4 100644 --- a/src/strategy/actions/PetsAction.cpp +++ b/src/strategy/actions/PetsAction.cpp @@ -25,7 +25,9 @@ bool PetsAction::Execute(Event event) if (param.empty()) { // If no parameter is provided, show usage instructions and return. - botAI->TellError("Usage: pet "); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_usage_error", "Usage: pet ", {}); + botAI->TellError(text); return false; } @@ -52,7 +54,9 @@ bool PetsAction::Execute(Event event) // If no pets or guardians are found, notify and return. if (targets.empty()) { - botAI->TellError("You have no pet or guardian pet."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_no_pet_error", "You have no pet or guardian pet.", {}); + botAI->TellError(text); return false; } @@ -63,42 +67,54 @@ bool PetsAction::Execute(Event event) if (param == "aggressive") { react = REACT_AGGRESSIVE; - stanceText = "aggressive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_aggressive", "aggressive", {}); } else if (param == "defensive") { react = REACT_DEFENSIVE; - stanceText = "defensive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_defensive", "defensive", {}); } else if (param == "passive") { react = REACT_PASSIVE; - stanceText = "passive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_passive", "passive", {}); } // The "stance" command simply reports the current stance of each pet/guardian. else if (param == "stance") { for (Creature* target : targets) { - std::string type = target->IsPet() ? "pet" : "guardian"; + std::string type = target->IsPet() ? + sPlayerbotTextMgr->GetBotTextOrDefault("pet_type_pet", "pet", {}) : + sPlayerbotTextMgr->GetBotTextOrDefault("pet_type_guardian", "guardian", {}); std::string name = target->GetName(); std::string stance; switch (target->GetReactState()) { case REACT_AGGRESSIVE: - stance = "aggressive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_aggressive", "aggressive", {}); break; case REACT_DEFENSIVE: - stance = "defensive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_defensive", "defensive", {}); break; case REACT_PASSIVE: - stance = "passive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_passive", "passive", {}); break; default: - stance = "unknown"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_unknown", "unknown", {}); break; } - botAI->TellMaster("Current stance of " + type + " \"" + name + "\": " + stance + "."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_report", "Current stance of %type \"%name\": %stance.", + {{"type", type}, {"name", name}, {"stance", stance}}); + botAI->TellMaster(text); } return true; } @@ -121,17 +137,31 @@ bool PetsAction::Execute(Event event) // If no valid target is selected, show an error and return. if (!targetUnit) { - botAI->TellError("No valid target selected by master."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_no_target_error", "No valid target selected by master.", {}); + botAI->TellError(text); return false; } if (!targetUnit->IsAlive()) { - botAI->TellError("Target is not alive."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_target_dead_error", "Target is not alive.", {}); + botAI->TellError(text); return false; } if (!bot->IsValidAttackTarget(targetUnit)) { - botAI->TellError("Target is not a valid attack target for the bot."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_invalid_target_error", "Target is not a valid attack target for the bot.", {}); + botAI->TellError(text); + return false; + } + if (sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) + && (targetUnit->IsPlayer() || targetUnit->IsPet())) + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_pvp_prohibited_error", "I cannot command my pet to attack players in PvP prohibited areas.", {}); + botAI->TellError(text); return false; } @@ -182,9 +212,17 @@ bool PetsAction::Execute(Event event) } // Inform the master if the command succeeded or failed. if (didAttack && sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to attack your target."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_attack_success", "Pet commanded to attack your target.", {}); + botAI->TellMaster(text); + } else if (!didAttack) - botAI->TellError("Pet did not attack. (Already attacking or unable to attack target)"); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_attack_failed", "Pet did not attack. (Already attacking or unable to attack target)", {}); + botAI->TellError(text); + } return didAttack; } // The "follow" command makes all pets/guardians follow the bot. @@ -192,7 +230,11 @@ bool PetsAction::Execute(Event event) { botAI->PetFollow(); if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to follow."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_follow_success", "Pet commanded to follow.", {}); + botAI->TellMaster(text); + } return true; } // The "stay" command causes all pets/guardians to stop and stay in place. @@ -229,14 +271,20 @@ bool PetsAction::Execute(Event event) } } if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to stay."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stay_success", "Pet commanded to stay.", {}); + botAI->TellMaster(text); + } return true; } // Unknown command: show usage instructions and return. else { - botAI->TellError("Unknown pet command: " + param + - ". Use: pet "); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_unknown_command_error", "Unknown pet command: %param. Use: pet ", + {{"param", param}}); + botAI->TellError(text); return false; } @@ -251,7 +299,12 @@ bool PetsAction::Execute(Event event) // Inform the master of the new stance if debug is enabled. if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet stance set to " + stanceText + "."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_set_success", "Pet stance set to %stance.", + {{"stance", stanceText}}); + botAI->TellMaster(text); + } return true; -} +} \ No newline at end of file diff --git a/src/strategy/actions/RandomBotUpdateAction.cpp b/src/strategy/actions/RandomBotUpdateAction.cpp index 1536cd6b92..61a8360e59 100644 --- a/src/strategy/actions/RandomBotUpdateAction.cpp +++ b/src/strategy/actions/RandomBotUpdateAction.cpp @@ -13,10 +13,10 @@ bool RandomBotUpdateAction::Execute(Event event) if (!sRandomPlayerbotMgr->IsRandomBot(bot)) return false; - if (bot->GetGroup() && botAI->GetGroupMaster()) + if (bot->GetGroup() && botAI->GetGroupLeader()) { - PlayerbotAI* groupMasterBotAI = GET_PLAYERBOT_AI(botAI->GetGroupMaster()); - if (!groupMasterBotAI || groupMasterBotAI->IsRealPlayer()) + PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader()); + if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer()) return true; } diff --git a/src/strategy/actions/ReleaseSpiritAction.cpp b/src/strategy/actions/ReleaseSpiritAction.cpp index b06fc89263..17fcc42a0b 100644 --- a/src/strategy/actions/ReleaseSpiritAction.cpp +++ b/src/strategy/actions/ReleaseSpiritAction.cpp @@ -168,15 +168,15 @@ bool AutoReleaseSpiritAction::ShouldAutoRelease() const if (!bot->GetGroup()) return true; - Player* groupMaster = botAI->GetGroupMaster(); - if (!groupMaster || groupMaster == bot) + Player* groupLeader = botAI->GetGroupLeader(); + if (!groupLeader || groupLeader == bot) return true; if (!botAI->HasActivePlayerMaster()) return true; if (botAI->HasActivePlayerMaster() && - groupMaster->GetMapId() == bot->GetMapId() && + groupLeader->GetMapId() == bot->GetMapId() && bot->GetMap() && (bot->GetMap()->IsRaid() || bot->GetMap()->IsDungeon())) { @@ -184,7 +184,7 @@ bool AutoReleaseSpiritAction::ShouldAutoRelease() const } return sServerFacade->IsDistanceGreaterThan( - AI_VALUE2(float, "distance", "master target"), + AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->sightDistance); } diff --git a/src/strategy/actions/ResetInstancesAction.cpp b/src/strategy/actions/ResetInstancesAction.cpp index 1c0a6f2480..cce5eef1e3 100644 --- a/src/strategy/actions/ResetInstancesAction.cpp +++ b/src/strategy/actions/ResetInstancesAction.cpp @@ -16,4 +16,4 @@ bool ResetInstancesAction::Execute(Event event) return true; } -bool ResetInstancesAction::isUseful() { return botAI->GetGroupMaster() == bot; }; +bool ResetInstancesAction::isUseful() { return botAI->GetGroupLeader() == bot; }; diff --git a/src/strategy/actions/ReviveFromCorpseAction.cpp b/src/strategy/actions/ReviveFromCorpseAction.cpp index 74237670df..ce0b1fa05d 100644 --- a/src/strategy/actions/ReviveFromCorpseAction.cpp +++ b/src/strategy/actions/ReviveFromCorpseAction.cpp @@ -17,14 +17,14 @@ bool ReviveFromCorpseAction::Execute(Event event) { - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Corpse* corpse = bot->GetCorpse(); - // follow master when master revives + // follow group Leader when group Leader revives WorldPacket& p = event.getPacket(); - if (!p.empty() && p.GetOpcode() == CMSG_RECLAIM_CORPSE && master && !corpse && bot->IsAlive()) + if (!p.empty() && p.GetOpcode() == CMSG_RECLAIM_CORPSE && groupLeader && !corpse && bot->IsAlive()) { - if (sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + if (sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->farDistance)) { if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) @@ -43,10 +43,10 @@ bool ReviveFromCorpseAction::Execute(Event event) // time(nullptr)) // return false; - if (master) + if (groupLeader) { - if (!GET_PLAYERBOT_AI(master) && master->isDead() && master->GetCorpse() && - sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + if (!GET_PLAYERBOT_AI(groupLeader) && groupLeader->isDead() && groupLeader->GetCorpse() && + sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->farDistance)) return false; } @@ -79,15 +79,15 @@ bool FindCorpseAction::Execute(Event event) if (bot->InBattleground()) return false; - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Corpse* corpse = bot->GetCorpse(); if (!corpse) return false; - // if (master) + // if (groupLeader) // { - // if (!GET_PLAYERBOT_AI(master) && - // sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + // if (!GET_PLAYERBOT_AI(groupLeader) && + // sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), // sPlayerbotAIConfig->farDistance)) return false; // } @@ -110,20 +110,20 @@ bool FindCorpseAction::Execute(Event event) WorldPosition botPos(bot); WorldPosition corpsePos(corpse); WorldPosition moveToPos = corpsePos; - WorldPosition masterPos(master); + WorldPosition leaderPos(groupLeader); float reclaimDist = CORPSE_RECLAIM_RADIUS - 5.0f; float corpseDist = botPos.distance(corpsePos); int64 deadTime = time(nullptr) - corpse->GetGhostTime(); - bool moveToMaster = master && master != bot && masterPos.fDist(corpsePos) < reclaimDist; + bool moveToLeader = groupLeader && groupLeader != bot && leaderPos.fDist(corpsePos) < reclaimDist; // Should we ressurect? If so, return false. if (corpseDist < reclaimDist) { - if (moveToMaster) // We are near master. + if (moveToLeader) // We are near group leader. { - if (botPos.fDist(masterPos) < sPlayerbotAIConfig->spellDistance) + if (botPos.fDist(leaderPos) < sPlayerbotAIConfig->spellDistance) return false; } else if (deadTime > 8 * MINUTE) // We have walked too long already. @@ -140,8 +140,8 @@ bool FindCorpseAction::Execute(Event event) // If we are getting close move to a save ressurrection spot instead of just the corpse. if (corpseDist < sPlayerbotAIConfig->reactDistance) { - if (moveToMaster) - moveToPos = masterPos; + if (moveToLeader) + moveToPos = leaderPos; else { FleeManager manager(bot, reclaimDist, 0.0, urand(0, 1), moveToPos); @@ -215,12 +215,12 @@ GraveyardStruct const* SpiritHealerAction::GetGrave(bool startZone) if (!startZone && ClosestGrave) return ClosestGrave; - if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && botAI->GetGroupMaster() && botAI->GetGroupMaster() != bot) + if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && botAI->GetGroupLeader() && botAI->GetGroupLeader() != bot) { - Player* master = botAI->GetGroupMaster(); - if (master && master != bot) + Player* groupLeader = botAI->GetGroupLeader(); + if (groupLeader && groupLeader != bot) { - ClosestGrave = sGraveyard->GetClosestGraveyard(master, bot->GetTeamId()); + ClosestGrave = sGraveyard->GetClosestGraveyard(groupLeader, bot->GetTeamId()); if (ClosestGrave) return ClosestGrave; diff --git a/src/strategy/actions/RewardAction.cpp b/src/strategy/actions/RewardAction.cpp index 1022166d0f..8fe1c6ff72 100644 --- a/src/strategy/actions/RewardAction.cpp +++ b/src/strategy/actions/RewardAction.cpp @@ -35,8 +35,8 @@ bool RewardAction::Execute(Event event) return true; } - Unit* mtar = AI_VALUE(Unit*, "master target"); - if (mtar && Reward(itemId, mtar)) + Unit* groupLeaderUnit = AI_VALUE(Unit*, "group leader"); + if (groupLeaderUnit && Reward(itemId, groupLeaderUnit)) return true; botAI->TellError("Cannot talk to quest giver"); diff --git a/src/strategy/actions/RpgSubActions.cpp b/src/strategy/actions/RpgSubActions.cpp index 784f6bbb20..aa4269fa92 100644 --- a/src/strategy/actions/RpgSubActions.cpp +++ b/src/strategy/actions/RpgSubActions.cpp @@ -76,7 +76,7 @@ void RpgHelper::setFacing(GuidPosition guidPosition) void RpgHelper::setDelay(bool waitForGroup) { - if (!botAI->HasRealPlayerMaster() || (waitForGroup && botAI->GetGroupMaster() == bot && bot->GetGroup())) + if (!botAI->HasRealPlayerMaster() || (waitForGroup && botAI->GetGroupLeader() == bot && bot->GetGroup())) botAI->SetNextCheckDelay(sPlayerbotAIConfig->rpgDelay); else botAI->SetNextCheckDelay(sPlayerbotAIConfig->rpgDelay / 5); diff --git a/src/strategy/actions/SecurityCheckAction.cpp b/src/strategy/actions/SecurityCheckAction.cpp index 1320eb04a6..c47a6e52cb 100644 --- a/src/strategy/actions/SecurityCheckAction.cpp +++ b/src/strategy/actions/SecurityCheckAction.cpp @@ -22,8 +22,8 @@ bool SecurityCheckAction::Execute(Event event) ItemQualities threshold = group->GetLootThreshold(); if (method == MASTER_LOOT || method == FREE_FOR_ALL || threshold > ITEM_QUALITY_UNCOMMON) { - if ((botAI->GetGroupMaster()->GetSession()->GetSecurity() == SEC_PLAYER) && - (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupMaster()->GetGuildId())) + if ((botAI->GetGroupLeader()->GetSession()->GetSecurity() == SEC_PLAYER) && + (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupLeader()->GetGuildId())) { botAI->TellError("I will play with this loot type only if I'm in your guild :/"); botAI->ChangeStrategy("+passive,+stay", BOT_STATE_NON_COMBAT); diff --git a/src/strategy/actions/SeeSpellAction.cpp b/src/strategy/actions/SeeSpellAction.cpp index 8b90adece0..f42dcade64 100644 --- a/src/strategy/actions/SeeSpellAction.cpp +++ b/src/strategy/actions/SeeSpellAction.cpp @@ -12,6 +12,7 @@ #include "RTSCValues.h" #include "RtscAction.h" #include "PositionValue.h" +#include "ByteBuffer.h" Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp, bool important) @@ -31,27 +32,52 @@ Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, bool SeeSpellAction::Execute(Event event) { - WorldPacket p(event.getPacket()); // + // RTSC packet data + WorldPacket p(event.getPacket()); + uint8 castCount; uint32 spellId; - uint8 castCount, castFlags; - Player* master = botAI->GetMaster(); + uint8 castFlags; - p.rpos(0); - p >> castCount >> spellId >> castFlags; + // check RTSC header size = castCount (uint8) + spellId (uint32) + castFlags (uint8) + uint32 const rtscHeaderSize = sizeof(uint8) + sizeof(uint32) + sizeof(uint8); + if (p.size() < rtscHeaderSize) + { + LOG_WARN("playerbots", "SeeSpellAction: Corrupt RTSC packet size={}, expected>={}", p.size(), rtscHeaderSize); + return false; + } + Player* master = botAI->GetMaster(); if (!master) return false; + // read RTSC packet data + p.rpos(0); // set read position to start + p >> castCount >> spellId >> castFlags; + // if (!botAI->HasStrategy("RTSC", botAI->GetState())) // return false; if (spellId != RTSC_MOVE_SPELL) return false; - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); - + // should not throw exception,just defensive measure to prevent any crashes when core function breaks. SpellCastTargets targets; - targets.Read(p, botAI->GetMaster()); + try + { + targets.Read(p, master); + if (!targets.GetDst()) + { + // do not dereference a null destination; ignore malformed RTSC packets instead of crashing + LOG_WARN("playerbots", "SeeSpellAction: (malformed) RTSC payload does not contain full targets data"); + return false; + } + } + catch (ByteBufferException const&) + { + // ignore malformed RTSC packets instead of crashing + LOG_WARN("playerbots", "SeeSpellAction: Failed deserialization (malformed) RTSC payload"); + return false; + } WorldPosition spellPosition(master->GetMapId(), targets.GetDst()->_position); SET_AI_VALUE(WorldPosition, "see spell location", spellPosition); diff --git a/src/strategy/actions/TellMasterAction.cpp b/src/strategy/actions/TellMasterAction.cpp index 701f93d773..4b8d96d561 100644 --- a/src/strategy/actions/TellMasterAction.cpp +++ b/src/strategy/actions/TellMasterAction.cpp @@ -22,7 +22,7 @@ bool OutOfReactRangeAction::Execute(Event event) bool OutOfReactRangeAction::isUseful() { - bool canFollow = Follow(AI_VALUE(Unit*, "master target")); + bool canFollow = Follow(AI_VALUE(Unit*, "group leader")); if (!canFollow) { return false; diff --git a/src/strategy/actions/TravelAction.cpp b/src/strategy/actions/TravelAction.cpp index 5804fd7d24..f99f8b29d3 100644 --- a/src/strategy/actions/TravelAction.cpp +++ b/src/strategy/actions/TravelAction.cpp @@ -64,7 +64,7 @@ bool MoveToDarkPortalAction::Execute(Event event) { if (bot->GetGroup()) if (bot->GetGroup()->GetLeaderGUID() != bot->GetGUID() && - !GET_PLAYERBOT_AI(GET_PLAYERBOT_AI(bot)->GetGroupMaster())) + !GET_PLAYERBOT_AI(GET_PLAYERBOT_AI(bot)->GetGroupLeader())) return false; if (bot->GetLevel() > 57) diff --git a/src/strategy/druid/DruidActions.cpp b/src/strategy/druid/DruidActions.cpp index b5a493dbb5..217db82508 100644 --- a/src/strategy/druid/DruidActions.cpp +++ b/src/strategy/druid/DruidActions.cpp @@ -7,6 +7,9 @@ #include "Event.h" #include "Playerbots.h" +#include "ServerFacade.h" +#include "AoeValues.h" +#include "TargetValue.h" NextAction** CastAbolishPoisonAction::getAlternatives() { @@ -30,6 +33,32 @@ bool CastEntanglingRootsCcAction::Execute(Event event) { return botAI->CastSpell Value* CastHibernateCcAction::GetTargetValue() { return context->GetValue("cc target", "hibernate"); } bool CastHibernateCcAction::Execute(Event event) { return botAI->CastSpell("hibernate", GetTarget()); } +bool CastStarfallAction::isUseful() +{ + if (!CastSpellAction::isUseful()) + return false; + + // Avoid breaking CC + WorldLocation aoePos = *context->GetValue("aoe position"); + Unit* ccTarget = context->GetValue("current cc target")->Get(); + if (ccTarget && ccTarget->IsAlive()) + { + float dist2d = sServerFacade->GetDistance2d(ccTarget, aoePos.GetPositionX(), aoePos.GetPositionY()); + if (sServerFacade->IsDistanceLessOrEqualThan(dist2d, sPlayerbotAIConfig->aoeRadius)) + return false; + } + + // Avoid single-target usage on initial pull + uint8 aoeCount = *context->GetValue("aoe count"); + if (aoeCount < 2) + { + Unit* target = context->GetValue("current target")->Get(); + if (!target || (!botAI->HasAura("moonfire", target) && !botAI->HasAura("insect swarm", target))) + return false; + } + + return true; +} NextAction** CastReviveAction::getPrerequisites() { diff --git a/src/strategy/druid/DruidActions.h b/src/strategy/druid/DruidActions.h index 402073d26d..d0af6e5a49 100644 --- a/src/strategy/druid/DruidActions.h +++ b/src/strategy/druid/DruidActions.h @@ -144,6 +144,8 @@ class CastStarfallAction : public CastSpellAction { public: CastStarfallAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "starfall") {} + + bool isUseful() override; }; class CastHurricaneAction : public CastSpellAction diff --git a/src/strategy/generic/GroupStrategy.cpp b/src/strategy/generic/GroupStrategy.cpp index 9406e8409d..9cfba6e2fc 100644 --- a/src/strategy/generic/GroupStrategy.cpp +++ b/src/strategy/generic/GroupStrategy.cpp @@ -11,7 +11,6 @@ void GroupStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("invite nearby", 4.0f), nullptr))); triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("invite guild", 4.0f), nullptr))); - triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("leave far away", 4.0f), nullptr))); - triggers.push_back( - new TriggerNode("seldom", NextAction::array(0, new NextAction("reset instances", 1.0f), nullptr))); + triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("leave far away", 4.0f), nullptr))); + triggers.push_back(new TriggerNode("seldom", NextAction::array(0, new NextAction("reset instances", 1.0f), nullptr))); } diff --git a/src/strategy/generic/WorldPacketHandlerStrategy.cpp b/src/strategy/generic/WorldPacketHandlerStrategy.cpp index 69b7685e2f..97b1ba1ba8 100644 --- a/src/strategy/generic/WorldPacketHandlerStrategy.cpp +++ b/src/strategy/generic/WorldPacketHandlerStrategy.cpp @@ -5,8 +5,6 @@ #include "WorldPacketHandlerStrategy.h" -#include "Playerbots.h" - void WorldPacketHandlerStrategy::InitTriggers(std::vector& triggers) { PassTroughStrategy::InitTriggers(triggers); @@ -69,7 +67,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back(new TriggerNode("questgiver quest details", NextAction::array(0, new NextAction("turn in query quest", relevance), nullptr))); // loot roll - triggers.push_back(new TriggerNode("very often", NextAction::array(0, new NextAction("loot roll", 10.0f), nullptr))); + triggers.push_back(new TriggerNode("very often", NextAction::array(0, new NextAction("loot roll", relevance), nullptr))); } WorldPacketHandlerStrategy::WorldPacketHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) diff --git a/src/strategy/raids/RaidStrategyContext.h b/src/strategy/raids/RaidStrategyContext.h index f16fa72546..ee2bac3270 100644 --- a/src/strategy/raids/RaidStrategyContext.h +++ b/src/strategy/raids/RaidStrategyContext.h @@ -22,7 +22,7 @@ class RaidStrategyContext : public NamedObjectContext RaidStrategyContext() : NamedObjectContext(false, true) { creators["aq20"] = &RaidStrategyContext::aq20; - creators["mc"] = &RaidStrategyContext::mc; + creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; creators["magtheridon"] = &RaidStrategyContext::magtheridon; @@ -38,7 +38,7 @@ class RaidStrategyContext : public NamedObjectContext private: static Strategy* aq20(PlayerbotAI* botAI) { return new RaidAq20Strategy(botAI); } - static Strategy* mc(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } + static Strategy* moltencore(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); } static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } diff --git a/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h b/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h index 8cd01c2e6b..c7becc8362 100644 --- a/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h +++ b/src/strategy/raids/gruulslair/RaidGruulsLairHelpers.h @@ -15,7 +15,7 @@ namespace GruulsLairHelpers SPELL_SPELL_SHIELD = 33054, // Hunter - SPELL_MISDIRECTION = 34477, + SPELL_MISDIRECTION = 35079, // Warlock SPELL_BANISH = 18647, // Rank 2 diff --git a/src/strategy/raids/karazhan/RaidKarazhanActionContext.h b/src/strategy/raids/karazhan/RaidKarazhanActionContext.h index 2c1f71686a..34f276a94b 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActionContext.h +++ b/src/strategy/raids/karazhan/RaidKarazhanActionContext.h @@ -1,5 +1,5 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANACTIONS_CONTEXT_H -#define _PLAYERBOT_RAIDKARAZHANACTIONS_CONTEXT_H +#ifndef _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H +#define _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H #include "RaidKarazhanActions.h" #include "NamedObjectContext.h" @@ -9,77 +9,254 @@ class RaidKarazhanActionContext : public NamedObjectContext public: RaidKarazhanActionContext() { - creators["karazhan attumen the huntsman stack behind"] = &RaidKarazhanActionContext::karazhan_attumen_the_huntsman_stack_behind; + // Trash + creators["mana warp stun creature before warp breach"] = + &RaidKarazhanActionContext::mana_warp_stun_creature_before_warp_breach; - creators["karazhan moroes mark target"] = &RaidKarazhanActionContext::karazhan_moroes_mark_target; + // Attumen the Huntsman + creators["attumen the huntsman mark target"] = + &RaidKarazhanActionContext::attumen_the_huntsman_mark_target; - creators["karazhan maiden of virtue position boss"] = &RaidKarazhanActionContext::karazhan_maiden_of_virtue_position_boss; - creators["karazhan maiden of virtue position ranged"] = &RaidKarazhanActionContext::karazhan_maiden_of_virtue_position_ranged; + creators["attumen the huntsman split bosses"] = + &RaidKarazhanActionContext::attumen_the_huntsman_split_bosses; - creators["karazhan big bad wolf position boss"] = &RaidKarazhanActionContext::karazhan_big_bad_wolf_position_boss; - creators["karazhan big bad wolf run away"] = &RaidKarazhanActionContext::karazhan_big_bad_wolf_run_away; + creators["attumen the huntsman stack behind"] = + &RaidKarazhanActionContext::attumen_the_huntsman_stack_behind; - creators["karazhan romulo and julianne mark target"] = &RaidKarazhanActionContext::karazhan_romulo_and_julianne_mark_target; + creators["attumen the huntsman manage dps timer"] = + &RaidKarazhanActionContext::attumen_the_huntsman_manage_dps_timer; - creators["karazhan wizard of oz mark target"] = &RaidKarazhanActionContext::karazhan_wizard_of_oz_mark_target; - creators["karazhan wizard of oz scorch strawman"] = &RaidKarazhanActionContext::karazhan_wizard_of_oz_scorch_strawman; + // Moroes + creators["moroes main tank attack boss"] = + &RaidKarazhanActionContext::moroes_main_tank_attack_boss; - creators["karazhan the curator mark target"] = &RaidKarazhanActionContext::karazhan_the_curator_mark_target; - creators["karazhan the curator position boss"] = &RaidKarazhanActionContext::karazhan_the_curator_position_boss; - creators["karazhan the curator spread ranged"] = &RaidKarazhanActionContext::karazhan_the_curator_spread_ranged; + creators["moroes mark target"] = + &RaidKarazhanActionContext::moroes_mark_target; - creators["karazhan terestian illhoof mark target"] = &RaidKarazhanActionContext::karazhan_terestian_illhoof_mark_target; + // Maiden of Virtue + creators["maiden of virtue move boss to healer"] = + &RaidKarazhanActionContext::maiden_of_virtue_move_boss_to_healer; - creators["karazhan shade of aran arcane explosion run away"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_arcane_explosion_run_away; - creators["karazhan shade of aran flame wreath stop movement"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_flame_wreath_stop_movement; - creators["karazhan shade of aran mark conjured elemental"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_mark_conjured_elemental; - creators["karazhan shade of aran spread ranged"] = &RaidKarazhanActionContext::karazhan_shade_of_aran_spread_ranged; + creators["maiden of virtue position ranged"] = + &RaidKarazhanActionContext::maiden_of_virtue_position_ranged; - creators["karazhan netherspite block red beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_red_beam; - creators["karazhan netherspite block blue beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_blue_beam; - creators["karazhan netherspite block green beam"] = &RaidKarazhanActionContext::karazhan_netherspite_block_green_beam; - creators["karazhan netherspite avoid beam and void zone"] = &RaidKarazhanActionContext::karazhan_netherspite_avoid_beam_and_void_zone; - creators["karazhan netherspite banish phase avoid void zone"] = &RaidKarazhanActionContext::karazhan_netherspite_banish_phase_avoid_void_zone; + // The Big Bad Wolf + creators["big bad wolf position boss"] = + &RaidKarazhanActionContext::big_bad_wolf_position_boss; - creators["karazhan prince malchezaar non tank avoid hazard"] = &RaidKarazhanActionContext::karazhan_prince_malchezaar_non_tank_avoid_hazard; - creators["karazhan prince malchezaar tank avoid hazard"] = &RaidKarazhanActionContext::karazhan_prince_malchezaar_tank_avoid_hazard; + creators["big bad wolf run away from boss"] = + &RaidKarazhanActionContext::big_bad_wolf_run_away_from_boss; + + // Romulo and Julianne + creators["romulo and julianne mark target"] = + &RaidKarazhanActionContext::romulo_and_julianne_mark_target; + + // The Wizard of Oz + creators["wizard of oz mark target"] = + &RaidKarazhanActionContext::wizard_of_oz_mark_target; + creators["wizard of oz scorch strawman"] = + &RaidKarazhanActionContext::wizard_of_oz_scorch_strawman; + + // The Curator + creators["the curator mark astral flare"] = + &RaidKarazhanActionContext::the_curator_mark_astral_flare; + + creators["the curator position boss"] = + &RaidKarazhanActionContext::the_curator_position_boss; + + creators["the curator spread ranged"] = + &RaidKarazhanActionContext::the_curator_spread_ranged; + + // Terestian Illhoof + creators["terestian illhoof mark target"] = + &RaidKarazhanActionContext::terestian_illhoof_mark_target; + + // Shade of Aran + creators["shade of aran run away from arcane explosion"] = + &RaidKarazhanActionContext::shade_of_aran_run_away_from_arcane_explosion; + + creators["shade of aran stop moving during flame wreath"] = + &RaidKarazhanActionContext::shade_of_aran_stop_moving_during_flame_wreath; + + creators["shade of aran mark conjured elemental"] = + &RaidKarazhanActionContext::shade_of_aran_mark_conjured_elemental; + + creators["shade of aran ranged maintain distance"] = + &RaidKarazhanActionContext::shade_of_aran_ranged_maintain_distance; + + // Netherspite + creators["netherspite block red beam"] = + &RaidKarazhanActionContext::netherspite_block_red_beam; + + creators["netherspite block blue beam"] = + &RaidKarazhanActionContext::netherspite_block_blue_beam; + + creators["netherspite block green beam"] = + &RaidKarazhanActionContext::netherspite_block_green_beam; + + creators["netherspite avoid beam and void zone"] = + &RaidKarazhanActionContext::netherspite_avoid_beam_and_void_zone; + + creators["netherspite banish phase avoid void zone"] = + &RaidKarazhanActionContext::netherspite_banish_phase_avoid_void_zone; + + creators["netherspite manage timers and trackers"] = + &RaidKarazhanActionContext::netherspite_manage_timers_and_trackers; + + // Prince Malchezaar + creators["prince malchezaar enfeebled avoid hazard"] = + &RaidKarazhanActionContext::prince_malchezaar_enfeebled_avoid_hazard; + + creators["prince malchezaar non tank avoid infernal"] = + &RaidKarazhanActionContext::prince_malchezaar_non_tank_avoid_infernal; + + creators["prince malchezaar main tank movement"] = + &RaidKarazhanActionContext::prince_malchezaar_main_tank_movement; + + // Nightbane + creators["nightbane ground phase position boss"] = + &RaidKarazhanActionContext::nightbane_ground_phase_position_boss; + + creators["nightbane ground phase rotate ranged positions"] = + &RaidKarazhanActionContext::nightbane_ground_phase_rotate_ranged_positions; + + creators["nightbane cast fear ward on main tank"] = + &RaidKarazhanActionContext::nightbane_cast_fear_ward_on_main_tank; + + creators["nightbane control pet aggression"] = + &RaidKarazhanActionContext::nightbane_control_pet_aggression; + + creators["nightbane flight phase movement"] = + &RaidKarazhanActionContext::nightbane_flight_phase_movement; + + creators["nightbane manage timers and trackers"] = + &RaidKarazhanActionContext::nightbane_manage_timers_and_trackers; } private: - static Action* karazhan_attumen_the_huntsman_stack_behind(PlayerbotAI* botAI) { return new KarazhanAttumenTheHuntsmanStackBehindAction(botAI); } + // Trash + static Action* mana_warp_stun_creature_before_warp_breach( + PlayerbotAI* botAI) { return new ManaWarpStunCreatureBeforeWarpBreachAction(botAI); } + + // Attumen the Huntsman + static Action* attumen_the_huntsman_mark_target( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanMarkTargetAction(botAI); } + + static Action* attumen_the_huntsman_split_bosses( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanSplitBossesAction(botAI); } + + static Action* attumen_the_huntsman_stack_behind( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanStackBehindAction(botAI); } + + static Action* attumen_the_huntsman_manage_dps_timer( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanManageDpsTimerAction(botAI); } + + // Moroes + static Action* moroes_main_tank_attack_boss( + PlayerbotAI* botAI) { return new MoroesMainTankAttackBossAction(botAI); } + + static Action* moroes_mark_target( + PlayerbotAI* botAI) { return new MoroesMarkTargetAction(botAI); } + + // Maiden of Virtue + static Action* maiden_of_virtue_move_boss_to_healer( + PlayerbotAI* botAI) { return new MaidenOfVirtueMoveBossToHealerAction(botAI); } + + static Action* maiden_of_virtue_position_ranged( + PlayerbotAI* botAI) { return new MaidenOfVirtuePositionRangedAction(botAI); } + + // The Big Bad Wolf + static Action* big_bad_wolf_position_boss( + PlayerbotAI* botAI) { return new BigBadWolfPositionBossAction(botAI); } + + static Action* big_bad_wolf_run_away_from_boss( + PlayerbotAI* botAI) { return new BigBadWolfRunAwayFromBossAction(botAI); } + + // Romulo and Julianne + static Action* romulo_and_julianne_mark_target( + PlayerbotAI* botAI) { return new RomuloAndJulianneMarkTargetAction(botAI); } + + // The Wizard of Oz + static Action* wizard_of_oz_mark_target( + PlayerbotAI* botAI) { return new WizardOfOzMarkTargetAction(botAI); } + + static Action* wizard_of_oz_scorch_strawman( + PlayerbotAI* botAI) { return new WizardOfOzScorchStrawmanAction(botAI); } + + // The Curator + static Action* the_curator_mark_astral_flare( + PlayerbotAI* botAI) { return new TheCuratorMarkAstralFlareAction(botAI); } + + static Action* the_curator_position_boss( + PlayerbotAI* botAI) { return new TheCuratorPositionBossAction(botAI); } + + static Action* the_curator_spread_ranged( + PlayerbotAI* botAI) { return new TheCuratorSpreadRangedAction(botAI); } + + // Terestian Illhoof + static Action* terestian_illhoof_mark_target( + PlayerbotAI* botAI) { return new TerestianIllhoofMarkTargetAction(botAI); } + + // Shade of Aran + static Action* shade_of_aran_run_away_from_arcane_explosion( + PlayerbotAI* botAI) { return new ShadeOfAranRunAwayFromArcaneExplosionAction(botAI); } + + static Action* shade_of_aran_stop_moving_during_flame_wreath( + PlayerbotAI* botAI) { return new ShadeOfAranStopMovingDuringFlameWreathAction(botAI); } + + static Action* shade_of_aran_mark_conjured_elemental( + PlayerbotAI* botAI) { return new ShadeOfAranMarkConjuredElementalAction(botAI); } + + static Action* shade_of_aran_ranged_maintain_distance( + PlayerbotAI* botAI) { return new ShadeOfAranRangedMaintainDistanceAction(botAI); } + + // Netherspite + static Action* netherspite_block_red_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockRedBeamAction(botAI); } + + static Action* netherspite_block_blue_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockBlueBeamAction(botAI); } + + static Action* netherspite_block_green_beam( + PlayerbotAI* botAI) { return new NetherspiteBlockGreenBeamAction(botAI); } + + static Action* netherspite_avoid_beam_and_void_zone( + PlayerbotAI* botAI) { return new NetherspiteAvoidBeamAndVoidZoneAction(botAI); } + + static Action* netherspite_banish_phase_avoid_void_zone( + PlayerbotAI* botAI) { return new NetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } - static Action* karazhan_moroes_mark_target(PlayerbotAI* botAI) { return new KarazhanMoroesMarkTargetAction(botAI); } + static Action* netherspite_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NetherspiteManageTimersAndTrackersAction(botAI); } - static Action* karazhan_maiden_of_virtue_position_boss(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtuePositionBossAction(botAI); } - static Action* karazhan_maiden_of_virtue_position_ranged(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtuePositionRangedAction(botAI); } + // Prince Malchezaar + static Action* prince_malchezaar_enfeebled_avoid_hazard( + PlayerbotAI* botAI) { return new PrinceMalchezaarEnfeebledAvoidHazardAction(botAI); } - static Action* karazhan_big_bad_wolf_position_boss(PlayerbotAI* botAI) { return new KarazhanBigBadWolfPositionBossAction(botAI); } - static Action* karazhan_big_bad_wolf_run_away(PlayerbotAI* botAI) { return new KarazhanBigBadWolfRunAwayAction(botAI); } + static Action* prince_malchezaar_non_tank_avoid_infernal( + PlayerbotAI* botAI) { return new PrinceMalchezaarNonTankAvoidInfernalAction(botAI); } - static Action* karazhan_romulo_and_julianne_mark_target(PlayerbotAI* botAI) { return new KarazhanRomuloAndJulianneMarkTargetAction(botAI); } + static Action* prince_malchezaar_main_tank_movement( + PlayerbotAI* botAI) { return new PrinceMalchezaarMainTankMovementAction(botAI); } - static Action* karazhan_wizard_of_oz_mark_target(PlayerbotAI* botAI) { return new KarazhanWizardOfOzMarkTargetAction(botAI); } - static Action* karazhan_wizard_of_oz_scorch_strawman(PlayerbotAI* botAI) { return new KarazhanWizardOfOzScorchStrawmanAction(botAI); } + // Nightbane + static Action* nightbane_ground_phase_position_boss( + PlayerbotAI* botAI) { return new NightbaneGroundPhasePositionBossAction(botAI); } - static Action* karazhan_the_curator_mark_target(PlayerbotAI* botAI) { return new KarazhanTheCuratorMarkTargetAction(botAI); } - static Action* karazhan_the_curator_position_boss(PlayerbotAI* botAI) { return new KarazhanTheCuratorPositionBossAction(botAI); } - static Action* karazhan_the_curator_spread_ranged(PlayerbotAI* botAI) { return new KarazhanTheCuratorSpreadRangedAction(botAI); } + static Action* nightbane_ground_phase_rotate_ranged_positions( + PlayerbotAI* botAI) { return new NightbaneGroundPhaseRotateRangedPositionsAction(botAI); } - static Action* karazhan_terestian_illhoof_mark_target(PlayerbotAI* botAI) { return new KarazhanTerestianIllhoofMarkTargetAction(botAI); } + static Action* nightbane_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new NightbaneCastFearWardOnMainTankAction(botAI); } - static Action* karazhan_shade_of_aran_arcane_explosion_run_away(PlayerbotAI* botAI) { return new KarazhanShadeOfAranArcaneExplosionRunAwayAction(botAI); } - static Action* karazhan_shade_of_aran_flame_wreath_stop_movement(PlayerbotAI* botAI) { return new KarazhanShadeOfAranFlameWreathStopMovementAction(botAI); } - static Action* karazhan_shade_of_aran_mark_conjured_elemental(PlayerbotAI* botAI) { return new KarazhanShadeOfAranMarkConjuredElementalAction(botAI); } - static Action* karazhan_shade_of_aran_spread_ranged(PlayerbotAI* botAI) { return new KarazhanShadeOfAranSpreadRangedAction(botAI); } + static Action* nightbane_control_pet_aggression( + PlayerbotAI* botAI) { return new NightbaneControlPetAggressionAction(botAI); } - static Action* karazhan_netherspite_block_red_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockRedBeamAction(botAI); } - static Action* karazhan_netherspite_block_blue_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockBlueBeamAction(botAI); } - static Action* karazhan_netherspite_block_green_beam(PlayerbotAI* botAI) { return new KarazhanNetherspiteBlockGreenBeamAction(botAI); } - static Action* karazhan_netherspite_avoid_beam_and_void_zone(PlayerbotAI* botAI) { return new KarazhanNetherspiteAvoidBeamAndVoidZoneAction(botAI); } - static Action* karazhan_netherspite_banish_phase_avoid_void_zone(PlayerbotAI* botAI) { return new KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } + static Action* nightbane_flight_phase_movement( + PlayerbotAI* botAI) { return new NightbaneFlightPhaseMovementAction(botAI); } - static Action* karazhan_prince_malchezaar_non_tank_avoid_hazard(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarNonTankAvoidHazardAction(botAI); } - static Action* karazhan_prince_malchezaar_tank_avoid_hazard(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarTankAvoidHazardAction(botAI); } + static Action* nightbane_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NightbaneManageTimersAndTrackersAction(botAI); } }; #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanActions.cpp b/src/strategy/raids/karazhan/RaidKarazhanActions.cpp index e45d63f7b2..e452cf4583 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActions.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanActions.cpp @@ -1,81 +1,215 @@ #include "RaidKarazhanActions.h" #include "RaidKarazhanHelpers.h" -#include "AiObjectContext.h" -#include "PlayerbotAI.h" -#include "PlayerbotMgr.h" -#include "PlayerbotTextMgr.h" #include "Playerbots.h" -#include "Position.h" +#include "PlayerbotTextMgr.h" + +using namespace KarazhanHelpers; + +// Trash + +// Mana Warps blow up when they die for massive raid damage +// But they cannot cast the ability if they are stunned +bool ManaWarpStunCreatureBeforeWarpBreachAction::Execute(Event event) +{ + Unit* manaWarp = GetFirstAliveUnitByEntry(botAI, NPC_MANA_WARP); + if (!manaWarp) + return false; + + static const std::array spells = + { + "bash", + "concussion blow", + "hammer of justice", + "kidney shot", + "maim", + "revenge stun", + "shadowfury", + "shockwave" + }; + + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, manaWarp)) + return botAI->CastSpell(spell, manaWarp); + } + + return false; +} + +// Attumen the Huntsman + +// Prioritize Midnight until Attumen is mounted +bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) +{ + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (attumenMounted) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithStar(bot, attumenMounted); + + SetRtiTarget(botAI, "star", attumenMounted); + + if (bot->GetTarget() != attumenMounted->GetGUID()) + { + bot->SetTarget(attumenMounted->GetGUID()); + return Attack(attumenMounted); + } + } + else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithStar(bot, midnight); + + if (!botAI->IsAssistTankOfIndex(bot, 0)) + { + SetRtiTarget(botAI, "star", midnight); + + if (bot->GetTarget() != midnight->GetGUID()) + { + bot->SetTarget(midnight->GetGUID()); + return Attack(midnight); + } + } + } + + return false; +} -namespace +// Off tank should move Attumen out of the way so he doesn't cleave bots +bool AttumenTheHuntsmanSplitBossesAction::Execute(Event event) { - // Big Bad Wolf - static int currentIndex = 0; - // Netherspite - static std::map beamMoveTimes; - static std::map lastBeamMoveSideways; + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return false; + + Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN); + if (!attumen) + return false; + + MarkTargetWithSquare(bot, attumen); + SetRtiTarget(botAI, "square", attumen); + + if (bot->GetVictim() != attumen) + return Attack(attumen); + + if (attumen->GetVictim() == bot && midnight->GetVictim() != bot) + { + const float safeDistance = 6.0f; + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (nearestPlayer && attumen->GetExactDist2d(nearestPlayer) < safeDistance) + return MoveFromGroup(safeDistance + 2.0f); + } + + return false; } -bool KarazhanAttumenTheHuntsmanStackBehindAction::Execute(Event event) +// Stack behind mounted Attumen (inside minimum range of Berserker Charge) +bool AttumenTheHuntsmanStackBehindAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); - - float distance = 5.0f; - float orientation = boss->GetOrientation() + M_PI; - float x = boss->GetPositionX(); - float y = boss->GetPositionY(); - float z = boss->GetPositionZ(); - float rx = x + cos(orientation) * distance; - float ry = y + sin(orientation) * distance; - - if (bot->GetExactDist2d(rx, ry) > 1.0f) + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return false; + + const float distanceBehind = botAI->IsRanged(bot) ? 6.0f : 2.0f; + float orientation = attumenMounted->GetOrientation() + M_PI; + float rearX = attumenMounted->GetPositionX() + std::cos(orientation) * distanceBehind; + float rearY = attumenMounted->GetPositionY() + std::sin(orientation) * distanceBehind; + + if (bot->GetExactDist2d(rearX, rearY) > 1.0f) { - return MoveTo(bot->GetMapId(), rx, ry, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, attumenMounted->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } return false; } -bool KarazhanAttumenTheHuntsmanStackBehindAction::isUseful() +// Reset timer for bots to pause DPS when Attumen mounts Midnight +bool AttumenTheHuntsmanManageDpsTimerAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return false; - return boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot); + if (midnight && midnight->GetHealth() == midnight->GetMaxHealth()) + attumenDpsWaitTimer.erase(KARAZHAN_MAP_ID); + + // Midnight is still present as a separate (invisible) unit after Attumen mounts + // So this block can be reached + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return false; + + const time_t now = std::time(nullptr); + + if (attumenMounted) + attumenDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + + return false; } -bool KarazhanMoroesMarkTargetAction::Execute(Event event) +// Moroes + +bool MoroesMainTankAttackBossAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); + Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes"); + if (!moroes) + return false; + MarkTargetWithCircle(bot, moroes); + SetRtiTarget(botAI, "circle", moroes); + + if (bot->GetVictim() != moroes) + return Attack(moroes); + + return false; +} + +// Mark targets with skull in the recommended kill order +bool MoroesMarkTargetAction::Execute(Event event) +{ Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); Unit* rafe = AI_VALUE2(Unit*, "find target", "baron rafe dreuger"); Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - Unit* target = karazhanHelper.GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); + Unit* target = GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); + + if (target) + { + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithSkull(bot, target); - karazhanHelper.MarkTargetWithSkull(target); + SetRtiTarget(botAI, "skull", target); + } return false; } -bool KarazhanMaidenOfVirtuePositionBossAction::Execute(Event event) +// Maiden of Virtue + +// Tank the boss in the center of the room +// Move to healers after Repentenace to break the stun +bool MaidenOfVirtueMoveBossToHealerAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - Unit* healer = nullptr; + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + if (!maiden) + return false; + if (bot->GetVictim() != maiden) + return Attack(maiden); + + Unit* healer = nullptr; if (Group* group = bot->GetGroup()) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsHeal(member) || !member->HasAura(SPELL_REPENTANCE)) - { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsHeal(member) || + !member->HasAura(SPELL_REPENTANCE)) continue; - } + healer = member; break; } @@ -84,580 +218,534 @@ bool KarazhanMaidenOfVirtuePositionBossAction::Execute(Event event) if (healer) { float angle = healer->GetOrientation(); - float targetX = healer->GetPositionX() + cos(angle) * 6.0f; - float targetY = healer->GetPositionY() + sin(angle) * 6.0f; - float targetZ = healer->GetPositionZ(); + float targetX = healer->GetPositionX() + std::cos(angle) * 6.0f; + float targetY = healer->GetPositionY() + std::sin(angle) * 6.0f; { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + return MoveTo(KARAZHAN_MAP_ID, targetX, targetY, healer->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION); - - if (distanceToBossPosition > maxDistance) + const Position& position = MAIDEN_OF_VIRTUE_BOSS_POSITION; + const float maxDistance = 2.0f; + float distanceToPosition = maiden->GetExactDist2d(position); + if (distanceToPosition > maxDistance) { - float dX = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - float mX = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + float dX = position.GetPositionX() - maiden->GetPositionX(); + float dY = position.GetPositionY() - maiden->GetPositionY(); + float mX = position.GetPositionX() + (dX / distanceToPosition) * maxDistance; + float mY = position.GetPositionY() + (dY / distanceToPosition) * maxDistance; { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), mX, mY, - bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } return false; } -bool KarazhanMaidenOfVirtuePositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; -} - -bool KarazhanMaidenOfVirtuePositionRangedAction::Execute(Event event) +// Spread out ranged DPS between the pillars +bool MaidenOfVirtuePositionRangedAction::Execute(Event event) { - int maxIndex = 7; - int index = 0; - - const GuidVector members = AI_VALUE(GuidVector, "group members"); + const uint8 maxIndex = 7; + uint8 index = 0; - for (auto const& memberGuid : members) + if (Group* group = bot->GetGroup()) { - Unit* member = botAI->GetUnit(memberGuid); - - if (!member || !botAI->IsRanged(member->ToPlayer())) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; - } + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; - if (member == bot) - break; + if (member == bot) + break; - if (index >= maxIndex) - { - index = 0; - continue; + if (index >= maxIndex) + { + index = 0; + continue; + } + index++; } - index++; } - float distance = bot->GetExactDist2d(KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index]); - const float maxDistance = 2.0f; - if (distance > maxDistance) + const Position& position = MAIDEN_OF_VIRTUE_RANGED_POSITION[index]; + if (bot->GetExactDist2d(position) > 2.0f) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index].GetPositionX(), - KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[index].GetPositionY(), bot->GetPositionZ(), false, - false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanMaidenOfVirtuePositionRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); - - return boss && botAI->IsRanged(bot); -} +// The Big Bad Wolf -bool KarazhanBigBadWolfPositionBossAction::Execute(Event event) +// Tank the boss at the front left corner of the stage +bool BigBadWolfPositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + if (!wolf) + return false; - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION); + if (bot->GetVictim() != wolf) + return Attack(wolf); - if (distanceToBossPosition > maxDistance) + if (wolf->GetVictim() == bot) { - float dX = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); + const Position& position = BIG_BAD_WOLF_BOSS_POSITION; + const float maxStep = 2.0f; + float dist = wolf->GetExactDist2d(position); - float mX = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; - - float moveDistance = bot->GetExactDist2d(mX, mY); - if (moveDistance < 0.5f) + if (dist > 0.0f && dist > maxStep) { - return false; + float dX = position.GetPositionX() - wolf->GetPositionX(); + float dY = position.GetPositionY() - wolf->GetPositionY(); + float moveDist = std::min(maxStep, dist); + float moveX = wolf->GetPositionX() + (dX / dist) * moveDist; + float moveY = wolf->GetPositionY() + (dY / dist) * moveDist; + + return MoveTo(KARAZHAN_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } - - return MoveTo(bot->GetMapId(), mX, mY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanBigBadWolfPositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot && - !bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD); -} - -bool KarazhanBigBadWolfRunAwayAction::Execute(Event event) +// Run away, little girl, run away +bool BigBadWolfRunAwayFromBossAction::Execute(Event event) { - constexpr float threshold = 1.0f; - Position target = KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[currentIndex]; + const ObjectGuid botGuid = bot->GetGUID(); + uint8 index = bigBadWolfRunIndex.count(botGuid) ? bigBadWolfRunIndex[botGuid] : 0; - while (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) < threshold) + while (bot->GetExactDist2d(BIG_BAD_WOLF_RUN_POSITION[index].GetPositionX(), + BIG_BAD_WOLF_RUN_POSITION[index].GetPositionY()) < 1.0f) { - currentIndex = (currentIndex + 1) % 4; - target = KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[currentIndex]; + index = (index + 1) % 4; } + bigBadWolfRunIndex[botGuid] = index; bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + const Position& position = BIG_BAD_WOLF_RUN_POSITION[index]; + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } -bool KarazhanBigBadWolfRunAwayAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - - return boss && bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD); -} +// Romulo and Julianne -bool KarazhanRomuloAndJulianneMarkTargetAction::Execute(Event event) +// Keep the couple within 10% HP of each other +bool RomuloAndJulianneMarkTargetAction::Execute(Event event) { - Unit* target = nullptr; Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); + if (!romulo) + return false; + Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!julianne) + return false; + + Unit* target = nullptr; + const float maxPctDifference = 10.0f; - const int maxPctDifference = 10; if (julianne->GetHealthPct() + maxPctDifference < romulo->GetHealthPct() || julianne->GetHealthPct() < 1.0f) - { target = romulo; - } else if (romulo->GetHealthPct() + maxPctDifference < julianne->GetHealthPct() || romulo->GetHealthPct() < 1.0f) - { target = julianne; - } - if (!target) - { - return false; - } + else + target = (romulo->GetHealthPct() >= julianne->GetHealthPct()) ? romulo : julianne; - RaidKarazhanHelpers karazhanHelper(botAI); - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanWizardOfOzMarkTargetAction::Execute(Event event) +// The Wizard of Oz + +// Mark targets with skull in the recommended kill order +bool WizardOfOzMarkTargetAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - Unit* target = karazhanHelper.GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); + Unit* target = GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanWizardOfOzScorchStrawmanAction::Execute(Event event) +// Mages spam Scorch on Strawman to disorient him +bool WizardOfOzScorchStrawmanAction::Execute(Event event) { Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); - if (!strawman || !strawman->IsAlive() || bot->getClass() != CLASS_MAGE) - { - return false; - } - - if (botAI->CanCastSpell("scorch", strawman)) - { - botAI->CastSpell("scorch", strawman); - } + if (strawman && botAI->CanCastSpell("scorch", strawman)) + return botAI->CastSpell("scorch", strawman); return false; } -bool KarazhanTheCuratorMarkTargetAction::Execute(Event event) +// The Curator + +// Prioritize destroying Astral Flares +bool TheCuratorMarkAstralFlareAction::Execute(Event event) { - Unit* target = AI_VALUE2(Unit*, "find target", "astral flare"); - if (!target || !target->IsAlive()) - { + Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); + if (!flare) return false; - } - RaidKarazhanHelpers karazhanHelper(botAI); - karazhanHelper.MarkTargetWithSkull(target); + if (IsMapIDTimerManager(botAI, bot)) + MarkTargetWithSkull(bot, flare); + + SetRtiTarget(botAI, "skull", flare); return false; } -bool KarazhanTheCuratorPositionBossAction::Execute(Event event) +// Tank the boss in the center of the hallway near the Guardian's Library +// Main tank and off tank will attack the boss; others will focus on Astral Flares +bool TheCuratorPositionBossAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - const float maxDistance = 3.0f; - const float distanceToBossPosition = boss->GetExactDist2d(KARAZHAN_THE_CURATOR_BOSS_POSITION); + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return false; + + MarkTargetWithCircle(bot, curator); + SetRtiTarget(botAI, "circle", curator); - if (distanceToBossPosition > maxDistance) + if (bot->GetVictim() != curator) + return Attack(curator); + + if (curator->GetVictim() == bot) { - float dX = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionX() - boss->GetPositionX(); - float dY = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionY() - boss->GetPositionY(); - float mX = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; - float mY = KARAZHAN_THE_CURATOR_BOSS_POSITION.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + const Position& position = THE_CURATOR_BOSS_POSITION; + const float maxDistance = 3.0f; + float distanceToBossPosition = curator->GetExactDist2d(position); + + if (distanceToBossPosition > maxDistance) { - return MoveTo(bot->GetMapId(), mX, mY, - bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, - false); + float dX = position.GetPositionX() - curator->GetPositionX(); + float dY = position.GetPositionY() - curator->GetPositionY(); + float mX = position.GetPositionX() + (dX / distanceToBossPosition) * maxDistance; + float mY = position.GetPositionY() + (dY / distanceToBossPosition) * maxDistance; + + return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } } return false; } -bool KarazhanTheCuratorPositionBossAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; -} - -bool KarazhanTheCuratorSpreadRangedAction::Execute(Event event) +// Spread out ranged DPS to avoid Arcing Sear damage +bool TheCuratorSpreadRangedAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); const float minDistance = 5.0f; - Unit* nearestPlayer = karazhanHelper.GetNearestPlayerInRadius(minDistance); + Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance); if (nearestPlayer) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - + bot->InterruptNonMeleeSpells(true); return FleePosition(nearestPlayer->GetPosition(), minDistance); } return false; } -bool KarazhanTheCuratorSpreadRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); - - return boss && botAI->IsRanged(bot); -} +// Terestian Illhoof -bool KarazhanTerestianIllhoofMarkTargetAction::Execute(Event event) +// Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof +bool TerestianIllhoofMarkTargetAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "terestian illhoof"); - if (!boss) - { - return false; - } + Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains"); + Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek"); + Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_DEMON_CHAINS); - if (!target || !target->IsAlive()) - { - target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_KILREK); - if (!target || !target->IsAlive()) - { - target = boss; - } - } - karazhanHelper.MarkTargetWithSkull(target); + if (target) + MarkTargetWithSkull(bot, target); return false; } -bool KarazhanShadeOfAranArcaneExplosionRunAwayAction::Execute(Event event) +// Shade of Aran + +// Run to the edge of the room to avoid Arcane Explosion +bool ShadeOfAranRunAwayFromArcaneExplosionAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - const float safeDistance = 20.0f; - const float distance = bot->GetDistance2d(boss); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return false; + const float safeDistance = 20.0f; + float distance = bot->GetDistance2d(aran); if (distance < safeDistance) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveAway(boss, safeDistance - distance); + bot->InterruptNonMeleeSpells(true); + return MoveAway(aran, safeDistance - distance); } return false; } -bool KarazhanShadeOfAranArcaneExplosionRunAwayAction::isUseful() +// I will not move when Flame Wreath is cast or the raid blows up +bool ShadeOfAranStopMovingDuringFlameWreathAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - - return boss && boss->IsAlive() && boss->HasUnitState(UNIT_STATE_CASTING) && - boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION); -} + AI_VALUE(LastMovement&, "last movement").Set(nullptr); -bool KarazhanShadeOfAranFlameWreathStopMovementAction::Execute(Event event) -{ - RaidKarazhanHelpers karazhanHelper(botAI); - if (karazhanHelper.IsFlameWreathActive()) + if (bot->isMoving()) { - AI_VALUE(LastMovement&, "last movement").Set(nullptr); bot->GetMotionMaster()->Clear(); - if (bot->isMoving()) - { - bot->StopMoving(); - } + bot->StopMoving(); return true; } return false; } -bool KarazhanShadeOfAranMarkConjuredElementalAction::Execute(Event event) +// Mark Conjured Elementals with skull so DPS can burn them down +bool ShadeOfAranMarkConjuredElementalAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - Unit* target = karazhanHelper.GetFirstAliveUnitByEntry(NPC_CONJURED_ELEMENTAL); - - if (!boss || !boss->IsAlive() || - !target || !target->IsAlive() || target->HasAura(SPELL_WARLOCK_BANISH)) - { - return false; - } + Unit* elemental = GetFirstAliveUnitByEntry(botAI, NPC_CONJURED_ELEMENTAL); - karazhanHelper.MarkTargetWithSkull(target); + if (elemental) + MarkTargetWithSkull(bot, elemental); return false; } -bool KarazhanShadeOfAranSpreadRangedAction::Execute(Event event) +// Don't get closer than 11 yards to Aran to avoid counterspell +// Don't get farther than 15 yards from Aran to avoid getting stuck in alcoves +bool ShadeOfAranRangedMaintainDistanceAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + const float minDist = 11.0f; + const float maxDist = 15.0f; + const float ringIncrement = M_PI / 8; + const float distIncrement = 0.5f; + + float bestX = 0, bestY = 0, bestMoveDist = std::numeric_limits::max(); + bool found = false; - const float maxBossDistance = 12.0f; - float bossDistance = bot->GetExactDist2d(boss); - if (bossDistance > maxBossDistance) + for (float dist = minDist; dist <= maxDist; dist += distIncrement) { - float dX = bot->GetPositionX() - boss->GetPositionX(); - float dY = bot->GetPositionY() - boss->GetPositionY(); - float length = std::sqrt(dX * dX + dY * dY); - dX /= length; - dY /= length; - float tX = boss->GetPositionX() + dX * maxBossDistance; - float tY = boss->GetPositionY() + dY * maxBossDistance; + for (float angle = 0; angle < 2 * M_PI; angle += ringIncrement) { - return MoveTo(bot->GetMapId(), tX, tY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + float x = aran->GetPositionX() + std::cos(angle) * dist; + float y = aran->GetPositionY() + std::sin(angle) * dist; + + bool tooClose = false; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + if (member->GetExactDist2d(x, y) < 3.0f) + { + tooClose = true; + break; + } + } + if (tooClose) + continue; + + float moveDist = bot->GetExactDist2d(x, y); + if (moveDist < bestMoveDist) + { + bestMoveDist = moveDist; + bestX = x; + bestY = y; + found = true; + } } } - const float minDistance = 5.0f; - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* nearestPlayer = karazhanHelper.GetNearestPlayerInRadius(minDistance); - if (nearestPlayer) + if (found && bestMoveDist > 0.5f) { - return FleePosition(nearestPlayer->GetPosition(), minDistance); + return MoveTo(KARAZHAN_MAP_ID, bestX, bestY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanShadeOfAranSpreadRangedAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - RaidKarazhanHelpers karazhanHelper(botAI); - - return boss && boss->IsAlive() && botAI->IsRanged(bot) && !karazhanHelper.IsFlameWreathActive() && - !(boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)); -} +// Netherspite // One tank bot per phase will dance in and out of the red beam (5 seconds in, 5 seconds out) -// Tank bots will ignore void zones--their positioning is too important -bool KarazhanNetherspiteBlockRedBeamAction::Execute(Event event) +// Tank bots will ignore void zones--their positioning is too important to risk losing beam control +bool NetherspiteBlockRedBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + if (!redPortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingRedBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == redBlocker); - bool wasBlocking = wasBlockingRedBeam[botGuid]; - Position beamPos = karazhanHelper.GetPositionOnBeam(boss, redPortal, 18.0f); + auto it = _wasBlockingRedBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingRedBeam.end()) ? it->second : false; + + Position beamPos = GetPositionOnBeam(netherspite, redPortal, 18.0f); if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_red", "%player is moving to block the red beam!", ph); + "netherspite_beam_blocking_red", "%player is moving to block the red beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingRedBeam[botGuid] = true; + _wasBlockingRedBeam[botGuid] = true; - uint32 intervalSecs = 5; - - if (beamMoveTimes[botGuid] == 0) - { - beamMoveTimes[botGuid] = time(nullptr); - lastBeamMoveSideways[botGuid] = false; - } - if (time(nullptr) - beamMoveTimes[botGuid] >= intervalSecs) + const uint8 intervalSecs = 5; + if (std::time(nullptr) - redBeamMoveTimer[botGuid] >= intervalSecs) { lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid]; - beamMoveTimes[botGuid] = time(nullptr); + redBeamMoveTimer[botGuid] = std::time(nullptr); } if (!lastBeamMoveSideways[botGuid]) - { - return MoveTo(bot->GetMapId(), beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } + return MoveTo(KARAZHAN_MAP_ID, beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); else { - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float px = redPortal->GetPositionX(); - float py = redPortal->GetPositionY(); - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(redPortal); if (length == 0.0f) - { return false; - } - dx /= length; - dy /= length; + float dx = (redPortal->GetPositionX() - netherspite->GetPositionX()) / length; + float dy = (redPortal->GetPositionY() - netherspite->GetPositionY()) / length; float perpDx = -dy; float perpDy = dx; float sideX = beamPos.GetPositionX() + perpDx * 3.0f; float sideY = beamPos.GetPositionY() + perpDy * 3.0f; float sideZ = beamPos.GetPositionZ(); - return MoveTo(bot->GetMapId(), sideX, sideY, sideZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); + return MoveTo(KARAZHAN_MAP_ID, sideX, sideY, sideZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } - wasBlockingRedBeam[botGuid] = false; + _wasBlockingRedBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockRedBeamAction::isUseful() +Position NetherspiteBlockRedBeamAction::GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); - - ObjectGuid botGuid = bot->GetGUID(); - static std::map lastBossBanishState; - bool bossIsBanished = boss && boss->HasAura(SPELL_NETHERSPITE_BANISHED); - - if (lastBossBanishState[botGuid] != bossIsBanished) - { - if (!bossIsBanished) - { - beamMoveTimes[botGuid] = 0; - lastBeamMoveSideways[botGuid] = false; - } - lastBossBanishState[botGuid] = bossIsBanished; - } - - return boss && redPortal && !bossIsBanished; + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); + float px = portal->GetPositionX(); + float py = portal->GetPositionY(); + + float dx = px - bx; + float dy = py - by; + float length = netherspite->GetExactDist2d(px, py); + if (length == 0.0f) + return Position(bx, by, bz); + + dx /= length; + dy /= length; + float targetX = bx + dx * distanceFromBoss; + float targetY = by + dy * distanceFromBoss; + float targetZ = bz; + + return Position(targetX, targetY, targetZ); } -// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 26 debuff stacks) +// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 25 debuff stacks) // When avoiding void zones, blocking bots will move along the beam to continue blocking -bool KarazhanNetherspiteBlockBlueBeamAction::Execute(Event event) +bool NetherspiteBlockBlueBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + if (!bluePortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingBlueBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == blueBlocker); - bool wasBlocking = wasBlockingBlueBeam[botGuid]; + + auto it = _wasBlockingBlueBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingBlueBeam.end()) ? it->second : false; if (wasBlocking && !isBlockingNow) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", ph); + "netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", placeholders); bot->Yell(text, LANG_UNIVERSAL); - wasBlockingBlueBeam[botGuid] = false; - - return false; + _wasBlockingBlueBeam[botGuid] = false; } if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", ph); + "netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingBlueBeam[botGuid] = true; + _wasBlockingBlueBeam[botGuid] = true; + + float idealDistance = botAI->IsRanged(bot) ? 25.0f : 18.0f; + std::vector voidZones = GetAllVoidZones(botAI, bot); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); float px = bluePortal->GetPositionX(); float py = bluePortal->GetPositionY(); + float dx = px - bx; float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(bluePortal); if (length == 0.0f) - { return false; - } dx /= length; dy /= length; float bestDist = 150.0f; Position bestPos; bool found = false; + for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f) { float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - bool outsideAllVoidZones = true; - for (Unit* voidZone : voidZones) - { - float voidZoneDist = sqrt(pow(candidateX - voidZone->GetPositionX(), 2) + - pow(candidateY - voidZone->GetPositionY(), 2)); - if (voidZoneDist < 4.0f) - { - outsideAllVoidZones = false; - break; - } - } - if (!outsideAllVoidZones) - { + if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) continue; - } - float distToIdeal = fabs(dist - 18.0f); + + float distToIdeal = fabs(dist - idealDistance); if (!found || distToIdeal < bestDist) { bestDist = distToIdeal; @@ -665,490 +753,454 @@ bool KarazhanNetherspiteBlockBlueBeamAction::Execute(Event event) found = true; } } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; } - wasBlockingBlueBeam[botGuid] = false; + _wasBlockingBlueBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockBlueBeamAction::isUseful() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); - - return boss && bluePortal && !boss->HasAura(SPELL_NETHERSPITE_BANISHED); -} - -// Two healer bots will block the green beam for each phase (swap at 26 debuff stacks) +// Two healer bots will block the green beam for each phase (swap at 25 debuff stacks) // OR one rogue or DPS warrior bot will block the green beam for an entire phase (if they begin the phase as the blocker) // When avoiding void zones, blocking bots will move along the beam to continue blocking -bool KarazhanNetherspiteBlockGreenBeamAction::Execute(Event event) +bool NetherspiteBlockGreenBeamAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; + Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + if (!greenPortal) + return false; - RaidKarazhanHelpers karazhanHelper(botAI); - static std::map wasBlockingGreenBeam; - ObjectGuid botGuid = bot->GetGUID(); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); + const ObjectGuid botGuid = bot->GetGUID(); + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); bool isBlockingNow = (bot == greenBlocker); - bool wasBlocking = wasBlockingGreenBeam[botGuid]; + + auto it = _wasBlockingGreenBeam.find(botGuid); + bool wasBlocking = (it != _wasBlockingGreenBeam.end()) ? it->second : false; if (wasBlocking && !isBlockingNow) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", ph); + "netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", placeholders); bot->Yell(text, LANG_UNIVERSAL); - wasBlockingGreenBeam[botGuid] = false; - - return false; + _wasBlockingGreenBeam[botGuid] = false; } if (isBlockingNow) { if (!wasBlocking) { - std::map ph; - ph["%player"] = bot->GetName(); + std::map placeholders{{"%player", bot->GetName()}}; std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( - "netherspite_beam_blocking_green", "%player is moving to block the green beam!", ph); + "netherspite_beam_blocking_green", "%player is moving to block the green beam!", placeholders); bot->Yell(text, LANG_UNIVERSAL); } - wasBlockingGreenBeam[botGuid] = true; + _wasBlockingGreenBeam[botGuid] = true; - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); + std::vector voidZones = GetAllVoidZones(botAI, bot); + + float bx = netherspite->GetPositionX(); + float by = netherspite->GetPositionY(); + float bz = netherspite->GetPositionZ(); float px = greenPortal->GetPositionX(); float py = greenPortal->GetPositionY(); + float dx = px - bx; float dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(greenPortal); if (length == 0.0f) - { return false; - } dx /= length; dy /= length; float bestDist = 150.0f; Position bestPos; bool found = false; + for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f) { float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - bool outsideAllVoidZones = true; - for (Unit* voidZone : voidZones) - { - float voidZoneDist = sqrt(pow(candidateX - voidZone->GetPositionX(), 2) + - pow(candidateY - voidZone->GetPositionY(), 2)); - if (voidZoneDist < 4.0f) - { - outsideAllVoidZones = false; - break; - } - } - if (!outsideAllVoidZones) - { - continue; - } - float distToIdeal = fabs(dist - 18.0f); - if (!found || distToIdeal < bestDist) + if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) + continue; + + float distToIdeal = fabs(dist - 18.0f); + if (!found || distToIdeal < bestDist) { bestDist = distToIdeal; bestPos = Position(candidateX, candidateY, candidateZ); found = true; } } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; } - wasBlockingGreenBeam[botGuid] = false; + _wasBlockingGreenBeam[botGuid] = false; return false; } -bool KarazhanNetherspiteBlockGreenBeamAction::isUseful() +// All bots not currently blocking a beam will avoid beams and void zones +bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) + return false; - return boss && greenPortal && !boss->HasAura(SPELL_NETHERSPITE_BANISHED); -} + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + std::vector voidZones = GetAllVoidZones(botAI, bot); + + bool nearVoidZone = !IsSafePosition(bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), voidZones, 4.0f); -// All bots not currently blocking a beam will avoid beams and void zones -bool KarazhanNetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - bool nearVoidZone = false; - for (Unit* vz : voidZones) - { - if (bot->GetExactDist2d(vz) < 4.0f) - { - nearVoidZone = true; - break; - } - } - struct BeamAvoid { Unit* portal; float minDist, maxDist; }; std::vector beams; Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + if (redPortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = redPortal->GetPositionX(), py = redPortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(redPortal); beams.push_back({redPortal, 0.0f, length}); } + if (bluePortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = bluePortal->GetPositionX(), py = bluePortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(bluePortal); beams.push_back({bluePortal, 0.0f, length}); } + if (greenPortal) { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = greenPortal->GetPositionX(), py = greenPortal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); + float length = netherspite->GetExactDist2d(greenPortal); beams.push_back({greenPortal, 0.0f, length}); } - bool nearBeam = false; - for (auto const& beam : beams) - { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = bot->GetPositionX() - bx, botdy = bot->GetPositionY() - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(bot->GetPositionX() - beamX, 2) + pow(bot->GetPositionY() - beamY, 2)); - if (distToBeam < 5.0f && t > beam.minDist && t < beam.maxDist) - { - nearBeam = true; - break; - } - } + + bool nearBeam = !IsAwayFromBeams(bot->GetPositionX(), bot->GetPositionY(), beams, netherspite); + if (!nearVoidZone && !nearBeam) - { return false; - } - const float minMoveDist = 2.0f, maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f; - float bossZ = boss->GetPositionZ(); + const float minMoveDist = 2.0f; + const float minMoveDistSq = minMoveDist * minMoveDist; + const float maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f; + float netherspiteZ = netherspite->GetPositionZ(); Position bestCandidate; - float bestDist = 0.0f; + float bestDistSq = std::numeric_limits::max(); bool found = false; + + const float botX = bot->GetPositionX(); + const float botY = bot->GetPositionY(); + for (float angle = 0; angle < 2 * M_PI; angle += stepAngle) { for (float dist = 2.0f; dist <= maxSearchDist; dist += stepDist) { - float cx = bot->GetPositionX() + cos(angle) * dist; - float cy = bot->GetPositionY() + sin(angle) * dist; - float cz = bossZ; - if (std::any_of(voidZones.begin(), voidZones.end(), [&](Unit* vz){ return Position(cx, cy, cz).GetExactDist2d(vz) < 4.0f; })) - { - continue; - } - bool tooCloseToBeam = false; - for (auto const& beam : beams) - { - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = cx - bx, botdy = cy - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(cx - beamX, 2) + pow(cy - beamY, 2)); - if (distToBeam < 5.0f && t > beam.minDist && t < beam.maxDist) - { - tooCloseToBeam = true; - break; - } - } - if (tooCloseToBeam) - { + float cx = botX + std::cos(angle) * dist; + float cy = botY + std::sin(angle) * dist; + float cz = netherspiteZ; + + if (!IsSafePosition(cx, cy, cz, voidZones, 4.0f) || + !IsAwayFromBeams(cx, cy, beams, netherspite)) continue; - } - float moveDist = sqrt(pow(cx - bot->GetPositionX(), 2) + pow(cy - bot->GetPositionY(), 2)); - if (moveDist < minMoveDist) - { + + float dx = cx - botX; + float dy = cy - botY; + float moveDistSq = dx*dx + dy*dy; + if (moveDistSq < minMoveDistSq) continue; - } - if (!found || moveDist < bestDist) + + if (!found || moveDistSq < bestDistSq) { bestCandidate = Position(cx, cy, cz); - bestDist = moveDist; + bestDistSq = moveDistSq; found = true; } } } - if (found && karazhanHelper.IsSafePosition(bestCandidate.GetPositionX(), - bestCandidate.GetPositionY(), bestCandidate.GetPositionZ(), - voidZones, 4.0f)) + + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - - return MoveTo(bot->GetMapId(), bestCandidate.GetPositionX(), bestCandidate.GetPositionY(), - bestCandidate.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestCandidate.GetPositionX(), bestCandidate.GetPositionY(), + bestCandidate.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; } -bool KarazhanNetherspiteAvoidBeamAndVoidZoneAction::isUseful() +bool NetherspiteAvoidBeamAndVoidZoneAction::IsAwayFromBeams( + float x, float y, const std::vector& beams, Unit* netherspite) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || boss->HasAura(SPELL_NETHERSPITE_BANISHED)) + for (auto const& beam : beams) { - return false; - } + float bx = netherspite->GetPositionX(), by = netherspite->GetPositionY(); + float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY(); + float dx = px - bx, dy = py - by; + float length = netherspite->GetExactDist2d(beam.portal); - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - if (bot == redBlocker || bot == blueBlocker || bot == greenBlocker) - { - return false; + if (length == 0.0f) + continue; + + dx /= length; dy /= length; + float botdx = x - bx, botdy = y - by; + float distanceAlongBeam = (botdx * dx + botdy * dy); + float beamX = bx + dx * distanceAlongBeam, beamY = by + dy * distanceAlongBeam; + float distToBeam = sqrt((x - beamX) * (x - beamX) + (y - beamY) * (y - beamY)); + + if (distToBeam < 5.0f && distanceAlongBeam > beam.minDist && distanceAlongBeam < beam.maxDist) + return false; } return true; } -bool KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event) +bool NetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event) { - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); + std::vector voidZones = GetAllVoidZones(botAI, bot); for (Unit* vz : voidZones) { if (vz->GetEntry() == NPC_VOID_ZONE && bot->GetExactDist2d(vz) < 4.0f) - { return FleePosition(vz->GetPosition(), 4.0f); - } } return false; } -bool KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction::isUseful() +bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->HasAura(SPELL_NETHERSPITE_BANISHED)) - { + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite) return false; + + const ObjectGuid botGuid = bot->GetGUID(); + const time_t now = std::time(nullptr); + + // DpsWaitTimer is for pausing DPS during phase transitions + // redBeamMoveTimer and lastBeamMoveSideways are for tank dancing in/out of the red beam + if (netherspite->GetHealth() == netherspite->GetMaxHealth() && + !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) + { + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.insert_or_assign(KARAZHAN_MAP_ID, now); + + if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) + { + redBeamMoveTimer.erase(botGuid); + lastBeamMoveSideways.erase(botGuid); + } } + else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + { + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.erase(KARAZHAN_MAP_ID); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - for (Unit* vz : voidZones) + if (botAI->IsTank(bot)) + { + redBeamMoveTimer.erase(botGuid); + lastBeamMoveSideways.erase(botGuid); + } + } + else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (bot->GetExactDist2d(vz) < 4.0f) + if (IsMapIDTimerManager(botAI, bot)) + netherspiteDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + + if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) { - return true; + redBeamMoveTimer.try_emplace(botGuid, now); + lastBeamMoveSideways.try_emplace(botGuid, false); } } return false; } -bool KarazhanPrinceMalchezaarNonTankAvoidHazardAction::Execute(Event event) +// Move away from the boss to avoid Shadow Nova when Enfeebled +// Do not cross within Infernal Hellfire radius while doing so +bool PrinceMalchezaarEnfeebledAvoidHazardAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector infernals = karazhanHelper.GetSpawnedInfernals(); + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; + + std::vector infernals = GetSpawnedInfernals(botAI); const float minSafeBossDistance = 34.0f; + const float minSafeBossDistanceSq = minSafeBossDistance * minSafeBossDistance; const float maxSafeBossDistance = 60.0f; const float safeInfernalDistance = 23.0f; - const float stepSize = 0.5f; - const int numAngles = 64; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float distIncrement = 0.5f; + const uint8 numAngles = 64; + float bx = bot->GetPositionX(); float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); - float bossX = boss->GetPositionX(); - float bossY = boss->GetPositionY(); - float bossZ = boss->GetPositionZ(); - float bestMoveDist = std::numeric_limits::max(); + float malchezaarX = malchezaar->GetPositionX(); + float malchezaarY = malchezaar->GetPositionY(); + float malchezaarZ = malchezaar->GetPositionZ(); + float bestMoveDistSq = std::numeric_limits::max(); float bestDestX = 0.0f, bestDestY = 0.0f, bestDestZ = bz; bool found = false; - if (bot->HasAura(SPELL_ENFEEBLE)) + for (int i = 0; i < numAngles; ++i) { - for (int i = 0; i < numAngles; ++i) + float angle = (2 * M_PI * i) / numAngles; + float dx = std::cos(angle); + float dy = std::sin(angle); + + for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += distIncrement) { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += stepSize) + float x = malchezaarX + dx * dist; + float y = malchezaarY + dy * dist; + float destZ = malchezaarZ; + float destX = x, destY = y; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) + continue; + + float ddx = destX - malchezaarX; + float ddy = destY - malchezaarY; + float distFromBossSq = ddx*ddx + ddy*ddy; + if (distFromBossSq < minSafeBossDistanceSq) + continue; + + bool pathSafe = IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ), + infernals, safeInfernalDistance, distIncrement); + float mdx = destX - bx; + float mdy = destY - by; + float moveDistSq = mdx*mdx + mdy*mdy; + + if (pathSafe && moveDistSq < bestMoveDistSq) { - float x = bossX + dx * dist; - float y = bossY + dy * dist; - float destZ = bossZ; - float destX = x, destY = y, destZ2 = destZ; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ2, true)) - { - continue; - } - float distFromBoss = sqrt(pow(destX - bossX, 2) + pow(destY - bossY, 2)); - if (distFromBoss < minSafeBossDistance) - { - continue; - } - bool pathSafe = karazhanHelper.IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ2), - infernals, safeInfernalDistance, stepSize); - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (pathSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ2; - found = true; - } + bestMoveDistSq = moveDistSq; + bestDestX = destX; + bestDestY = destY; + bestDestZ = destZ; + found = true; } } - if (found) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + } - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - } + if (found) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} +// Move away from infernals while staying within range of the boss +// Prioritize finding a safe path to the new location, but will fallback to just finding a safe location if needed +bool PrinceMalchezaarNonTankAvoidInfernalAction::Execute(Event event) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) return false; - } - if (!bot->HasAura(SPELL_ENFEEBLE)) + std::vector infernals = GetSpawnedInfernals(botAI); + + const float safeInfernalDistance = 23.0f; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float maxSafeBossDistance = 35.0f; + + float bx = bot->GetPositionX(); + float by = bot->GetPositionY(); + float bz = bot->GetPositionZ(); + float malchezaarX = malchezaar->GetPositionX(); + float malchezaarY = malchezaar->GetPositionY(); + float malchezaarZ = malchezaar->GetPositionZ(); + + bool nearInfernal = false; + for (Unit* infernal : infernals) { - bool nearInfernal = false; - for (Unit* infernal : infernals) + float dx = bx - infernal->GetPositionX(); + float dy = by - infernal->GetPositionY(); + float infernalDistSq = dx*dx + dy*dy; + if (infernalDistSq < safeInfernalDistanceSq) { - float infernalDist = sqrt(pow(bx - infernal->GetPositionX(), 2) + pow(by - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - nearInfernal = true; - break; - } + nearInfernal = true; + break; } - if (nearInfernal) + } + + float bestDestX = bx, bestDestY = by, bestDestZ = bz; + bool found = false; + + if (nearInfernal) + { + const float distIncrement = 0.5f; + const uint8 numAngles = 64; + + // 1. Try to find a safe position with a safe path + found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance, + true, bestDestX, bestDestY, bestDestZ); + + // 2. Fallback: try to find a safe position (ignore path safety) + if (!found) { - float bestMoveDist = std::numeric_limits::max(); - float bestDestX = bx, bestDestY = by, bestDestZ = bz; - bool found = false; - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSafeBossDistance; dist += stepSize) - { - float x = bossX + dx * dist; - float y = bossY + dy * dist; - float destZ = bossZ; - float destX = x, destY = y, destZ2 = destZ; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bossX, bossY, bossZ, destX, destY, destZ2, true)) - { - continue; - } - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - if (!destSafe) - continue; - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ2; - found = true; - } - } - } - if (found) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance, + false, bestDestX, bestDestY, bestDestZ); + } - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } + if (found) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); } } return false; } -bool KarazhanPrinceMalchezaarNonTankAvoidHazardAction::isUseful() +// This is similar to the non-tank avoid infernal action, but the movement is based on the bot's location +// And the safe distance from infernals is larger to give melee more room to maneuver +bool PrinceMalchezaarMainTankMovementAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return false; - return boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot); -} + if (bot->GetVictim() != malchezaar) + return Attack(malchezaar); + + std::vector infernals = GetSpawnedInfernals(botAI); + + const float safeInfernalDistance = 30.0f; + const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance; + const float maxSampleDist = 75.0f; -bool KarazhanPrinceMalchezaarTankAvoidHazardAction::Execute(Event event) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - RaidKarazhanHelpers karazhanHelper(botAI); - std::vector infernals = karazhanHelper.GetSpawnedInfernals(); - - const float safeInfernalDistance = 28.0f; - const float stepSize = 0.5f; - const int numAngles = 64; - const float maxSampleDist = 60.0f; float bx = bot->GetPositionX(); float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); @@ -1156,116 +1208,283 @@ bool KarazhanPrinceMalchezaarTankAvoidHazardAction::Execute(Event event) bool nearInfernal = false; for (Unit* infernal : infernals) { - float infernalDist = sqrt(pow(bx - infernal->GetPositionX(), 2) + pow(by - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) + float dx = bx - infernal->GetPositionX(); + float dy = by - infernal->GetPositionY(); + float infernalDistSq = dx*dx + dy*dy; + if (infernalDistSq < safeInfernalDistanceSq) { nearInfernal = true; break; } } - float bestMoveDist = std::numeric_limits::max(); float bestDestX = bx, bestDestY = by, bestDestZ = bz; bool found = false; if (nearInfernal) { - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) - { - float x = bx + dx * dist; - float y = by + dy * dist; - float z = bz; - - float destX = x, destY = y, destZ = z; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) - continue; + const float distIncrement = 0.5f; + const uint8 numAngles = 64; - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - if (!destSafe) - continue; + // 1. Try to find a safe position with a safe path + found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist, + true, bestDestX, bestDestY, bestDestZ); - bool pathSafe = karazhanHelper.IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ), - infernals, safeInfernalDistance, stepSize); - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (pathSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ; - found = true; - } - } - } + // 2. Fallback: try to find a safe position (ignore path safety) if (!found) { - for (int i = 0; i < numAngles; ++i) - { - float angle = (2 * M_PI * i) / numAngles; - float dx = cos(angle); - float dy = sin(angle); - for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) - { - float x = bx + dx * dist; - float y = by + dy * dist; - float z = bz; - - float destX = x, destY = y, destZ = z; - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true)) - continue; - - bool destSafe = true; - for (Unit* infernal : infernals) - { - float infernalDist = sqrt(pow(destX - infernal->GetPositionX(), 2) + pow(destY - infernal->GetPositionY(), 2)); - if (infernalDist < safeInfernalDistance) - { - destSafe = false; - break; - } - } - float moveDist = sqrt(pow(destX - bx, 2) + pow(destY - by, 2)); - if (destSafe && moveDist < bestMoveDist) - { - bestMoveDist = moveDist; - bestDestX = destX; - bestDestY = destY; - bestDestZ = destZ; - found = true; - } - } - } + found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz, + infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist, + false, bestDestX, bestDestY, bestDestZ); } + if (found) { bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); + return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +// The tank position is near the Southeastern area of the Master's Terrace +// The tank moves Nightbane into position in two steps to try to get Nightbane to face sideways to the raid +bool NightbaneGroundPhasePositionBossAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + MarkTargetWithSkull(bot, nightbane); + + if (bot->GetVictim() != nightbane) + return Attack(nightbane); + + const ObjectGuid botGuid = bot->GetGUID(); + uint8 step = nightbaneTankStep.count(botGuid) ? nightbaneTankStep[botGuid] : 0; + + if (nightbane->GetVictim() == bot) + { + const Position tankPositions[2] = + { + NIGHTBANE_TRANSITION_BOSS_POSITION, + NIGHTBANE_FINAL_BOSS_POSITION + }; + const Position& position = tankPositions[step]; + const float maxDistance = 0.5f; + float distanceToTarget = bot->GetExactDist2d(position); - return MoveTo(bot->GetMapId(), bestDestX, bestDestY, bestDestZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); + if ((distanceToTarget > maxDistance) && bot->IsWithinMeleeRange(nightbane)) + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); + + if (step == 0 && distanceToTarget <= maxDistance) + nightbaneTankStep[botGuid] = 1; + + if (step == 1 && distanceToTarget <= maxDistance) + { + float orientation = atan2(nightbane->GetPositionY() - bot->GetPositionY(), + nightbane->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); } } return false; } -bool KarazhanPrinceMalchezaarTankAvoidHazardAction::isUseful() +// Ranged bots rotate between 3 positions to avoid standing in Charred Earth, which lasts for +// 30s and has a minimum cooldown of 18s (so there can be 2 active at once) +// Ranged positions are near the Northeastern door to the tower +bool NightbaneGroundPhaseRotateRangedPositionsAction::Execute(Event event) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + const ObjectGuid botGuid = bot->GetGUID(); + uint8 index = nightbaneRangedStep.count(botGuid) ? nightbaneRangedStep[botGuid] : 0; + + const Position rangedPositions[3] = + { + NIGHTBANE_RANGED_POSITION1, + NIGHTBANE_RANGED_POSITION2, + NIGHTBANE_RANGED_POSITION3 + }; + const Position& position = rangedPositions[index]; + const float maxDistance = 2.0f; + float distanceToTarget = bot->GetExactDist2d(position); + + if (distanceToTarget <= maxDistance && + bot->HasAura(SPELL_CHARRED_EARTH) && !bot->HasAura(SPELL_BELLOWING_ROAR)) + { + index = (index + 1) % 3; + nightbaneRangedStep[botGuid] = index; + const Position& newPosition = rangedPositions[index]; + float newDistanceToTarget = bot->GetExactDist2d(newPosition); + if (newDistanceToTarget > maxDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, newPosition.GetPositionX(), newPosition.GetPositionY(), newPosition.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + return false; + } - return boss && botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot; + if (distanceToTarget > maxDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// For countering Bellowing Roars during the ground phase +bool NightbaneCastFearWardOnMainTankAction::Execute(Event event) +{ + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Put pets on passive during the flight phase so they don't try to chase Nightbane off the map +bool NightbaneControlPetAggressionAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Pet* pet = bot->GetPet(); + if (!pet) + return false; + + if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z && pet->GetReactState() == REACT_PASSIVE) + pet->SetReactState(REACT_DEFENSIVE); + + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z && pet->GetReactState() != REACT_PASSIVE) + { + pet->AttackStop(); + pet->SetReactState(REACT_PASSIVE); + } + + return false; +} + +// 1. Stack at the "Flight Stack Position" near Nightbane so he doesn't use Fireball Barrage +// 2. Once Rain of Bones hits, the whole party moves to a new stack position +// This action lasts for the first 35 seconds of the flight phase, after which Nightbane gets +// ready to land, and the player will need to lead the bots over near the ground phase position +bool NightbaneFlightPhaseMovementAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + return false; + + MarkTargetWithMoon(bot, nightbane); + + Unit* botTarget = botAI->GetUnit(bot->GetTarget()); + if (botTarget && botTarget == nightbane) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + } + + const ObjectGuid botGuid = bot->GetGUID(); + bool hasRainOfBones = bot->HasAura(SPELL_RAIN_OF_BONES); + + if (hasRainOfBones) + nightbaneRainOfBonesHit[botGuid] = true; + + float destX, destY, destZ; + if (nightbaneRainOfBonesHit[botGuid]) + { + destX = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionX(); + destY = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionY(); + destZ = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionZ(); + } + else + { + destX = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionX(); + destY = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionY(); + destZ = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionZ(); + } + + if (bot->GetExactDist2d(destX, destY) > 2.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(KARAZHAN_MAP_ID, destX, destY, destZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool NightbaneManageTimersAndTrackersAction::Execute(Event event) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + const ObjectGuid botGuid = bot->GetGUID(); + const time_t now = std::time(nullptr); + + // Erase DPS wait timer and tank and ranged position tracking on encounter reset + if (nightbane->GetHealth() == nightbane->GetMaxHealth()) + { + if (botAI->IsMainTank(bot)) + nightbaneTankStep.erase(botGuid); + + if (botAI->IsRanged(bot)) + nightbaneRangedStep.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + nightbaneDpsWaitTimer.erase(KARAZHAN_MAP_ID); + } + // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer + else if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + { + nightbaneRainOfBonesHit.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + { + nightbaneFlightPhaseStartTimer.erase(KARAZHAN_MAP_ID); + nightbaneDpsWaitTimer.try_emplace(KARAZHAN_MAP_ID, now); + } + } + // Erase DPS wait timer and tank and ranged position tracking and start flight phase timer + // at beginning of flight phase + else if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + { + if (botAI->IsMainTank(bot)) + nightbaneTankStep.erase(botGuid); + + if (botAI->IsRanged(bot)) + nightbaneRangedStep.erase(botGuid); + + if (IsMapIDTimerManager(botAI, bot)) + { + nightbaneDpsWaitTimer.erase(KARAZHAN_MAP_ID); + nightbaneFlightPhaseStartTimer.try_emplace(KARAZHAN_MAP_ID, now); + } + } + + return false; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanActions.h b/src/strategy/raids/karazhan/RaidKarazhanActions.h index 4ab24ed94f..2d50944cd3 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanActions.h +++ b/src/strategy/raids/karazhan/RaidKarazhanActions.h @@ -2,217 +2,322 @@ #define _PLAYERBOT_RAIDKARAZHANACTIONS_H #include "Action.h" +#include "AttackAction.h" #include "MovementActions.h" -class KarazhanAttumenTheHuntsmanStackBehindAction : public MovementAction +class ManaWarpStunCreatureBeforeWarpBreachAction : public AttackAction { public: - KarazhanAttumenTheHuntsmanStackBehindAction(PlayerbotAI* botAI, std::string const name = "karazhan attumen the huntsman stack behind") : MovementAction(botAI, name) {} + ManaWarpStunCreatureBeforeWarpBreachAction( + PlayerbotAI* botAI, std::string const name = "mana warp stun creature before warp breach") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanMoroesMarkTargetAction : public Action +class AttumenTheHuntsmanMarkTargetAction : public AttackAction { public: - KarazhanMoroesMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan moroes mark target") : Action(botAI, name) {} - + AttumenTheHuntsmanMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman mark target") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanMaidenOfVirtuePositionBossAction : public MovementAction +class AttumenTheHuntsmanSplitBossesAction : public AttackAction { public: - KarazhanMaidenOfVirtuePositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan maiden of virtue position boss") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanSplitBossesAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman split bosses") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanMaidenOfVirtuePositionRangedAction : public MovementAction +class AttumenTheHuntsmanStackBehindAction : public MovementAction { public: - KarazhanMaidenOfVirtuePositionRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan maiden of virtue position ranged") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanStackBehindAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman stack behind") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanBigBadWolfPositionBossAction : public MovementAction +class AttumenTheHuntsmanManageDpsTimerAction : public Action { public: - KarazhanBigBadWolfPositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan big bad wolf position boss") : MovementAction(botAI, name) {} - + AttumenTheHuntsmanManageDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "attumen the huntsman manage dps timer") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanBigBadWolfRunAwayAction : public MovementAction +class MoroesMainTankAttackBossAction : public AttackAction { public: - KarazhanBigBadWolfRunAwayAction(PlayerbotAI* botAI, std::string const name = "karazhan big bad wolf run away") : MovementAction(botAI, name) {} - + MoroesMainTankAttackBossAction( + PlayerbotAI* botAI, std::string const name = "moroes main tank attack boss") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; - -private: - size_t currentIndex = 0; }; -class KarazhanRomuloAndJulianneMarkTargetAction : public Action +class MoroesMarkTargetAction : public Action { public: - KarazhanRomuloAndJulianneMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan romulo and julianne mark target") : Action(botAI, name) {} - + MoroesMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "moroes mark target") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanWizardOfOzMarkTargetAction : public Action +class MaidenOfVirtueMoveBossToHealerAction : public AttackAction { public: - KarazhanWizardOfOzMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan wizard of oz mark target") : Action(botAI, name) {} - + MaidenOfVirtueMoveBossToHealerAction( + PlayerbotAI* botAI, std::string const name = "maiden of virtue move boss to healer") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanWizardOfOzScorchStrawmanAction : public Action +class MaidenOfVirtuePositionRangedAction : public MovementAction { public: - KarazhanWizardOfOzScorchStrawmanAction(PlayerbotAI* botAI, std::string const name = "karazhan wizard of oz scorch strawman") : Action(botAI, name) {} - + MaidenOfVirtuePositionRangedAction( + PlayerbotAI* botAI, std::string const name = "maiden of virtue position ranged") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanTheCuratorMarkTargetAction : public Action +class BigBadWolfPositionBossAction : public AttackAction { public: - KarazhanTheCuratorMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator mark target") : Action(botAI, name) {} - + BigBadWolfPositionBossAction( + PlayerbotAI* botAI, std::string const name = "big bad wolf position boss") : AttackAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanTheCuratorPositionBossAction : public MovementAction +class BigBadWolfRunAwayFromBossAction : public MovementAction { public: - KarazhanTheCuratorPositionBossAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator position boss") : MovementAction(botAI, name) {} - + BigBadWolfRunAwayFromBossAction( + PlayerbotAI* botAI, std::string const name = "big bad wolf run away from boss") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanTheCuratorSpreadRangedAction : public MovementAction +class RomuloAndJulianneMarkTargetAction : public Action { public: - KarazhanTheCuratorSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan the curator spread ranged") : MovementAction(botAI, name) {} + RomuloAndJulianneMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "romulo and julianne mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class WizardOfOzMarkTargetAction : public Action +{ +public: + WizardOfOzMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "wizard of oz mark target") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanTerestianIllhoofMarkTargetAction : public Action +class WizardOfOzScorchStrawmanAction : public Action { public: - KarazhanTerestianIllhoofMarkTargetAction(PlayerbotAI* botAI, std::string const name = "karazhan terestian illhoof mark target") : Action(botAI, name) {} + WizardOfOzScorchStrawmanAction( + PlayerbotAI* botAI, std::string const name = "wizard of oz scorch strawman") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class TheCuratorMarkAstralFlareAction : public Action +{ +public: + TheCuratorMarkAstralFlareAction( + PlayerbotAI* botAI, std::string const name = "the curator mark astral flare") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranArcaneExplosionRunAwayAction : public MovementAction +class TheCuratorPositionBossAction : public AttackAction { public: - KarazhanShadeOfAranArcaneExplosionRunAwayAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran arcane explosion run away") : MovementAction(botAI, name) {} + TheCuratorPositionBossAction( + PlayerbotAI* botAI, std::string const name = "the curator position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; +class TheCuratorSpreadRangedAction : public MovementAction +{ +public: + TheCuratorSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "the curator spread ranged") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanShadeOfAranFlameWreathStopMovementAction : public MovementAction +class TerestianIllhoofMarkTargetAction : public Action { public: - KarazhanShadeOfAranFlameWreathStopMovementAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran flame wreath stop bot") : MovementAction(botAI, name) {} + TerestianIllhoofMarkTargetAction( + PlayerbotAI* botAI, std::string const name = "terestian illhoof mark target") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class ShadeOfAranRunAwayFromArcaneExplosionAction : public MovementAction +{ +public: + ShadeOfAranRunAwayFromArcaneExplosionAction( + PlayerbotAI* botAI, std::string const name = "shade of aran run away from arcane explosion") : MovementAction(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranMarkConjuredElementalAction : public Action +class ShadeOfAranStopMovingDuringFlameWreathAction : public MovementAction { public: - KarazhanShadeOfAranMarkConjuredElementalAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran mark conjured elemental") : Action(botAI, name) {} + ShadeOfAranStopMovingDuringFlameWreathAction( + PlayerbotAI* botAI, std::string const name = "shade of aran stop moving during flame wreath") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class ShadeOfAranMarkConjuredElementalAction : public Action +{ +public: + ShadeOfAranMarkConjuredElementalAction( + PlayerbotAI* botAI, std::string const name = "shade of aran mark conjured elemental") : Action(botAI, name) {} bool Execute(Event event) override; }; -class KarazhanShadeOfAranSpreadRangedAction : public MovementAction +class ShadeOfAranRangedMaintainDistanceAction : public MovementAction { public: - KarazhanShadeOfAranSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "karazhan shade of aran spread ranged") : MovementAction(botAI, name) {} + ShadeOfAranRangedMaintainDistanceAction( + PlayerbotAI* botAI, std::string const name = "shade of aran ranged maintain distance") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class NetherspiteBlockRedBeamAction : public MovementAction +{ +public: + NetherspiteBlockRedBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block red beam") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; + +private: + Position GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss); + std::unordered_map _wasBlockingRedBeam; }; -class KarazhanNetherspiteBlockRedBeamAction : public MovementAction +class NetherspiteBlockBlueBeamAction : public MovementAction { public: - KarazhanNetherspiteBlockRedBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block red beam") : MovementAction(botAI, name) {} + NetherspiteBlockBlueBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block blue beam") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +private: + std::unordered_map _wasBlockingBlueBeam; +}; + +class NetherspiteBlockGreenBeamAction : public MovementAction +{ +public: + NetherspiteBlockGreenBeamAction( + PlayerbotAI* botAI, std::string const name = "netherspite block green beam") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; + +private: + std::unordered_map _wasBlockingGreenBeam; }; -class KarazhanNetherspiteBlockBlueBeamAction : public MovementAction +class NetherspiteAvoidBeamAndVoidZoneAction : public MovementAction { public: - KarazhanNetherspiteBlockBlueBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block blue beam") : MovementAction(botAI, name) {} + NetherspiteAvoidBeamAndVoidZoneAction( + PlayerbotAI* botAI, std::string const name = "netherspite avoid beam and void zone") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + struct BeamAvoid + { + Unit* portal; + float minDist, maxDist; + }; + bool IsAwayFromBeams(float x, float y, const std::vector& beams, Unit* netherspite); +}; +class NetherspiteBanishPhaseAvoidVoidZoneAction : public MovementAction +{ +public: + NetherspiteBanishPhaseAvoidVoidZoneAction( + PlayerbotAI* botAI, std::string const name = "netherspite banish phase avoid void zone") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBlockGreenBeamAction : public MovementAction +class NetherspiteManageTimersAndTrackersAction : public Action { public: - KarazhanNetherspiteBlockGreenBeamAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite block green beam") : MovementAction(botAI, name) {} + NetherspiteManageTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "netherspite manage timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class PrinceMalchezaarEnfeebledAvoidHazardAction : public MovementAction +{ +public: + PrinceMalchezaarEnfeebledAvoidHazardAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar enfeebled avoid hazard") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteAvoidBeamAndVoidZoneAction : public MovementAction +class PrinceMalchezaarNonTankAvoidInfernalAction : public MovementAction { public: - KarazhanNetherspiteAvoidBeamAndVoidZoneAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite avoid beam and void zone") : MovementAction(botAI, name) {} + PrinceMalchezaarNonTankAvoidInfernalAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar non tank avoid infernal") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class PrinceMalchezaarMainTankMovementAction : public AttackAction +{ +public: + PrinceMalchezaarMainTankMovementAction( + PlayerbotAI* botAI, std::string const name = "prince malchezaar main tank movement") : AttackAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction : public MovementAction +class NightbaneGroundPhasePositionBossAction : public AttackAction { public: - KarazhanNetherspiteBanishPhaseAvoidVoidZoneAction(PlayerbotAI* botAI, std::string const name = "karazhan netherspite banish phase avoid void zone") : MovementAction(botAI, name) {} + NightbaneGroundPhasePositionBossAction( + PlayerbotAI* botAI, std::string const name = "nightbane ground phase position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; +class NightbaneGroundPhaseRotateRangedPositionsAction : public MovementAction +{ +public: + NightbaneGroundPhaseRotateRangedPositionsAction( + PlayerbotAI* botAI, std::string const name = "nightbane ground phase rotate ranged positions") : MovementAction(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanPrinceMalchezaarNonTankAvoidHazardAction : public MovementAction +class NightbaneCastFearWardOnMainTankAction : public Action { public: - KarazhanPrinceMalchezaarNonTankAvoidHazardAction(PlayerbotAI* botAI, std::string const name = "karazhan prince malchezaar non-tank avoid hazard") : MovementAction(botAI, name) {} + NightbaneCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "nightbane cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; +class NightbaneControlPetAggressionAction : public Action +{ +public: + NightbaneControlPetAggressionAction( + PlayerbotAI* botAI, std::string const name = "nightbane control pet aggression") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; -class KarazhanPrinceMalchezaarTankAvoidHazardAction : public MovementAction +class NightbaneFlightPhaseMovementAction : public MovementAction { public: - KarazhanPrinceMalchezaarTankAvoidHazardAction(PlayerbotAI* botAI, std::string const name = "karazhan prince malchezaar tank avoid hazard") : MovementAction(botAI, name) {} + NightbaneFlightPhaseMovementAction( + PlayerbotAI* botAI, std::string const name = "nightbane flight phase movement") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +class NightbaneManageTimersAndTrackersAction : public Action +{ +public: + NightbaneManageTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "nightbane manage timers and trackers") : Action(botAI, name) {} bool Execute(Event event) override; - bool isUseful() override; }; #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp b/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp index 296341746f..9ca9f7b300 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanHelpers.cpp @@ -1,316 +1,356 @@ -#include -#include - #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" -#include "AiObjectContext.h" -#include "PlayerbotMgr.h" -#include "Position.h" -#include "Spell.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" -const Position KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION = Position(-10945.881f, -2103.782f, 92.712f); -const Position KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[8] = -{ - { -10931.178f, -2116.580f, 92.179f }, - { -10925.828f, -2102.425f, 92.180f }, - { -10933.089f, -2088.5017f, 92.180f }, - { -10947.59f, -2082.8147f, 92.180f }, - { -10960.912f, -2090.4368f, 92.179f }, - { -10966.017f, -2105.288f, 92.175f }, - { -10959.242f, -2119.6172f, 92.180f }, - { -10944.495f, -2123.857f, 92.180f }, -}; - -const Position KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION = Position(-10913.391f, -1773.508f, 90.477f); -const Position KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[4] = +namespace KarazhanHelpers { - { -10875.456f, -1779.036f, 90.477f }, - { -10872.281f, -1751.638f, 90.477f }, - { -10910.492f, -1747.401f, 90.477f }, - { -10913.391f, -1773.508f, 90.477f }, -}; + // Attumen the Huntsman + std::unordered_map attumenDpsWaitTimer; + // Big Bad Wolf + std::unordered_map bigBadWolfRunIndex; + // Netherspite + std::unordered_map netherspiteDpsWaitTimer; + std::unordered_map redBeamMoveTimer; + std::unordered_map lastBeamMoveSideways; + // Nightbane + std::unordered_map nightbaneDpsWaitTimer; + std::unordered_map nightbaneTankStep; + std::unordered_map nightbaneRangedStep; + std::unordered_map nightbaneFlightPhaseStartTimer; + std::unordered_map nightbaneRainOfBonesHit; + + const Position MAIDEN_OF_VIRTUE_BOSS_POSITION = { -10945.881f, -2103.782f, 92.712f }; + const Position MAIDEN_OF_VIRTUE_RANGED_POSITION[8] = + { + { -10931.178f, -2116.580f, 92.179f }, + { -10925.828f, -2102.425f, 92.180f }, + { -10933.089f, -2088.502f, 92.180f }, + { -10947.590f, -2082.815f, 92.180f }, + { -10960.912f, -2090.437f, 92.179f }, + { -10966.017f, -2105.288f, 92.175f }, + { -10959.242f, -2119.617f, 92.180f }, + { -10944.495f, -2123.857f, 92.180f }, + }; + + const Position BIG_BAD_WOLF_BOSS_POSITION = { -10913.391f, -1773.508f, 90.477f }; + const Position BIG_BAD_WOLF_RUN_POSITION[4] = + { + { -10875.456f, -1779.036f, 90.477f }, + { -10872.281f, -1751.638f, 90.477f }, + { -10910.492f, -1747.401f, 90.477f }, + { -10913.391f, -1773.508f, 90.477f }, + }; + + const Position THE_CURATOR_BOSS_POSITION = { -11139.463f, -1884.645f, 165.765f }; + + const Position NIGHTBANE_TRANSITION_BOSS_POSITION = { -11160.646f, -1932.773f, 91.473f }; // near some ribs + const Position NIGHTBANE_FINAL_BOSS_POSITION = { -11173.530f, -1940.707f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION1 = { -11145.949f, -1970.927f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION2 = { -11143.594f, -1954.981f, 91.473f }; + const Position NIGHTBANE_RANGED_POSITION3 = { -11159.778f, -1961.031f, 91.473f }; + const Position NIGHTBANE_FLIGHT_STACK_POSITION = { -11159.555f, -1893.526f, 91.473f }; // Broken Barrel + const Position NIGHTBANE_RAIN_OF_BONES_POSITION = { -11165.233f, -1911.123f, 91.473f }; + + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) + { + if (!target) + return; -const Position KARAZHAN_THE_CURATOR_BOSS_POSITION = Position(-11139.463f, -1884.645f, 165.765f); + if (Group* group = bot->GetGroup()) + { + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } + } -void RaidKarazhanHelpers::MarkTargetWithSkull(Unit* target) -{ - if (!target) + void MarkTargetWithSkull(Player* bot, Unit* target) { - return; + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); } - if (Group* group = bot->GetGroup()) + void MarkTargetWithSquare(Player* bot, Unit* target) { - constexpr uint8_t skullIconId = 7; - ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); - - if (skullGuid != target->GetGUID()) - { - group->SetTargetIcon(skullIconId, bot->GetGUID(), target->GetGUID()); - } + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); } -} -Unit* RaidKarazhanHelpers::GetFirstAliveUnit(const std::vector& units) -{ - for (Unit* unit : units) + void MarkTargetWithStar(Player* bot, Unit* target) { - if (unit && unit->IsAlive()) - { - return unit; - } + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); } - return nullptr; -} + void MarkTargetWithCircle(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); + } -Unit* RaidKarazhanHelpers::GetFirstAliveUnitByEntry(uint32 entry) -{ - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + void MarkTargetWithMoon(Player* bot, Unit* target) + { + MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); + } - for (auto const& npcGuid : npcs) + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) { - Unit* unit = botAI->GetUnit(npcGuid); + if (!target) + return; - if (unit && unit->IsAlive() && unit->GetEntry() == entry) + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) { - return unit; + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); } } - return nullptr; -} - -Unit* RaidKarazhanHelpers::GetNearestPlayerInRadius(float radius) -{ - if (Group* group = bot->GetGroup()) + // Only one bot is needed to set/reset mapwide timers + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - - if (!member || !member->IsAlive() || member == bot) - { - continue; - } - - if (bot->GetExactDist2d(member) < radius) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - return member; + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) + return member == bot; } } + + return false; } - return nullptr; -} + Unit* GetFirstAliveUnit(const std::vector& units) + { + for (Unit* unit : units) + { + if (unit && unit->IsAlive()) + return unit; + } -bool RaidKarazhanHelpers::IsFlameWreathActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - Spell* currentSpell = boss ? boss->GetCurrentSpell(CURRENT_GENERIC_SPELL) : nullptr; - if (currentSpell && currentSpell->m_spellInfo && currentSpell->m_spellInfo->Id == SPELL_FLAME_WREATH) + return nullptr; + } + + Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) { - return true; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + return unit; + } + + return nullptr; } - if (Group* group = bot->GetGroup()) + Unit* GetNearestPlayerInRadius(Player* bot, float radius) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) - { - continue; - } - if (member->HasAura(SPELL_AURA_FLAME_WREATH)) + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) { - return true; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } } } - } - return false; -} + return nearestPlayer; + } -// Red beam blockers: tank bots, no Nether Exhaustion Red -std::vector RaidKarazhanHelpers::GetRedBlockers() -{ - std::vector redBlockers; - if (Group* group = bot->GetGroup()) + bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + Unit* aran = botAI->GetAiObjectContext()->GetValue("find target", "shade of aran")->Get(); + Spell* currentSpell = aran ? aran->GetCurrentSpell(CURRENT_GENERIC_SPELL) : nullptr; + + if (currentSpell && currentSpell->m_spellInfo && + currentSpell->m_spellInfo->Id == SPELL_FLAME_WREATH_CAST) + return true; + + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsTank(member) || !GET_PLAYERBOT_AI(member) || - member->HasAura(SPELL_NETHER_EXHAUSTION_RED)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_FLAME_WREATH_AURA)) + return true; } - redBlockers.push_back(member); } - } - return redBlockers; -} + return false; + } -// Blue beam blockers: non-Rogue/Warrior DPS bots, no Nether Exhaustion Blue and ≤25 stacks of Blue Beam debuff -std::vector RaidKarazhanHelpers::GetBlueBlockers() -{ - std::vector blueBlockers; - if (Group* group = bot->GetGroup()) + // Red beam blockers: tank bots, no Nether Exhaustion Red + std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + std::vector redBlockers; + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsTank(member) || !GET_PLAYERBOT_AI(member) || + member->HasAura(SPELL_NETHER_EXHAUSTION_RED)) + continue; + + redBlockers.push_back(member); } - bool isDps = botAI->IsDps(member); - bool isWarrior = member->getClass() == CLASS_WARRIOR; - bool isRogue = member->getClass() == CLASS_ROGUE; - bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_BLUE); - Aura* blueBuff = member->GetAura(SPELL_BLUE_BEAM_DEBUFF); - bool overStack = blueBuff && blueBuff->GetStackAmount() >= 26; - if (isDps && !isWarrior && !isRogue && !hasExhaustion && !overStack) + } + + return redBlockers; + } + + // Blue beam blockers: non-Rogue/Warrior DPS bots, no Nether Exhaustion Blue and <24 stacks of Blue Beam debuff + std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot) + { + std::vector blueBlockers; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - blueBlockers.push_back(member); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_BLUE); + Aura* blueBuff = member->GetAura(SPELL_BLUE_BEAM_DEBUFF); + bool overStack = blueBuff && blueBuff->GetStackAmount() >= 24; + + bool isDps = botAI->IsDps(member); + bool isWarrior = member->getClass() == CLASS_WARRIOR; + bool isRogue = member->getClass() == CLASS_ROGUE; + + if (isDps && !isWarrior && !isRogue && !hasExhaustion && !overStack) + blueBlockers.push_back(member); } } - } - return blueBlockers; -} + return blueBlockers; + } -// Green beam blockers: -// (1) Rogue and non-tank Warrior bots, no Nether Exhaustion Green -// (2) Healer bots, no Nether Exhaustion Green and ≤25 stacks of Green Beam debuff -std::vector RaidKarazhanHelpers::GetGreenBlockers() -{ - std::vector greenBlockers; - if (Group* group = bot->GetGroup()) + // Green beam blockers: + // (1) Prioritize Rogues and non-tank Warrior bots, no Nether Exhaustion Green + // (2) Then assign Healer bots, no Nether Exhaustion Green and <24 stacks of Green Beam debuff + std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot) { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + std::vector greenBlockers; + if (Group* group = bot->GetGroup()) { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); + bool isRogue = member->getClass() == CLASS_ROGUE; + bool isDpsWarrior = member->getClass() == CLASS_WARRIOR && botAI->IsDps(member); + bool eligibleRogueWarrior = (isRogue || isDpsWarrior) && !hasExhaustion; + + if (eligibleRogueWarrior) + greenBlockers.push_back(member); } - bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); - Aura* greenBuff = member->GetAura(SPELL_GREEN_BEAM_DEBUFF); - bool overStack = greenBuff && greenBuff->GetStackAmount() >= 26; - bool isRogue = member->getClass() == CLASS_ROGUE; - bool isDpsWarrior = member->getClass() == CLASS_WARRIOR && botAI->IsDps(member); - bool eligibleRogueWarrior = (isRogue || isDpsWarrior) && !hasExhaustion; - bool isHealer = botAI->IsHeal(member); - bool eligibleHealer = isHealer && !hasExhaustion && !overStack; - if (eligibleRogueWarrior || eligibleHealer) + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - greenBlockers.push_back(member); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + continue; + + bool hasExhaustion = member->HasAura(SPELL_NETHER_EXHAUSTION_GREEN); + Aura* greenBuff = member->GetAura(SPELL_GREEN_BEAM_DEBUFF); + bool overStack = greenBuff && greenBuff->GetStackAmount() >= 24; + bool isHealer = botAI->IsHeal(member); + bool eligibleHealer = isHealer && !hasExhaustion && !overStack; + + if (eligibleHealer) + greenBlockers.push_back(member); } } - } - - return greenBlockers; -} -Position RaidKarazhanHelpers::GetPositionOnBeam(Unit* boss, Unit* portal, float distanceFromBoss) -{ - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float bz = boss->GetPositionZ(); - float px = portal->GetPositionX(); - float py = portal->GetPositionY(); - - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - return Position(bx, by, bz); + return greenBlockers; } - dx /= length; - dy /= length; - float targetX = bx + dx * distanceFromBoss; - float targetY = by + dy * distanceFromBoss; - float targetZ = bz; + std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot) + { + static ObjectGuid currentRedBlocker; + static ObjectGuid currentGreenBlocker; + static ObjectGuid currentBlueBlocker; - return Position(targetX, targetY, targetZ); -} + Player* redBlocker = nullptr; + Player* greenBlocker = nullptr; + Player* blueBlocker = nullptr; -std::tuple RaidKarazhanHelpers::GetCurrentBeamBlockers() -{ - static ObjectGuid currentRedBlocker; - static ObjectGuid currentGreenBlocker; - static ObjectGuid currentBlueBlocker; + std::vector redBlockers = GetRedBlockers(botAI, bot); + if (!redBlockers.empty()) + { + auto it = std::find_if(redBlockers.begin(), redBlockers.end(), [](Player* player) + { + return player && player->GetGUID() == currentRedBlocker; + }); - Player* redBlocker = nullptr; - Player* greenBlocker = nullptr; - Player* blueBlocker = nullptr; + if (it != redBlockers.end()) + redBlocker = *it; + else + redBlocker = redBlockers.front(); - std::vector redBlockers = GetRedBlockers(); - if (!redBlockers.empty()) - { - auto it = std::find_if(redBlockers.begin(), redBlockers.end(), [](Player* p) - { - return p && p->GetGUID() == currentRedBlocker; - }); - if (it != redBlockers.end()) - { - redBlocker = *it; + currentRedBlocker = redBlocker ? redBlocker->GetGUID() : ObjectGuid::Empty; } else { - redBlocker = redBlockers.front(); + currentRedBlocker = ObjectGuid::Empty; + redBlocker = nullptr; } - currentRedBlocker = redBlocker ? redBlocker->GetGUID() : ObjectGuid::Empty; - } - else - { - currentRedBlocker = ObjectGuid::Empty; - redBlocker = nullptr; - } - std::vector greenBlockers = GetGreenBlockers(); - if (!greenBlockers.empty()) - { - auto it = std::find_if(greenBlockers.begin(), greenBlockers.end(), [](Player* p) - { - return p && p->GetGUID() == currentGreenBlocker; - }); - if (it != greenBlockers.end()) + std::vector greenBlockers = GetGreenBlockers(botAI, bot); + if (!greenBlockers.empty()) { - greenBlocker = *it; + auto it = std::find_if(greenBlockers.begin(), greenBlockers.end(), [](Player* player) + { + return player && player->GetGUID() == currentGreenBlocker; + }); + + if (it != greenBlockers.end()) + greenBlocker = *it; + else + greenBlocker = greenBlockers.front(); + + currentGreenBlocker = greenBlocker ? greenBlocker->GetGUID() : ObjectGuid::Empty; } else { - greenBlocker = greenBlockers.front(); + currentGreenBlocker = ObjectGuid::Empty; + greenBlocker = nullptr; } - currentGreenBlocker = greenBlocker ? greenBlocker->GetGUID() : ObjectGuid::Empty; - } - else - { - currentGreenBlocker = ObjectGuid::Empty; - greenBlocker = nullptr; - } - std::vector blueBlockers = GetBlueBlockers(); + std::vector blueBlockers = GetBlueBlockers(botAI, bot); if (!blueBlockers.empty()) { - auto it = std::find_if(blueBlockers.begin(), blueBlockers.end(), [](Player* p) + auto it = std::find_if(blueBlockers.begin(), blueBlockers.end(), [](Player* player) { - return p && p->GetGUID() == currentBlueBlocker; + return player && player->GetGUID() == currentBlueBlocker; }); + if (it != blueBlockers.end()) - { blueBlocker = *it; - } else - { blueBlocker = blueBlockers.front(); - } + currentBlueBlocker = blueBlocker ? blueBlocker->GetGUID() : ObjectGuid::Empty; } else @@ -319,91 +359,132 @@ std::tuple RaidKarazhanHelpers::GetCurrentBeamBlocker blueBlocker = nullptr; } - return std::make_tuple(redBlocker, greenBlocker, blueBlocker); -} + return std::make_tuple(redBlocker, greenBlocker, blueBlocker); + } -std::vector RaidKarazhanHelpers::GetAllVoidZones() -{ - std::vector voidZones; - const float radius = 30.0f; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + std::vector GetAllVoidZones(PlayerbotAI* botAI, Player* bot) { - Unit* unit = botAI->GetUnit(npcGuid); - if (!unit || unit->GetEntry() != NPC_VOID_ZONE) - { - continue; - } - float dist = bot->GetExactDist2d(unit); - if (dist < radius) + std::vector voidZones; + const float radius = 30.0f; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) { - voidZones.push_back(unit); + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || unit->GetEntry() != NPC_VOID_ZONE) + continue; + + float dist = bot->GetExactDist2d(unit); + if (dist < radius) + voidZones.push_back(unit); } - } - return voidZones; -} + return voidZones; + } -bool RaidKarazhanHelpers::IsSafePosition(float x, float y, float z, - const std::vector& hazards, float hazardRadius) -{ - for (Unit* hazard : hazards) + bool IsSafePosition(float x, float y, float z, const std::vector& hazards, float hazardRadius) { - float dist = std::sqrt(std::pow(x - hazard->GetPositionX(), 2) + std::pow(y - hazard->GetPositionY(), 2)); - if (dist < hazardRadius) + for (Unit* hazard : hazards) { - return false; + float dist = hazard->GetExactDist2d(x, y); + if (dist < hazardRadius) + return false; } - } - return true; -} + return true; + } -std::vector RaidKarazhanHelpers::GetSpawnedInfernals() const -{ - std::vector infernals; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + std::vector GetSpawnedInfernals(PlayerbotAI* botAI) { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == NPC_NETHERSPITE_INFERNAL) + std::vector infernals; + const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) { - infernals.push_back(unit); + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_NETHERSPITE_INFERNAL) + infernals.push_back(unit); } - } - return infernals; -} + return infernals; + } -bool RaidKarazhanHelpers::IsStraightPathSafe(const Position& start, const Position& target, const std::vector& hazards, float hazardRadius, float stepSize) -{ - float sx = start.GetPositionX(); - float sy = start.GetPositionY(); - float sz = start.GetPositionZ(); - float tx = target.GetPositionX(); - float ty = target.GetPositionY(); - float tz = target.GetPositionZ(); - float totalDist = std::sqrt(std::pow(tx - sx, 2) + std::pow(ty - sy, 2)); - if (totalDist == 0.0f) + bool IsStraightPathSafe(const Position& start, const Position& target, const std::vector& hazards, + float hazardRadius, float stepSize) { + float sx = start.GetPositionX(); + float sy = start.GetPositionY(); + float sz = start.GetPositionZ(); + float tx = target.GetPositionX(); + float ty = target.GetPositionY(); + float tz = target.GetPositionZ(); + + const float totalDist = start.GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + if (totalDist == 0.0f) + return true; + + for (float checkDist = 0.0f; checkDist <= totalDist; checkDist += stepSize) + { + float t = checkDist / totalDist; + float checkX = sx + (tx - sx) * t; + float checkY = sy + (ty - sy) * t; + float checkZ = sz + (tz - sz) * t; + for (Unit* hazard : hazards) + { + const float hx = checkX - hazard->GetPositionX(); + const float hy = checkY - hazard->GetPositionY(); + if ((hx*hx + hy*hy) < hazardRadius * hazardRadius) + return false; + } + } + return true; } - for (float checkDist = 0.0f; checkDist <= totalDist; checkDist += stepSize) + bool TryFindSafePositionWithSafePath( + Player* bot, float originX, float originY, float originZ, float centerX, float centerY, float centerZ, + const std::vector& hazards, float safeDistance, float stepSize, uint8 numAngles, + float maxSampleDist, bool requireSafePath, float& bestDestX, float& bestDestY, float& bestDestZ) { - float t = checkDist / totalDist; - float checkX = sx + (tx - sx) * t; - float checkY = sy + (ty - sy) * t; - float checkZ = sz + (tz - sz) * t; - for (Unit* hazard : hazards) + float bestMoveDist = std::numeric_limits::max(); + bool found = false; + + for (int i = 0; i < numAngles; ++i) { - float hazardDist = std::sqrt(std::pow(checkX - hazard->GetPositionX(), 2) + std::pow(checkY - hazard->GetPositionY(), 2)); - if (hazardDist < hazardRadius) + float angle = (2.0f * M_PI * i) / numAngles; + float dx = cos(angle); + float dy = sin(angle); + + for (float dist = stepSize; dist <= maxSampleDist; dist += stepSize) { - return false; + float x = centerX + dx * dist; + float y = centerY + dy * dist; + float z = centerZ; + float destX = x, destY = y, destZ = z; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, centerX, centerY, centerZ, + destX, destY, destZ, true)) + continue; + + if (!IsSafePosition(destX, destY, destZ, hazards, safeDistance)) + continue; + + if (requireSafePath) + { + if (!IsStraightPathSafe(Position(originX, originY, originZ), Position(destX, destY, destZ), + hazards, safeDistance, stepSize)) + continue; + } + + const float moveDist = Position(originX, originY, originZ).GetExactDist2d(destX, destY); + if (moveDist < bestMoveDist) + { + bestMoveDist = moveDist; + bestDestX = destX; + bestDestY = destY; + bestDestZ = destZ; + found = true; + } } } - } - return true; + return found; + } } diff --git a/src/strategy/raids/karazhan/RaidKarazhanHelpers.h b/src/strategy/raids/karazhan/RaidKarazhanHelpers.h index d26d8501d1..fb08333a25 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanHelpers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanHelpers.h @@ -1,85 +1,136 @@ #ifndef _PLAYERBOT_RAIDKARAZHANHELPERS_H_ #define _PLAYERBOT_RAIDKARAZHANHELPERS_H_ +#include +#include + #include "AiObject.h" -#include "Playerbots.h" #include "Position.h" +#include "Unit.h" -enum KarazhanSpells +namespace KarazhanHelpers { - // Maiden of Virtue - SPELL_REPENTANCE = 29511, + enum KarazhanSpells + { + // Maiden of Virtue + SPELL_REPENTANCE = 29511, - // Opera Event - SPELL_LITTLE_RED_RIDING_HOOD = 30756, + // Opera Event + SPELL_LITTLE_RED_RIDING_HOOD = 30756, - // Shade of Aran - SPELL_FLAME_WREATH = 30004, - SPELL_AURA_FLAME_WREATH = 29946, - SPELL_ARCANE_EXPLOSION = 29973, - SPELL_WARLOCK_BANISH = 18647, // Rank 2 + // The Curator + SPELL_CURATOR_EVOCATION = 30254, - // Netherspite - SPELL_GREEN_BEAM_DEBUFF = 30422, - SPELL_BLUE_BEAM_DEBUFF = 30423, - SPELL_NETHER_EXHAUSTION_RED = 38637, - SPELL_NETHER_EXHAUSTION_GREEN = 38638, - SPELL_NETHER_EXHAUSTION_BLUE = 38639, - SPELL_NETHERSPITE_BANISHED = 39833, - - // Prince Malchezaar - SPELL_ENFEEBLE = 30843, -}; - -enum KarazhanNpcs -{ - // Attumen the Huntsman - NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Shade of Aran + SPELL_FLAME_WREATH_CAST = 30004, + SPELL_FLAME_WREATH_AURA = 29946, + SPELL_ARCANE_EXPLOSION = 29973, + + // Netherspite + SPELL_RED_BEAM_DEBUFF = 30421, // "Nether Portal - Perseverance" (player aura) + SPELL_GREEN_BEAM_DEBUFF = 30422, // "Nether Portal - Serenity" (player aura) + SPELL_BLUE_BEAM_DEBUFF = 30423, // "Nether Portal - Dominance" (player aura) + SPELL_GREEN_BEAM_HEAL = 30467, // "Nether Portal - Serenity" (Netherspite aura) + SPELL_NETHER_EXHAUSTION_RED = 38637, + SPELL_NETHER_EXHAUSTION_GREEN = 38638, + SPELL_NETHER_EXHAUSTION_BLUE = 38639, + SPELL_NETHERSPITE_BANISHED = 39833, // "Vortex Shade Black" + + // Prince Malchezaar + SPELL_ENFEEBLE = 30843, + + // Nightbane + SPELL_CHARRED_EARTH = 30129, + SPELL_BELLOWING_ROAR = 36922, + SPELL_RAIN_OF_BONES = 37091, + + // Warlock + SPELL_WARLOCK_BANISH = 18647, + + // Priest + SPELL_FEAR_WARD = 6346, + }; - // Terestian Illhoof - NPC_KILREK = 17229, - NPC_DEMON_CHAINS = 17248, + enum KarazhanNPCs + { + // Trash + NPC_SPECTRAL_RETAINER = 16410, + NPC_MANA_WARP = 16530, - // Shade of Aran - NPC_CONJURED_ELEMENTAL = 17167, + // Attumen the Huntsman + NPC_ATTUMEN_THE_HUNTSMAN = 15550, + NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Shade of Aran + NPC_CONJURED_ELEMENTAL = 17167, + + // Netherspite + NPC_VOID_ZONE = 16697, + NPC_GREEN_PORTAL = 17367, // "Nether Portal - Serenity " + NPC_BLUE_PORTAL = 17368, // "Nether Portal - Dominance " + NPC_RED_PORTAL = 17369, // "Nether Portal - Perseverance " + + // Prince Malchezaar + NPC_NETHERSPITE_INFERNAL = 17646, + }; + + const uint32 KARAZHAN_MAP_ID = 532; + const float NIGHTBANE_FLIGHT_Z = 95.0f; + + // Attumen the Huntsman + extern std::unordered_map attumenDpsWaitTimer; + // Big Bad Wolf + extern std::unordered_map bigBadWolfRunIndex; // Netherspite - NPC_VOID_ZONE = 16697, - NPC_RED_PORTAL = 17369, - NPC_BLUE_PORTAL = 17368, - NPC_GREEN_PORTAL = 17367, - - // Prince Malchezaar - NPC_NETHERSPITE_INFERNAL = 17646, -}; - -extern const Position KARAZHAN_MAIDEN_OF_VIRTUE_BOSS_POSITION; -extern const Position KARAZHAN_MAIDEN_OF_VIRTUE_RANGED_POSITION[8]; -extern const Position KARAZHAN_BIG_BAD_WOLF_BOSS_POSITION; -extern const Position KARAZHAN_BIG_BAD_WOLF_RUN_POSITION[4]; -extern const Position KARAZHAN_THE_CURATOR_BOSS_POSITION; - -class RaidKarazhanHelpers : public AiObject -{ -public: - explicit RaidKarazhanHelpers(PlayerbotAI* botAI) : AiObject(botAI) {} - - void MarkTargetWithSkull(Unit* /*target*/); - Unit* GetFirstAliveUnit(const std::vector& /*units*/); - Unit* GetFirstAliveUnitByEntry(uint32 /*entry*/); - Unit* GetNearestPlayerInRadius(float /*radius*/ = 5.0f); - bool IsFlameWreathActive(); - Position GetPositionOnBeam(Unit* boss, Unit* portal, float distanceFromBoss); - std::vector GetRedBlockers(); - std::vector GetBlueBlockers(); - std::vector GetGreenBlockers(); - std::tuple GetCurrentBeamBlockers(); - std::vector GetAllVoidZones(); - bool IsSafePosition (float x, float y, float z, - const std::vector& hazards, float hazardRadius); - std::vector GetSpawnedInfernals() const; - bool IsStraightPathSafe(const Position& start, const Position& target, - const std::vector& hazards, float hazardRadius, float stepSize); -}; + extern std::unordered_map netherspiteDpsWaitTimer; + extern std::unordered_map redBeamMoveTimer; + extern std::unordered_map lastBeamMoveSideways; + // Nightbane + extern std::unordered_map nightbaneDpsWaitTimer; + extern std::unordered_map nightbaneTankStep; + extern std::unordered_map nightbaneRangedStep; + extern std::unordered_map nightbaneFlightPhaseStartTimer; + extern std::unordered_map nightbaneRainOfBonesHit; + + extern const Position MAIDEN_OF_VIRTUE_BOSS_POSITION; + extern const Position MAIDEN_OF_VIRTUE_RANGED_POSITION[8]; + extern const Position BIG_BAD_WOLF_BOSS_POSITION; + extern const Position BIG_BAD_WOLF_RUN_POSITION[4]; + extern const Position THE_CURATOR_BOSS_POSITION; + extern const Position NIGHTBANE_TRANSITION_BOSS_POSITION; + extern const Position NIGHTBANE_FINAL_BOSS_POSITION; + extern const Position NIGHTBANE_RANGED_POSITION1; + extern const Position NIGHTBANE_RANGED_POSITION2; + extern const Position NIGHTBANE_RANGED_POSITION3; + extern const Position NIGHTBANE_FLIGHT_STACK_POSITION; + extern const Position NIGHTBANE_RAIN_OF_BONES_POSITION; + + void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); + void MarkTargetWithSkull(Player* bot, Unit* target); + void MarkTargetWithSquare(Player* bot, Unit* target); + void MarkTargetWithStar(Player* bot, Unit* target); + void MarkTargetWithCircle(Player* bot, Unit* target); + void MarkTargetWithMoon(Player* bot, Unit* target); + void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); + bool IsMapIDTimerManager(PlayerbotAI* botAI, Player* bot); + Unit* GetFirstAliveUnit(const std::vector& units); + Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); + Unit* GetNearestPlayerInRadius(Player* bot, float radius); + bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot); + std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot); + std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot); + std::vector GetAllVoidZones(PlayerbotAI *botAI, Player* bot); + bool IsSafePosition (float x, float y, float z, const std::vector& hazards, float hazardRadius); + std::vector GetSpawnedInfernals(PlayerbotAI* botAI); + bool IsStraightPathSafe( + const Position& start, const Position& target, + const std::vector& hazards, float hazardRadius, float stepSize); + bool TryFindSafePositionWithSafePath( + Player* bot, float originX, float originY, float originZ, float centerX, float centerY, float centerZ, + const std::vector& hazards, float safeDistance, float stepSize, uint8 numAngles, + float maxSampleDist, bool requireSafePath, float& bestDestX, float& bestDestY, float& bestDestZ); +} #endif diff --git a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp index f68cbe8b57..cd64ea985b 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.cpp @@ -1,265 +1,359 @@ #include "RaidKarazhanMultipliers.h" #include "RaidKarazhanActions.h" #include "RaidKarazhanHelpers.h" -#include "AiObjectContext.h" #include "AttackAction.h" -#include "DruidBearActions.h" -#include "DruidCatActions.h" +#include "ChooseTargetActions.h" +#include "DruidActions.h" +#include "FollowActions.h" +#include "GenericActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "Playerbots.h" +#include "PriestActions.h" +#include "ReachTargetActions.h" #include "RogueActions.h" -#include "WarriorActions.h" +#include "ShamanActions.h" -static bool IsChargeAction(Action* action) +using namespace KarazhanHelpers; + +// Keep tanks from jumping back and forth between Attumen and Midnight +float AttumenTheHuntsmanDisableTankAssistMultiplier::GetValue(Action* action) { - return dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + if (!midnight) + return 1.0f; + + Unit* attumen = AI_VALUE2(Unit*, "find target", "attumen the huntsman"); + if (!attumen) + return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + return 1.0f; } -float KarazhanAttumenTheHuntsmanMultiplier::GetValue(Action* action) +// Try to get rid of jittering when bots are stacked behind Attumen +float AttumenTheHuntsmanStayStackedMultiplier::GetValue(Action* action) { - RaidKarazhanHelpers karazhanHelper(botAI); - Unit* boss = karazhanHelper.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); - if (boss && !(botAI->IsTank(bot) && botAI->HasAggro(boss) && boss->GetVictim() == bot) && - (dynamic_cast(action) && - !dynamic_cast(action))) + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) + return 1.0f; + + if (!botAI->IsMainTank(bot) && attumenMounted->GetVictim() != bot) { - return 0.0f; + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } return 1.0f; } -float KarazhanBigBadWolfMultiplier::GetValue(Action* action) +// Give the main tank 8 seconds to grab aggro when Attumen mounts Midnight +// In reality it's shorter because it takes Attumen a few seconds to aggro after mounting +float AttumenTheHuntsmanWaitForDpsMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); - if (!boss) - { + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!attumenMounted) return 1.0f; - } - if (bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 8; + + auto it = attumenDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == attumenDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) { - if ((dynamic_cast(action) && !dynamic_cast(action)) || - (dynamic_cast(action))) + if (!botAI->IsMainTank(bot)) { - return 0.0f; + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } return 1.0f; } -float KarazhanShadeOfAranMultiplier::GetValue(Action* action) +// The assist tank should stay on the boss to be 2nd on aggro and tank Hateful Bolts +float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); - if (!boss) - { + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Save Bloodlust/Heroism for Evocation (100% increased damage) +float TheCuratorDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + if (!curator) + return 1.0f; + + if (!curator->HasAura(SPELL_CURATOR_EVOCATION)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } - if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) + return 1.0f; +} + +// Don't charge back in when running from Arcane Explosion +float ShadeOfAranArcaneExplosionDisableChargeMultiplier::GetValue(Action* action) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return 1.0f; + + if (aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) { - if (IsChargeAction(action)) - { + if (dynamic_cast(action)) return 0.0f; - } - if (dynamic_cast(action)) + if (bot->GetDistance2d(aran) >= 20.0f) { - const float safeDistance = 20.0f; - if (bot->GetDistance2d(boss) >= safeDistance) - { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } } } - bool flameWreathActive = boss->HasAura(SPELL_FLAME_WREATH); - if (!flameWreathActive && bot->GetGroup()) - { - for (GroupReference* itr = bot->GetGroup()->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->HasAura(SPELL_AURA_FLAME_WREATH)) - { - flameWreathActive = true; - break; - } - } - } - if (flameWreathActive) + return 1.0f; +} + +// I will not move when Flame Wreath is cast or the raid blows up +float ShadeOfAranFlameWreathDisableMovementMultiplier::GetValue(Action* action) +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!aran) + return 1.0f; + + if (IsFlameWreathActive(botAI, bot)) { - if (dynamic_cast(action) || IsChargeAction(action)) - { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } } return 1.0f; } -float KarazhanNetherspiteBlueAndGreenBeamMultiplier::GetValue(Action* action) +// Try to rid of the jittering when blocking beams +float NetherspiteKeepBlockingBeamMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->IsAlive()) - { + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) return 1.0f; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + + if (bot == redBlocker) + { + if (dynamic_cast(action)) + return 0.0f; } - if (dynamic_cast(action) || dynamic_cast(action)) + if (bot == blueBlocker) { - return 0.0f; + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; } - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker /*unused*/, greenBlocker, blueBlocker] = karazhanHelper.GetCurrentBeamBlockers(); - bool isBlocker = (bot == greenBlocker || bot == blueBlocker); - if (isBlocker) + if (bot == greenBlocker) { - Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); - Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); - bool inBeam = false; - for (Unit* portal : {bluePortal, greenPortal}) - { - if (!portal) - { - continue; - } - float bx = boss->GetPositionX(), by = boss->GetPositionY(); - float px = portal->GetPositionX(), py = portal->GetPositionY(); - float dx = px - bx, dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length == 0.0f) - { - continue; - } - dx /= length; dy /= length; - float botdx = bot->GetPositionX() - bx, botdy = bot->GetPositionY() - by; - float t = (botdx * dx + botdy * dy); - float beamX = bx + dx * t, beamY = by + dy * t; - float distToBeam = sqrt(pow(bot->GetPositionX() - beamX, 2) + pow(bot->GetPositionY() - beamY, 2)); - if (distToBeam < 0.3f && t > 0.0f && t < length) - { - inBeam = true; - break; - } - } - if (inBeam) + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Give tanks 5 seconds to get aggro during phase transitions +float NetherspiteWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 5; + + auto it = netherspiteDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == netherspiteDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + if (!botAI->IsTank(bot)) { - std::vector voidZones = karazhanHelper.GetAllVoidZones(); - bool inVoidZone = false; - for (Unit* vz : voidZones) - { - if (bot->GetExactDist2d(vz) < 4.0f) - { - inVoidZone = true; - break; - } - } - if (!inVoidZone) - { - if (dynamic_cast(action) || IsChargeAction(action)) - { - return 0.0f; - } - } + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } + return 1.0f; +} + +// Disable standard "avoid aoe" strategy, which may interfere with scripted avoidance +float PrinceMalchezaarDisableAvoidAoeMultiplier::GetValue(Action* action) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + return 1.0f; } -float KarazhanNetherspiteRedBeamMultiplier::GetValue(Action* action) +// Don't run back into Shadow Nova when Enfeebled +float PrinceMalchezaarEnfeebleKeepDistanceMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); - if (!boss || !boss->IsAlive()) - { + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) return 1.0f; + + if (bot->HasAura(SPELL_ENFEEBLE)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; } - if (dynamic_cast(action)) + return 1.0f; +} + +// Wait until Phase 3 to use Bloodlust/Heroism +float PrinceMalchezaarDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + if (!malchezaar) + return 1.0f; + + if (malchezaar->GetHealthPct() > 30.0f) { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Pets tend to run out of bounds and cause skeletons to spawn off the map +// Pets also tend to pull adds from inside of the tower through the floor +// This multiplier DOES NOT impact Hunter and Warlock pets +// Hunter and Warlock pets are addressed in ControlPetAggressionAction +float NightbaneDisablePetsMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; + + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + { + if (dynamic_cast(action)) + return 0.0f; } - RaidKarazhanHelpers karazhanHelper(botAI); - auto [redBlocker, greenBlocker /*unused*/, blueBlocker /*unused*/] = karazhanHelper.GetCurrentBeamBlockers(); - static std::map beamMoveTimes; - static std::map lastBeamMoveSideways; - ObjectGuid botGuid = bot->GetGUID(); - Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); - if (bot == redBlocker && boss && redPortal) + return 1.0f; +} + +// Give the main tank 8 seconds to get aggro during phase transitions +float NightbaneWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint8 dpsWaitSeconds = 8; + + auto it = nightbaneDpsWaitTimer.find(KARAZHAN_MAP_ID); + if (it == nightbaneDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) { - Position blockingPos = karazhanHelper.GetPositionOnBeam(boss, redPortal, 18.0f); - float bx = boss->GetPositionX(); - float by = boss->GetPositionY(); - float px = redPortal->GetPositionX(); - float py = redPortal->GetPositionY(); - float dx = px - bx; - float dy = py - by; - float length = sqrt(dx*dx + dy*dy); - if (length != 0.0f) + if (!botAI->IsMainTank(bot)) { - dx /= length; - dy /= length; - float perpDx = -dy; - float perpDy = dx; - Position sidewaysPos(blockingPos.GetPositionX() + perpDx * 3.0f, - blockingPos.GetPositionY() + perpDy * 3.0f, - blockingPos.GetPositionZ()); - - uint32 intervalSecs = 5; - if (beamMoveTimes[botGuid] == 0) - { - beamMoveTimes[botGuid] = time(nullptr); - lastBeamMoveSideways[botGuid] = false; - } - if (time(nullptr) - beamMoveTimes[botGuid] >= intervalSecs) - { - lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid]; - beamMoveTimes[botGuid] = time(nullptr); - } - Position targetPos = lastBeamMoveSideways[botGuid] ? sidewaysPos : blockingPos; - float distToTarget = bot->GetExactDist2d(targetPos.GetPositionX(), targetPos.GetPositionY()); - const float positionTolerance = 1.5f; - if (distToTarget < positionTolerance) - { - if (dynamic_cast(action) || IsChargeAction(action)) - { - return 0.0f; - } - } + if (dynamic_cast(action) || (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } } return 1.0f; } -float KarazhanPrinceMalchezaarMultiplier::GetValue(Action* action) +// The "avoid aoe" strategy must be disabled for the main tank +// Otherwise, the main tank will spin Nightbane to avoid Charred Earth and wipe the raid +// It is also disabled for all bots during the flight phase +float NightbaneDisableAvoidAoeMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); - if (!boss || !boss->IsAlive()) - { + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) return 1.0f; - } - if (dynamic_cast(action)) + if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z || botAI->IsMainTank(bot)) { - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; } - if (botAI->IsMelee(bot) && bot->HasAura(SPELL_ENFEEBLE) && - !dynamic_cast(action)) - { + return 1.0f; +} + +// Disable some movement actions that conflict with the strategies +float NightbaneDisableMovementMultiplier::GetValue(Action* action) +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; - } - if (botAI->IsRanged(bot) && bot->HasAura(SPELL_ENFEEBLE) && - (dynamic_cast(action) && - !dynamic_cast(action))) + // Disable CombatFormationMoveAction for all bots except: + // (1) main tank and (2) only during the ground phase, other melee + if (botAI->IsRanged(bot) || + (botAI->IsMelee(bot) && !botAI->IsMainTank(bot) && + nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z)) { - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; } return 1.0f; diff --git a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h index c8a4ba3722..8ad5a1d4ec 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanMultipliers.h @@ -3,45 +3,131 @@ #include "Multiplier.h" -class KarazhanAttumenTheHuntsmanMultiplier : public Multiplier +class AttumenTheHuntsmanDisableTankAssistMultiplier : public Multiplier { public: - KarazhanAttumenTheHuntsmanMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan attumen the huntsman multiplier") {} + AttumenTheHuntsmanDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman disable tank assist multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanBigBadWolfMultiplier : public Multiplier +class AttumenTheHuntsmanStayStackedMultiplier : public Multiplier { public: - KarazhanBigBadWolfMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan big bad wolf multiplier") {} + AttumenTheHuntsmanStayStackedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman stay stacked multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanShadeOfAranMultiplier : public Multiplier +class AttumenTheHuntsmanWaitForDpsMultiplier : public Multiplier { public: - KarazhanShadeOfAranMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan shade of aran multiplier") {} + AttumenTheHuntsmanWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "attumen the huntsman wait for dps multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanNetherspiteBlueAndGreenBeamMultiplier : public Multiplier +class TheCuratorDisableTankAssistMultiplier : public Multiplier { public: - KarazhanNetherspiteBlueAndGreenBeamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan netherspite blue and green beam multiplier") {} + TheCuratorDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator disable tank assist multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanNetherspiteRedBeamMultiplier : public Multiplier +class TheCuratorDelayBloodlustAndHeroismMultiplier : public Multiplier { public: - KarazhanNetherspiteRedBeamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan netherspite red beam multiplier") {} + TheCuratorDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator delay bloodlust and heroism multiplier") {} virtual float GetValue(Action* action); }; -class KarazhanPrinceMalchezaarMultiplier : public Multiplier +class ShadeOfAranArcaneExplosionDisableChargeMultiplier : public Multiplier { public: - KarazhanPrinceMalchezaarMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "karazhan prince malchezaar multiplier") {} + ShadeOfAranArcaneExplosionDisableChargeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "shade of aran arcane explosion disable charge multiplier") {} + virtual float GetValue(Action* action); +}; + +class ShadeOfAranFlameWreathDisableMovementMultiplier : public Multiplier +{ +public: + ShadeOfAranFlameWreathDisableMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "shade of aran flame wreath disable movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class NetherspiteKeepBlockingBeamMultiplier : public Multiplier +{ +public: + NetherspiteKeepBlockingBeamMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "netherspite keep blocking beam multiplier") {} + virtual float GetValue(Action* action); +}; + +class NetherspiteWaitForDpsMultiplier : public Multiplier +{ +public: + NetherspiteWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "netherspite wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarDisableAvoidAoeMultiplier : public Multiplier +{ +public: + PrinceMalchezaarDisableAvoidAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar disable avoid aoe multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarEnfeebleKeepDistanceMultiplier : public Multiplier +{ +public: + PrinceMalchezaarEnfeebleKeepDistanceMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar enfeeble keep distance multiplier") {} + virtual float GetValue(Action* action); +}; + +class PrinceMalchezaarDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + PrinceMalchezaarDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "prince malchezaar delay bloodlust and heroism multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisablePetsMultiplier : public Multiplier +{ +public: + NightbaneDisablePetsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable pets multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneWaitForDpsMultiplier : public Multiplier +{ +public: + NightbaneWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisableAvoidAoeMultiplier : public Multiplier +{ +public: + NightbaneDisableAvoidAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable avoid aoe multiplier") {} + virtual float GetValue(Action* action); +}; + +class NightbaneDisableMovementMultiplier : public Multiplier +{ +public: + NightbaneDisableMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "nightbane disable movement multiplier") {} virtual float GetValue(Action* action); }; diff --git a/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp b/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp index f93c923c70..041651afd4 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanStrategy.cpp @@ -3,79 +3,160 @@ void RaidKarazhanStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "karazhan attumen the huntsman", NextAction::array(0, - new NextAction("karazhan attumen the huntsman stack behind", ACTION_RAID + 1), - nullptr))); + // Trash + triggers.push_back(new TriggerNode("mana warp is about to explode", + NextAction::array(0, new NextAction("mana warp stun creature before warp breach", ACTION_EMERGENCY + 6), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan moroes", NextAction::array(0, - new NextAction("karazhan moroes mark target", ACTION_RAID + 1), - nullptr))); + // Attumen the Huntsman + triggers.push_back(new TriggerNode("attumen the huntsman need target priority", + NextAction::array(0, new NextAction("attumen the huntsman mark target", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman attumen spawned", + NextAction::array(0, new NextAction("attumen the huntsman split bosses", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman attumen is mounted", + NextAction::array(0, new NextAction("attumen the huntsman stack behind", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("attumen the huntsman boss wipes aggro when mounting", + NextAction::array(0, new NextAction("attumen the huntsman manage dps timer", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan maiden of virtue", NextAction::array(0, - new NextAction("karazhan maiden of virtue position ranged", ACTION_RAID + 1), - new NextAction("karazhan maiden of virtue position boss", ACTION_RAID + 1), - nullptr))); + // Moroes + triggers.push_back(new TriggerNode("moroes boss engaged by main tank", + NextAction::array(0, new NextAction("moroes main tank attack boss", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("moroes need target priority", + NextAction::array(0, new NextAction("moroes mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan big bad wolf", NextAction::array(0, - new NextAction("karazhan big bad wolf run away", ACTION_EMERGENCY + 6), - new NextAction("karazhan big bad wolf position boss", ACTION_RAID + 1), - nullptr))); + // Maiden of Virtue + triggers.push_back(new TriggerNode("maiden of virtue healers are stunned by repentance", + NextAction::array(0, new NextAction("maiden of virtue move boss to healer", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("maiden of virtue holy wrath deals chain damage", + NextAction::array(0, new NextAction("maiden of virtue position ranged", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan romulo and julianne", NextAction::array(0, - new NextAction("karazhan romulo and julianne mark target", ACTION_RAID + 1), - nullptr))); + // The Big Bad Wolf + triggers.push_back(new TriggerNode("big bad wolf boss is chasing little red riding hood", + NextAction::array(0, new NextAction("big bad wolf run away from boss", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("big bad wolf boss engaged by tank", + NextAction::array(0, new NextAction("big bad wolf position boss", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan wizard of oz", NextAction::array(0, - new NextAction("karazhan wizard of oz scorch strawman", ACTION_RAID + 2), - new NextAction("karazhan wizard of oz mark target", ACTION_RAID + 1), - nullptr))); + // Romulo and Julianne + triggers.push_back(new TriggerNode("romulo and julianne both bosses revived", + NextAction::array(0, new NextAction("romulo and julianne mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan the curator", NextAction::array(0, - new NextAction("karazhan the curator spread ranged", ACTION_RAID + 2), - new NextAction("karazhan the curator position boss", ACTION_RAID + 2), - new NextAction("karazhan the curator mark target", ACTION_RAID + 1), - nullptr))); + // The Wizard of Oz + triggers.push_back(new TriggerNode("wizard of oz need target priority", + NextAction::array(0, new NextAction("wizard of oz mark target", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("wizard of oz strawman is vulnerable to fire", + NextAction::array(0, new NextAction("wizard of oz scorch strawman", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan terestian illhoof", NextAction::array(0, - new NextAction("karazhan terestian illhoof mark target", ACTION_RAID + 1), - nullptr))); + // The Curator + triggers.push_back(new TriggerNode("the curator astral flare spawned", + NextAction::array(0, new NextAction("the curator mark astral flare", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("the curator boss engaged by tanks", + NextAction::array(0, new NextAction("the curator position boss", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("the curator astral flares cast arcing sear", + NextAction::array(0, new NextAction("the curator spread ranged", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan shade of aran", NextAction::array(0, - new NextAction("karazhan shade of aran flame wreath stop movement", ACTION_EMERGENCY + 7), - new NextAction("karazhan shade of aran arcane explosion run away", ACTION_EMERGENCY + 6), - new NextAction("karazhan shade of aran spread ranged", ACTION_RAID + 2), - new NextAction("karazhan shade of aran mark conjured elemental", ACTION_RAID + 1), - nullptr))); + // Terestian Illhoof + triggers.push_back(new TriggerNode("terestian illhoof need target priority", + NextAction::array(0, new NextAction("terestian illhoof mark target", ACTION_RAID + 1), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan netherspite", NextAction::array(0, - new NextAction("karazhan netherspite block red beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite block blue beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite block green beam", ACTION_EMERGENCY + 8), - new NextAction("karazhan netherspite avoid beam and void zone", ACTION_EMERGENCY + 7), - new NextAction("karazhan netherspite banish phase avoid void zone", ACTION_RAID + 1), - nullptr))); + // Shade of Aran + triggers.push_back(new TriggerNode("shade of aran arcane explosion is casting", + NextAction::array(0, new NextAction("shade of aran run away from arcane explosion", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran flame wreath is active", + NextAction::array(0, new NextAction("shade of aran stop moving during flame wreath", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran conjured elementals summoned", + NextAction::array(0, new NextAction("shade of aran mark conjured elemental", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("shade of aran boss uses counterspell and blizzard", + NextAction::array(0, new NextAction("shade of aran ranged maintain distance", ACTION_RAID + 2), nullptr) + )); - triggers.push_back(new TriggerNode( - "karazhan prince malchezaar", NextAction::array(0, - new NextAction("karazhan prince malchezaar non tank avoid hazard", ACTION_EMERGENCY + 6), - new NextAction("karazhan prince malchezaar tank avoid hazard", ACTION_EMERGENCY + 6), - nullptr))); + // Netherspite + triggers.push_back(new TriggerNode("netherspite red beam is active", + NextAction::array(0, new NextAction("netherspite block red beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite blue beam is active", + NextAction::array(0, new NextAction("netherspite block blue beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite green beam is active", + NextAction::array(0, new NextAction("netherspite block green beam", ACTION_EMERGENCY + 8), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite bot is not beam blocker", + NextAction::array(0, new NextAction("netherspite avoid beam and void zone", ACTION_EMERGENCY + 7), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite boss is banished", + NextAction::array(0, new NextAction("netherspite banish phase avoid void zone", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("netherspite need to manage timers and trackers", + NextAction::array(0, new NextAction("netherspite manage timers and trackers", ACTION_EMERGENCY + 10), nullptr) + )); + + // Prince Malchezaar + triggers.push_back(new TriggerNode("prince malchezaar bot is enfeebled", + NextAction::array(0, new NextAction("prince malchezaar enfeebled avoid hazard", ACTION_EMERGENCY + 6), nullptr) + )); + triggers.push_back(new TriggerNode("prince malchezaar infernals are spawned", + NextAction::array(0, new NextAction("prince malchezaar non tank avoid infernal", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("prince malchezaar boss engaged by main tank", + NextAction::array(0, new NextAction("prince malchezaar main tank movement", ACTION_EMERGENCY + 6), nullptr) + )); + + // Nightbane + triggers.push_back(new TriggerNode("nightbane boss engaged by main tank", + NextAction::array(0, new NextAction("nightbane ground phase position boss", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane ranged bots are in charred earth", + NextAction::array(0, new NextAction("nightbane ground phase rotate ranged positions", ACTION_EMERGENCY + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane main tank is susceptible to fear", + NextAction::array(0, new NextAction("nightbane cast fear ward on main tank", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane pets ignore collision to chase flying boss", + NextAction::array(0, new NextAction("nightbane control pet aggression", ACTION_RAID + 2), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane boss is flying", + NextAction::array(0, new NextAction("nightbane flight phase movement", ACTION_RAID + 1), nullptr) + )); + triggers.push_back(new TriggerNode("nightbane need to manage timers and trackers", + NextAction::array(0, new NextAction("nightbane manage timers and trackers", ACTION_EMERGENCY + 10), nullptr) + )); } void RaidKarazhanStrategy::InitMultipliers(std::vector& multipliers) { - multipliers.push_back(new KarazhanShadeOfAranMultiplier(botAI)); - multipliers.push_back(new KarazhanNetherspiteBlueAndGreenBeamMultiplier(botAI)); - multipliers.push_back(new KarazhanNetherspiteRedBeamMultiplier(botAI)); - multipliers.push_back(new KarazhanPrinceMalchezaarMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanStayStackedMultiplier(botAI)); + multipliers.push_back(new AttumenTheHuntsmanWaitForDpsMultiplier(botAI)); + multipliers.push_back(new TheCuratorDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new TheCuratorDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new ShadeOfAranArcaneExplosionDisableChargeMultiplier(botAI)); + multipliers.push_back(new ShadeOfAranFlameWreathDisableMovementMultiplier(botAI)); + multipliers.push_back(new NetherspiteKeepBlockingBeamMultiplier(botAI)); + multipliers.push_back(new NetherspiteWaitForDpsMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarEnfeebleKeepDistanceMultiplier(botAI)); + multipliers.push_back(new PrinceMalchezaarDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new NightbaneDisablePetsMultiplier(botAI)); + multipliers.push_back(new NightbaneWaitForDpsMultiplier(botAI)); + multipliers.push_back(new NightbaneDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new NightbaneDisableMovementMultiplier(botAI)); } diff --git a/src/strategy/raids/karazhan/RaidKarazhanStrategy.h b/src/strategy/raids/karazhan/RaidKarazhanStrategy.h index c03e42856f..7d6b16deef 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanStrategy.h +++ b/src/strategy/raids/karazhan/RaidKarazhanStrategy.h @@ -7,7 +7,7 @@ class RaidKarazhanStrategy : public Strategy { public: - RaidKarazhanStrategy(PlayerbotAI* ai) : Strategy(ai) {} + RaidKarazhanStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} std::string const getName() override { return "karazhan"; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h b/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h index 0b6a29e828..fa2b7d37a5 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggerContext.h @@ -9,31 +9,255 @@ class RaidKarazhanTriggerContext : public NamedObjectContext public: RaidKarazhanTriggerContext() { - creators["karazhan attumen the huntsman"] = &RaidKarazhanTriggerContext::karazhan_attumen_the_huntsman; - creators["karazhan moroes"] = &RaidKarazhanTriggerContext::karazhan_moroes; - creators["karazhan maiden of virtue"] = &RaidKarazhanTriggerContext::karazhan_maiden_of_virtue; - creators["karazhan big bad wolf"] = &RaidKarazhanTriggerContext::karazhan_big_bad_wolf; - creators["karazhan romulo and julianne"] = &RaidKarazhanTriggerContext::karazhan_romulo_and_julianne; - creators["karazhan wizard of oz"] = &RaidKarazhanTriggerContext::karazhan_wizard_of_oz; - creators["karazhan the curator"] = &RaidKarazhanTriggerContext::karazhan_the_curator; - creators["karazhan terestian illhoof"] = &RaidKarazhanTriggerContext::karazhan_terestian_illhoof; - creators["karazhan shade of aran"] = &RaidKarazhanTriggerContext::karazhan_shade_of_aran; - creators["karazhan netherspite"] = &RaidKarazhanTriggerContext::karazhan_netherspite; - creators["karazhan prince malchezaar"] = &RaidKarazhanTriggerContext::karazhan_prince_malchezaar; + // Trash + creators["mana warp is about to explode"] = + &RaidKarazhanTriggerContext::mana_warp_is_about_to_explode; + + // Attumen the Huntsman + creators["attumen the huntsman need target priority"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_need_target_priority; + + creators["attumen the huntsman attumen spawned"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_attumen_spawned; + + creators["attumen the huntsman attumen is mounted"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_attumen_is_mounted; + + creators["attumen the huntsman boss wipes aggro when mounting"] = + &RaidKarazhanTriggerContext::attumen_the_huntsman_boss_wipes_aggro_when_mounting; + + // Moroes + creators["moroes boss engaged by main tank"] = + &RaidKarazhanTriggerContext::moroes_boss_engaged_by_main_tank; + + creators["moroes need target priority"] = + &RaidKarazhanTriggerContext::moroes_need_target_priority; + + // Maiden of Virtue + creators["maiden of virtue healers are stunned by repentance"] = + &RaidKarazhanTriggerContext::maiden_of_virtue_healers_are_stunned_by_repentance; + + creators["maiden of virtue holy wrath deals chain damage"] = + &RaidKarazhanTriggerContext::maiden_of_virtue_holy_wrath_deals_chain_damage; + + // The Big Bad Wolf + creators["big bad wolf boss engaged by tank"] = + &RaidKarazhanTriggerContext::big_bad_wolf_boss_engaged_by_tank; + + creators["big bad wolf boss is chasing little red riding hood"] = + &RaidKarazhanTriggerContext::big_bad_wolf_boss_is_chasing_little_red_riding_hood; + + // Romulo and Julianne + creators["romulo and julianne both bosses revived"] = + &RaidKarazhanTriggerContext::romulo_and_julianne_both_bosses_revived; + + // The Wizard of Oz + creators["wizard of oz need target priority"] = + &RaidKarazhanTriggerContext::wizard_of_oz_need_target_priority; + + creators["wizard of oz strawman is vulnerable to fire"] = + &RaidKarazhanTriggerContext::wizard_of_oz_strawman_is_vulnerable_to_fire; + + // The Curator + creators["the curator astral flare spawned"] = + &RaidKarazhanTriggerContext::the_curator_astral_flare_spawned; + + creators["the curator boss engaged by tanks"] = + &RaidKarazhanTriggerContext::the_curator_boss_engaged_by_tanks; + + creators["the curator astral flares cast arcing sear"] = + &RaidKarazhanTriggerContext::the_curator_astral_flares_cast_arcing_sear; + + // Terestian Illhoof + creators["terestian illhoof need target priority"] = + &RaidKarazhanTriggerContext::terestian_illhoof_need_target_priority; + + // Shade of Aran + creators["shade of aran arcane explosion is casting"] = + &RaidKarazhanTriggerContext::shade_of_aran_arcane_explosion_is_casting; + + creators["shade of aran flame wreath is active"] = + &RaidKarazhanTriggerContext::shade_of_aran_flame_wreath_is_active; + + creators["shade of aran conjured elementals summoned"] = + &RaidKarazhanTriggerContext::shade_of_aran_conjured_elementals_summoned; + + creators["shade of aran boss uses counterspell and blizzard"] = + &RaidKarazhanTriggerContext::shade_of_aran_boss_uses_counterspell_and_blizzard; + + // Netherspite + creators["netherspite red beam is active"] = + &RaidKarazhanTriggerContext::netherspite_red_beam_is_active; + + creators["netherspite blue beam is active"] = + &RaidKarazhanTriggerContext::netherspite_blue_beam_is_active; + + creators["netherspite green beam is active"] = + &RaidKarazhanTriggerContext::netherspite_green_beam_is_active; + + creators["netherspite bot is not beam blocker"] = + &RaidKarazhanTriggerContext::netherspite_bot_is_not_beam_blocker; + + creators["netherspite boss is banished"] = + &RaidKarazhanTriggerContext::netherspite_boss_is_banished; + + creators["netherspite need to manage timers and trackers"] = + &RaidKarazhanTriggerContext::netherspite_need_to_manage_timers_and_trackers; + + // Prince Malchezaar + creators["prince malchezaar bot is enfeebled"] = + &RaidKarazhanTriggerContext::prince_malchezaar_bot_is_enfeebled; + + creators["prince malchezaar infernals are spawned"] = + &RaidKarazhanTriggerContext::prince_malchezaar_infernals_are_spawned; + + creators["prince malchezaar boss engaged by main tank"] = + &RaidKarazhanTriggerContext::prince_malchezaar_boss_engaged_by_main_tank; + + // Nightbane + creators["nightbane boss engaged by main tank"] = + &RaidKarazhanTriggerContext::nightbane_boss_engaged_by_main_tank; + + creators["nightbane ranged bots are in charred earth"] = + &RaidKarazhanTriggerContext::nightbane_ranged_bots_are_in_charred_earth; + + creators["nightbane main tank is susceptible to fear"] = + &RaidKarazhanTriggerContext::nightbane_main_tank_is_susceptible_to_fear; + + creators["nightbane pets ignore collision to chase flying boss"] = + &RaidKarazhanTriggerContext::nightbane_pets_ignore_collision_to_chase_flying_boss; + + creators["nightbane boss is flying"] = + &RaidKarazhanTriggerContext::nightbane_boss_is_flying; + + creators["nightbane need to manage timers and trackers"] = + &RaidKarazhanTriggerContext::nightbane_need_to_manage_timers_and_trackers; } private: - static Trigger* karazhan_attumen_the_huntsman(PlayerbotAI* botAI) { return new KarazhanAttumenTheHuntsmanTrigger(botAI); } - static Trigger* karazhan_moroes(PlayerbotAI* botAI) { return new KarazhanMoroesTrigger(botAI); } - static Trigger* karazhan_maiden_of_virtue(PlayerbotAI* botAI) { return new KarazhanMaidenOfVirtueTrigger(botAI); } - static Trigger* karazhan_big_bad_wolf(PlayerbotAI* botAI) { return new KarazhanBigBadWolfTrigger(botAI); } - static Trigger* karazhan_romulo_and_julianne(PlayerbotAI* botAI) { return new KarazhanRomuloAndJulianneTrigger(botAI); } - static Trigger* karazhan_wizard_of_oz(PlayerbotAI* botAI) { return new KarazhanWizardOfOzTrigger(botAI); } - static Trigger* karazhan_the_curator(PlayerbotAI* botAI) { return new KarazhanTheCuratorTrigger(botAI); } - static Trigger* karazhan_terestian_illhoof(PlayerbotAI* botAI) { return new KarazhanTerestianIllhoofTrigger(botAI); } - static Trigger* karazhan_shade_of_aran(PlayerbotAI* botAI) { return new KarazhanShadeOfAranTrigger(botAI); } - static Trigger* karazhan_netherspite(PlayerbotAI* botAI) { return new KarazhanNetherspiteTrigger(botAI); } - static Trigger* karazhan_prince_malchezaar(PlayerbotAI* botAI) { return new KarazhanPrinceMalchezaarTrigger(botAI); } + // Trash + static Trigger* mana_warp_is_about_to_explode( + PlayerbotAI* botAI) { return new ManaWarpIsAboutToExplodeTrigger(botAI); } + + // Attumen the Huntsman + static Trigger* attumen_the_huntsman_need_target_priority( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanNeedTargetPriorityTrigger(botAI); } + + static Trigger* attumen_the_huntsman_attumen_spawned( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenSpawnedTrigger(botAI); } + + static Trigger* attumen_the_huntsman_attumen_is_mounted( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenIsMountedTrigger(botAI); } + + static Trigger* attumen_the_huntsman_boss_wipes_aggro_when_mounting( + PlayerbotAI* botAI) { return new AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger(botAI); } + + // Moroes + static Trigger* moroes_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new MoroesBossEngagedByMainTankTrigger(botAI); } + + static Trigger* moroes_need_target_priority( + PlayerbotAI* botAI) { return new MoroesNeedTargetPriorityTrigger(botAI); } + + // Maiden of Virtue + static Trigger* maiden_of_virtue_healers_are_stunned_by_repentance( + PlayerbotAI* botAI) { return new MaidenOfVirtueHealersAreStunnedByRepentanceTrigger(botAI); } + + static Trigger* maiden_of_virtue_holy_wrath_deals_chain_damage( + PlayerbotAI* botAI) { return new MaidenOfVirtueHolyWrathDealsChainDamageTrigger(botAI); } + + // The Big Bad Wolf + static Trigger* big_bad_wolf_boss_engaged_by_tank( + PlayerbotAI* botAI) { return new BigBadWolfBossEngagedByTankTrigger(botAI); } + + static Trigger* big_bad_wolf_boss_is_chasing_little_red_riding_hood( + PlayerbotAI* botAI) { return new BigBadWolfBossIsChasingLittleRedRidingHoodTrigger(botAI); } + + // Romulo and Julianne + static Trigger* romulo_and_julianne_both_bosses_revived( + PlayerbotAI* botAI) { return new RomuloAndJulianneBothBossesRevivedTrigger(botAI); } + + // The Wizard of Oz + static Trigger* wizard_of_oz_need_target_priority( + PlayerbotAI* botAI) { return new WizardOfOzNeedTargetPriorityTrigger(botAI); } + + static Trigger* wizard_of_oz_strawman_is_vulnerable_to_fire( + PlayerbotAI* botAI) { return new WizardOfOzStrawmanIsVulnerableToFireTrigger(botAI); } + + // The Curator + static Trigger* the_curator_astral_flare_spawned( + PlayerbotAI* botAI) { return new TheCuratorAstralFlareSpawnedTrigger(botAI); } + + static Trigger* the_curator_boss_engaged_by_tanks( + PlayerbotAI* botAI) { return new TheCuratorBossEngagedByTanksTrigger(botAI); } + + static Trigger* the_curator_astral_flares_cast_arcing_sear( + PlayerbotAI* botAI) { return new TheCuratorBossAstralFlaresCastArcingSearTrigger(botAI); } + + // Terestian Illhoof + static Trigger* terestian_illhoof_need_target_priority( + PlayerbotAI* botAI) { return new TerestianIllhoofNeedTargetPriorityTrigger(botAI); } + + // Shade of Aran + static Trigger* shade_of_aran_arcane_explosion_is_casting( + PlayerbotAI* botAI) { return new ShadeOfAranArcaneExplosionIsCastingTrigger(botAI); } + + static Trigger* shade_of_aran_flame_wreath_is_active( + PlayerbotAI* botAI) { return new ShadeOfAranFlameWreathIsActiveTrigger(botAI); } + + static Trigger* shade_of_aran_conjured_elementals_summoned( + PlayerbotAI* botAI) { return new ShadeOfAranConjuredElementalsSummonedTrigger(botAI); } + + static Trigger* shade_of_aran_boss_uses_counterspell_and_blizzard( + PlayerbotAI* botAI) { return new ShadeOfAranBossUsesCounterspellAndBlizzardTrigger(botAI); } + + // Netherspite + static Trigger* netherspite_red_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteRedBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_blue_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteBlueBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_green_beam_is_active( + PlayerbotAI* botAI) { return new NetherspiteGreenBeamIsActiveTrigger(botAI); } + + static Trigger* netherspite_bot_is_not_beam_blocker( + PlayerbotAI* botAI) { return new NetherspiteBotIsNotBeamBlockerTrigger(botAI); } + + static Trigger* netherspite_boss_is_banished( + PlayerbotAI* botAI) { return new NetherspiteBossIsBanishedTrigger(botAI); } + + static Trigger* netherspite_need_to_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NetherspiteNeedToManageTimersAndTrackersTrigger(botAI); } + + // Prince Malchezaar + static Trigger* prince_malchezaar_bot_is_enfeebled( + PlayerbotAI* botAI) { return new PrinceMalchezaarBotIsEnfeebledTrigger(botAI); } + + static Trigger* prince_malchezaar_infernals_are_spawned( + PlayerbotAI* botAI) { return new PrinceMalchezaarInfernalsAreSpawnedTrigger(botAI); } + + static Trigger* prince_malchezaar_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new PrinceMalchezaarBossEngagedByMainTankTrigger(botAI); } + + // Nightbane + static Trigger* nightbane_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new NightbaneBossEngagedByMainTankTrigger(botAI); } + + static Trigger* nightbane_ranged_bots_are_in_charred_earth( + PlayerbotAI* botAI) { return new NightbaneRangedBotsAreInCharredEarthTrigger(botAI); } + + static Trigger* nightbane_main_tank_is_susceptible_to_fear( + PlayerbotAI* botAI) { return new NightbaneMainTankIsSusceptibleToFearTrigger(botAI); } + + static Trigger* nightbane_pets_ignore_collision_to_chase_flying_boss( + PlayerbotAI* botAI) { return new NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger(botAI); } + + static Trigger* nightbane_boss_is_flying( + PlayerbotAI* botAI) { return new NightbaneBossIsFlyingTrigger(botAI); } + + static Trigger* nightbane_need_to_manage_timers_and_trackers( + PlayerbotAI* botAI) { return new NightbaneNeedToManageTimersAndTrackersTrigger(botAI); } }; -#endif +#endif \ No newline at end of file diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp b/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp index f70ae70fde..e39006f76e 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggers.cpp @@ -3,17 +3,64 @@ #include "RaidKarazhanActions.h" #include "Playerbots.h" -bool KarazhanAttumenTheHuntsmanTrigger::IsActive() +using namespace KarazhanHelpers; + +bool ManaWarpIsAboutToExplodeTrigger::IsActive() +{ + Unit* manaWarp = AI_VALUE2(Unit*, "find target", "mana warp"); + return manaWarp && manaWarp->GetHealthPct() < 15; +} + +bool AttumenTheHuntsmanNeedTargetPriorityTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + return midnight != nullptr; +} + +bool AttumenTheHuntsmanAttumenSpawnedTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN); + return attumen != nullptr; +} + +bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + return attumenMounted && attumenMounted->GetVictim() != bot; +} + +bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() { - RaidKarazhanHelpers helpers(botAI); - Unit* boss = helpers.GetFirstAliveUnitByEntry(NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); + return midnight != nullptr; } -bool KarazhanMoroesTrigger::IsActive() +bool MoroesBossEngagedByMainTankTrigger::IsActive() { + if (!botAI->IsMainTank(bot)) + return false; + Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes"); + return moroes != nullptr; +} + +bool MoroesNeedTargetPriorityTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); @@ -21,39 +68,67 @@ bool KarazhanMoroesTrigger::IsActive() Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - return ((moroes && moroes->IsAlive()) || - (dorothea && dorothea->IsAlive()) || - (catriona && catriona->IsAlive()) || - (keira && keira->IsAlive()) || - (rafe && rafe->IsAlive()) || - (robin && robin->IsAlive()) || - (crispin && crispin->IsAlive())); + Unit* target = GetFirstAliveUnit({ dorothea, catriona, keira, rafe, robin, crispin }); + return target != nullptr; } -bool KarazhanMaidenOfVirtueTrigger::IsActive() +bool MaidenOfVirtueHealersAreStunnedByRepentanceTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + if (!botAI->IsTank(bot)) + return false; - return boss && boss->IsAlive(); + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + return maiden && maiden->GetVictim() == bot; } -bool KarazhanBigBadWolfTrigger::IsActive() +bool MaidenOfVirtueHolyWrathDealsChainDamageTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + if (!botAI->IsRanged(bot)) + return false; - return boss && boss->IsAlive(); + Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue"); + return maiden != nullptr; } -bool KarazhanRomuloAndJulianneTrigger::IsActive() +bool BigBadWolfBossEngagedByTankTrigger::IsActive() { - Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!botAI->IsTank(bot) || bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + return false; + + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + return wolf != nullptr; +} + +bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() +{ + if (!bot->HasAura(SPELL_LITTLE_RED_RIDING_HOOD)) + return false; + + Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf"); + return wolf != nullptr; +} + +bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() +{ + if (!IsMapIDTimerManager(botAI, bot)) + return false; + Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); + if (!romulo) + return false; - return julianne && julianne->IsAlive() && romulo && romulo->IsAlive(); + Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne"); + if (!julianne) + return false; + + return true; } -bool KarazhanWizardOfOzTrigger::IsActive() +bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { + if (!IsMapIDTimerManager(botAI, bot)) + return false; + Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); @@ -61,45 +136,249 @@ bool KarazhanWizardOfOzTrigger::IsActive() Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - return ((dorothee && dorothee->IsAlive()) || - (tito && tito->IsAlive()) || - (roar && roar->IsAlive()) || - (strawman && strawman->IsAlive()) || - (tinhead && tinhead->IsAlive()) || - (crone && crone->IsAlive())); + Unit* target = GetFirstAliveUnit({ dorothee, tito, roar, strawman, tinhead, crone }); + return target != nullptr; +} + +bool WizardOfOzStrawmanIsVulnerableToFireTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE) + return false; + + Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); + return strawman && strawman->IsAlive(); +} + +bool TheCuratorAstralFlareSpawnedTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); + return flare != nullptr; +} + +bool TheCuratorBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + return curator != nullptr; +} + +bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* curator = AI_VALUE2(Unit*, "find target", "the curator"); + return curator != nullptr; } -bool KarazhanTheCuratorTrigger::IsActive() +bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "the curator"); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + return illhoof != nullptr; } -bool KarazhanTerestianIllhoofTrigger::IsActive() +bool ShadeOfAranArcaneExplosionIsCastingTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION) && + !IsFlameWreathActive(botAI, bot); +} - return boss && boss->IsAlive(); +bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() +{ + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && IsFlameWreathActive(botAI, bot); } -bool KarazhanShadeOfAranTrigger::IsActive() +// Exclusion of Banish is so the player may Banish elementals if they wish +bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "shade of aran"); + if (!IsMapIDTimerManager(botAI, bot)) + return false; - return boss && boss->IsAlive(); + Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); + return elemental && elemental->IsAlive() && + !elemental->HasAura(SPELL_WARLOCK_BANISH); } -bool KarazhanNetherspiteTrigger::IsActive() +bool ShadeOfAranBossUsesCounterspellAndBlizzardTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!botAI->IsRanged(bot)) + return false; - return boss && boss->IsAlive(); + Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran"); + return aran && !(aran->HasUnitState(UNIT_STATE_CASTING) && + aran->FindCurrentSpellBySpellId(SPELL_ARCANE_EXPLOSION)) && + !IsFlameWreathActive(botAI, bot); } -bool KarazhanPrinceMalchezaarTrigger::IsActive() +bool NetherspiteRedBeamIsActiveTrigger::IsActive() { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; - return boss && boss->IsAlive(); + Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); + return redPortal != nullptr; +} + +bool NetherspiteBlueBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f); + return bluePortal != nullptr; +} + +bool NetherspiteGreenBeamIsActiveTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f); + return greenPortal != nullptr; +} + +bool NetherspiteBotIsNotBeamBlockerTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); + return bot != redBlocker && bot != blueBlocker && bot != greenBlocker; +} + +bool NetherspiteBossIsBanishedTrigger::IsActive() +{ + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + if (!netherspite || !netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) + return false; + + std::vector voidZones = GetAllVoidZones(botAI, bot); + for (Unit* vz : voidZones) + { + if (bot->GetExactDist2d(vz) < 4.0f) + return true; + } + + return false; +} + +bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() +{ + if (!botAI->IsTank(bot) && !IsMapIDTimerManager(botAI, bot)) + return false; + + Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); + return netherspite != nullptr; +} + +bool PrinceMalchezaarBotIsEnfeebledTrigger::IsActive() +{ + return bot->HasAura(SPELL_ENFEEBLE); +} + +bool PrinceMalchezaarInfernalsAreSpawnedTrigger::IsActive() +{ + if (botAI->IsMainTank(bot) || bot->HasAura(SPELL_ENFEEBLE)) + return false; + + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + return malchezaar != nullptr; +} + +bool PrinceMalchezaarBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar"); + return malchezaar != nullptr; +} + +bool NightbaneBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane && nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z; +} + +bool NightbaneRangedBotsAreInCharredEarthTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane && nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z; +} + +bool NightbaneMainTankIsSusceptibleToFearTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && + botAI->CanCastSpell("fear ward", mainTank); +} + +bool NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane) + return false; + + Pet* pet = bot->GetPet(); + return pet && pet->IsAlive(); +} + +bool NightbaneBossIsFlyingTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) + return false; + + const time_t now = std::time(nullptr); + const uint8 flightPhaseDurationSeconds = 35; + + return nightbaneFlightPhaseStartTimer.find(KARAZHAN_MAP_ID) != nightbaneFlightPhaseStartTimer.end() && + (now - nightbaneFlightPhaseStartTimer[KARAZHAN_MAP_ID] < flightPhaseDurationSeconds); +} + +bool NightbaneNeedToManageTimersAndTrackersTrigger::IsActive() +{ + Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane"); + return nightbane != nullptr; } diff --git a/src/strategy/raids/karazhan/RaidKarazhanTriggers.h b/src/strategy/raids/karazhan/RaidKarazhanTriggers.h index 69f441525b..e3465ef1e8 100644 --- a/src/strategy/raids/karazhan/RaidKarazhanTriggers.h +++ b/src/strategy/raids/karazhan/RaidKarazhanTriggers.h @@ -3,80 +3,298 @@ #include "Trigger.h" -class KarazhanAttumenTheHuntsmanTrigger : public Trigger +class ManaWarpIsAboutToExplodeTrigger : public Trigger { public: - KarazhanAttumenTheHuntsmanTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan attumen the huntsman") {} + ManaWarpIsAboutToExplodeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mana warp is about to explode") {} bool IsActive() override; }; -class KarazhanMoroesTrigger : public Trigger +class AttumenTheHuntsmanNeedTargetPriorityTrigger : public Trigger { public: - KarazhanMoroesTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan moroes") {} + AttumenTheHuntsmanNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman need target priority") {} bool IsActive() override; }; -class KarazhanMaidenOfVirtueTrigger : public Trigger +class AttumenTheHuntsmanAttumenSpawnedTrigger : public Trigger { public: - KarazhanMaidenOfVirtueTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan maiden of virtue") {} + AttumenTheHuntsmanAttumenSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman attumen spawned") {} bool IsActive() override; }; -class KarazhanBigBadWolfTrigger : public Trigger +class AttumenTheHuntsmanAttumenIsMountedTrigger : public Trigger { public: - KarazhanBigBadWolfTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan big bad wolf") {} + AttumenTheHuntsmanAttumenIsMountedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman attumen is mounted") {} bool IsActive() override; }; -class KarazhanRomuloAndJulianneTrigger : public Trigger +class AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger : public Trigger { public: - KarazhanRomuloAndJulianneTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan romulo and julianne") {} + AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "attumen the huntsman boss wipes aggro when mounting") {} bool IsActive() override; }; -class KarazhanWizardOfOzTrigger : public Trigger +class MoroesBossEngagedByMainTankTrigger : public Trigger { public: - KarazhanWizardOfOzTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan wizard of oz") {} + MoroesBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "moroes boss engaged by main tank") {} bool IsActive() override; }; -class KarazhanTheCuratorTrigger : public Trigger +class MoroesNeedTargetPriorityTrigger : public Trigger { public: - KarazhanTheCuratorTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan the curator") {} + MoroesNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "moroes need target priority") {} bool IsActive() override; }; -class KarazhanTerestianIllhoofTrigger : public Trigger +class MaidenOfVirtueHealersAreStunnedByRepentanceTrigger : public Trigger { public: - KarazhanTerestianIllhoofTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan terestian illhoof") {} + MaidenOfVirtueHealersAreStunnedByRepentanceTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "maiden of virtue healers are stunned by repentance") {} bool IsActive() override; }; -class KarazhanShadeOfAranTrigger : public Trigger +class MaidenOfVirtueHolyWrathDealsChainDamageTrigger : public Trigger { public: - KarazhanShadeOfAranTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan shade of aran") {} + MaidenOfVirtueHolyWrathDealsChainDamageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "maiden of virtue holy wrath deals chain damage") {} bool IsActive() override; }; -class KarazhanNetherspiteTrigger : public Trigger +class BigBadWolfBossEngagedByTankTrigger : public Trigger { public: - KarazhanNetherspiteTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan netherspite") {} + BigBadWolfBossEngagedByTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "big bad wolf boss engaged by tank") {} bool IsActive() override; }; -class KarazhanPrinceMalchezaarTrigger : public Trigger +class BigBadWolfBossIsChasingLittleRedRidingHoodTrigger : public Trigger { public: - KarazhanPrinceMalchezaarTrigger(PlayerbotAI* botAI) : Trigger(botAI, "karazhan prince malchezaar") {} + BigBadWolfBossIsChasingLittleRedRidingHoodTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "big bad wolf boss is chasing little red riding hood") {} + bool IsActive() override; +}; + +class RomuloAndJulianneBothBossesRevivedTrigger : public Trigger +{ +public: + RomuloAndJulianneBothBossesRevivedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "romulo and julianne both bosses revived") {} + bool IsActive() override; +}; + +class WizardOfOzNeedTargetPriorityTrigger : public Trigger +{ +public: + WizardOfOzNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "wizard of oz need target priority") {} + bool IsActive() override; +}; + +class WizardOfOzStrawmanIsVulnerableToFireTrigger : public Trigger +{ +public: + WizardOfOzStrawmanIsVulnerableToFireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "wizard of oz strawman is vulnerable to fire") {} + bool IsActive() override; +}; +class TheCuratorAstralFlareSpawnedTrigger : public Trigger +{ +public: + TheCuratorAstralFlareSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator astral flare spawned") {} + bool IsActive() override; +}; + +class TheCuratorBossEngagedByTanksTrigger : public Trigger +{ +public: + TheCuratorBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator boss engaged by tanks") {} + bool IsActive() override; +}; + +class TheCuratorBossAstralFlaresCastArcingSearTrigger : public Trigger +{ +public: + TheCuratorBossAstralFlaresCastArcingSearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the curator astral flares cast arcing sear") {} + bool IsActive() override; +}; + +class TerestianIllhoofNeedTargetPriorityTrigger : public Trigger +{ +public: + TerestianIllhoofNeedTargetPriorityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "terestian illhoof need target priority") {} + bool IsActive() override; +}; + +class ShadeOfAranArcaneExplosionIsCastingTrigger : public Trigger +{ +public: + ShadeOfAranArcaneExplosionIsCastingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran arcane explosion is casting") {} + bool IsActive() override; +}; + +class ShadeOfAranFlameWreathIsActiveTrigger : public Trigger +{ +public: + ShadeOfAranFlameWreathIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran flame wreath is active") {} + bool IsActive() override; +}; + +class ShadeOfAranConjuredElementalsSummonedTrigger : public Trigger +{ +public: + ShadeOfAranConjuredElementalsSummonedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran conjured elementals summoned") {} + bool IsActive() override; +}; + +class ShadeOfAranBossUsesCounterspellAndBlizzardTrigger : public Trigger +{ +public: + ShadeOfAranBossUsesCounterspellAndBlizzardTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of aran boss uses counterspell and blizzard") {} + bool IsActive() override; +}; + +class NetherspiteRedBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteRedBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite red beam is active") {} + bool IsActive() override; +}; + +class NetherspiteBlueBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteBlueBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite blue beam is active") {} + bool IsActive() override; +}; + +class NetherspiteGreenBeamIsActiveTrigger : public Trigger +{ +public: + NetherspiteGreenBeamIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite green beam is active") {} + bool IsActive() override; +}; + +class NetherspiteBotIsNotBeamBlockerTrigger : public Trigger +{ +public: + NetherspiteBotIsNotBeamBlockerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite bot is not beam blocker") {} + bool IsActive() override; +}; + +class NetherspiteBossIsBanishedTrigger : public Trigger +{ +public: + NetherspiteBossIsBanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite boss is banished") {} + bool IsActive() override; +}; + +class NetherspiteNeedToManageTimersAndTrackersTrigger : public Trigger +{ +public: + NetherspiteNeedToManageTimersAndTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "netherspite need to manage timers and trackers") {} + bool IsActive() override; +}; + +class PrinceMalchezaarBotIsEnfeebledTrigger : public Trigger +{ +public: + PrinceMalchezaarBotIsEnfeebledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar bot is enfeebled") {} + bool IsActive() override; +}; + +class PrinceMalchezaarInfernalsAreSpawnedTrigger : public Trigger +{ +public: + PrinceMalchezaarInfernalsAreSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar infernals are spawned") {} + bool IsActive() override; +}; + +class PrinceMalchezaarBossEngagedByMainTankTrigger : public Trigger +{ +public: + PrinceMalchezaarBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "prince malchezaar boss engaged by main tank") {} + bool IsActive() override; +}; + +class NightbaneBossEngagedByMainTankTrigger : public Trigger +{ +public: + NightbaneBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane boss engaged by main tank") {} + bool IsActive() override; +}; + +class NightbaneRangedBotsAreInCharredEarthTrigger : public Trigger +{ +public: + NightbaneRangedBotsAreInCharredEarthTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane ranged bots are in charred earth") {} + bool IsActive() override; +}; + +class NightbaneMainTankIsSusceptibleToFearTrigger : public Trigger +{ +public: + NightbaneMainTankIsSusceptibleToFearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane main tank is susceptible to fear") {} + bool IsActive() override; +}; + +class NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger : public Trigger +{ +public: + NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane pets ignore collision to chase flying boss") {} + bool IsActive() override; +}; + +class NightbaneBossIsFlyingTrigger : public Trigger +{ +public: + NightbaneBossIsFlyingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane boss is flying") {} + bool IsActive() override; +}; + +class NightbaneNeedToManageTimersAndTrackersTrigger : public Trigger +{ +public: + NightbaneNeedToManageTimersAndTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nightbane need to manage timers and trackers") {} bool IsActive() override; }; diff --git a/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h index 21fd9d459c..80c3a47e09 100644 --- a/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h +++ b/src/strategy/raids/magtheridon/RaidMagtheridonHelpers.h @@ -24,7 +24,7 @@ namespace MagtheridonHelpers SPELL_FEAR = 6215, // Hunter - SPELL_MISDIRECTION = 34477, + SPELL_MISDIRECTION = 35079, }; enum MagtheridonNPCs diff --git a/src/strategy/raids/moltencore/RaidMcActionContext.h b/src/strategy/raids/moltencore/RaidMcActionContext.h index 45e6e056cc..79a4a95a85 100644 --- a/src/strategy/raids/moltencore/RaidMcActionContext.h +++ b/src/strategy/raids/moltencore/RaidMcActionContext.h @@ -10,13 +10,39 @@ class RaidMcActionContext : public NamedObjectContext public: RaidMcActionContext() { - creators["mc check should move from group"] = &RaidMcActionContext::check_should_move_from_group; + creators["mc lucifron shadow resistance"] = &RaidMcActionContext::lucifron_shadow_resistance; + creators["mc magmadar fire resistance"] = &RaidMcActionContext::magmadar_fire_resistance; + creators["mc gehennas shadow resistance"] = &RaidMcActionContext::gehennas_shadow_resistance; + creators["mc garr fire resistance"] = &RaidMcActionContext::garr_fire_resistance; + creators["mc baron geddon fire resistance"] = &RaidMcActionContext::baron_geddon_fire_resistance; + creators["mc move from group"] = &RaidMcActionContext::check_should_move_from_group; creators["mc move from baron geddon"] = &RaidMcActionContext::move_from_baron_geddon; + creators["mc shazzrah move away"] = &RaidMcActionContext::shazzrah_move_away; + creators["mc sulfuron harbinger fire resistance"] = &RaidMcActionContext::sulfuron_harbinger_fire_resistance; + creators["mc golemagg fire resistance"] = &RaidMcActionContext::golemagg_fire_resistance; + creators["mc golemagg mark boss"] = &RaidMcActionContext::golemagg_mark_boss; + creators["mc golemagg main tank attack golemagg"] = &RaidMcActionContext::golemagg_main_tank_attack_golemagg; + creators["mc golemagg assist tank attack core rager"] = &RaidMcActionContext::golemagg_assist_tank_attack_core_rager; + creators["mc majordomo shadow resistance"] = &RaidMcActionContext::majordomo_shadow_resistance; + creators["mc ragnaros fire resistance"] = &RaidMcActionContext::ragnaros_fire_resistance; } private: - static Action* check_should_move_from_group(PlayerbotAI* ai) { return new McCheckShouldMoveFromGroupAction(ai); } - static Action* move_from_baron_geddon(PlayerbotAI* ai) { return new McMoveFromBaronGeddonAction(ai); } + static Action* lucifron_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "lucifron"); } + static Action* magmadar_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "magmadar"); } + static Action* gehennas_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "gehennas"); } + static Action* garr_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "garr"); } + static Action* baron_geddon_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "baron geddon"); } + static Action* check_should_move_from_group(PlayerbotAI* botAI) { return new McMoveFromGroupAction(botAI); } + static Action* move_from_baron_geddon(PlayerbotAI* botAI) { return new McMoveFromBaronGeddonAction(botAI); } + static Action* shazzrah_move_away(PlayerbotAI* botAI) { return new McShazzrahMoveAwayAction(botAI); } + static Action* sulfuron_harbinger_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "sulfuron harbinger"); } + static Action* golemagg_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "golemagg the incinerator"); } + static Action* golemagg_mark_boss(PlayerbotAI* botAI) { return new McGolemaggMarkBossAction(botAI); } + static Action* golemagg_main_tank_attack_golemagg(PlayerbotAI* botAI) { return new McGolemaggMainTankAttackGolemaggAction(botAI); } + static Action* golemagg_assist_tank_attack_core_rager(PlayerbotAI* botAI) { return new McGolemaggAssistTankAttackCoreRagerAction(botAI); } + static Action* majordomo_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "majordomo executus"); } + static Action* ragnaros_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "ragnaros"); } }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcActions.cpp b/src/strategy/raids/moltencore/RaidMcActions.cpp index b82a073c40..b18c8b8534 100644 --- a/src/strategy/raids/moltencore/RaidMcActions.cpp +++ b/src/strategy/raids/moltencore/RaidMcActions.cpp @@ -1,43 +1,215 @@ #include "RaidMcActions.h" #include "Playerbots.h" +#include "RtiTargetValue.h" +#include "RaidMcTriggers.h" +#include "RaidMcHelpers.h" -bool McCheckShouldMoveFromGroupAction::Execute(Event event) +static constexpr float LIVING_BOMB_DISTANCE = 20.0f; +static constexpr float INFERNO_DISTANCE = 20.0f; + +// don't get hit by Arcane Explosion but still be in casting range +static constexpr float ARCANE_EXPLOSION_DISTANCE = 26.0f; + +// dedicated tank positions; prevents assist tanks from positioning Core Ragers on steep walls on pull +static const Position GOLEMAGG_TANK_POSITION{795.7308, -994.8848, -207.18661}; +static const Position CORE_RAGER_TANK_POSITION{846.6453, -1019.0639, -198.9819}; + +static constexpr float GOLEMAGGS_TRUST_DISTANCE = 30.0f; +static constexpr float CORE_RAGER_STEP_DISTANCE = 5.0f; + +using namespace MoltenCoreHelpers; + +bool McMoveFromGroupAction::Execute(Event event) +{ + return MoveFromGroup(LIVING_BOMB_DISTANCE); +} + +bool McMoveFromBaronGeddonAction::Execute(Event event) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + { + float distToTravel = INFERNO_DISTANCE - bot->GetDistance2d(boss); + if (distToTravel > 0) + { + // Stop current spell first + bot->AttackStop(); + bot->InterruptNonMeleeSpells(false); + + return MoveAway(boss, distToTravel); + } + } + return false; +} + +bool McShazzrahMoveAwayAction::Execute(Event event) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "shazzrah")) + { + float distToTravel = ARCANE_EXPLOSION_DISTANCE - bot->GetDistance2d(boss); + if (distToTravel > 0) + return MoveAway(boss, distToTravel); + } + return false; +} + +bool McGolemaggMarkBossAction::Execute(Event event) { - if (bot->HasAura(20475)) // barron geddon's living bomb + if (Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) { - if (!botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT)) + if (Group* group = bot->GetGroup()) { - // add/remove from both for now as it will make it more obvious to - // player if this strat remains on after fight somehow - botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT); - return true; + ObjectGuid currentSkullGuid = group->GetTargetIcon(RtiTargetValue::skullIndex); + if (currentSkullGuid.IsEmpty() || currentSkullGuid != boss->GetGUID()) + { + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + return true; + } } } - else if (botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT)) + return false; +} + +bool McGolemaggTankAction::MoveUnitToPosition(Unit* target, const Position& tankPosition, float maxDistance, + float stepDistance) +{ + if (bot->GetVictim() != target) + return Attack(target); + if (target->GetVictim() == bot) { - // add/remove from both for now as it will make it more obvious to - // player if this strat remains on after fight somehow - botAI->ChangeStrategy("-move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("-move from group", BOT_STATE_COMBAT); + float distanceToTankPosition = bot->GetExactDist2d(tankPosition.GetPositionX(), tankPosition.GetPositionY()); + if (distanceToTankPosition > maxDistance) + { + float dX = tankPosition.GetPositionX() - bot->GetPositionX(); + float dY = tankPosition.GetPositionY() - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + float moveX = bot->GetPositionX() + (dX / dist) * stepDistance; + float moveY = bot->GetPositionY() + (dY / dist) * stepDistance; + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, + true); + } + } + else if (botAI->DoSpecificAction("taunt spell", Event(), true)) return true; + return false; +} + +bool McGolemaggTankAction::FindCoreRagers(Unit*& coreRager1, Unit*& coreRager2) const +{ + coreRager1 = coreRager2 = nullptr; + for (auto const& target : AI_VALUE(GuidVector, "possible targets no los")) + { + Unit* unit = botAI->GetUnit(target); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CORE_RAGER) + { + if (coreRager1 == nullptr) + coreRager1 = unit; + else if (coreRager2 == nullptr) + { + coreRager2 = unit; + break; // There should be no third Core Rager. + } + } + } + return coreRager1 != nullptr && coreRager2 != nullptr; +} + +bool McGolemaggMainTankAttackGolemaggAction::Execute(Event event) +{ + // At this point, we know we are not the last living tank in the group. + if (Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) + { + Unit* coreRager1; + Unit* coreRager2; + if (!FindCoreRagers(coreRager1, coreRager2)) + return false; // safety check + + // We only need to move if the Core Ragers still have Golemagg's Trust + if (coreRager1->HasAura(SPELL_GOLEMAGGS_TRUST) || coreRager2->HasAura(SPELL_GOLEMAGGS_TRUST)) + return MoveUnitToPosition(boss, GOLEMAGG_TANK_POSITION, boss->GetCombatReach()); } return false; } -bool McMoveFromBaronGeddonAction::Execute(Event event) +bool McGolemaggAssistTankAttackCoreRagerAction::Execute(Event event) { - const float radius = 25.0f; // more than should be needed but bots keep trying to run back in - if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator"); + if (!boss) + return false; + + // Step 0: Filter additional assist tanks. We only need 2. + bool isFirstAssistTank = PlayerbotAI::IsAssistTankOfIndex(bot, 0, true); + bool isSecondAssistTank = PlayerbotAI::IsAssistTankOfIndex(bot, 1, true); + if (!isFirstAssistTank && !isSecondAssistTank) + return Attack(boss); + + // Step 1: Find both Core Ragers + Unit* coreRager1; + Unit* coreRager2; + if (!FindCoreRagers(coreRager1, coreRager2)) + return false; // safety check + + // Step 2: Assign Core Rager to bot + Unit* myCoreRager = nullptr; + Unit* otherCoreRager = nullptr; + if (isFirstAssistTank) { - long distToTravel = radius - bot->GetDistance(boss); - if (distToTravel > 0) + myCoreRager = coreRager1; + otherCoreRager = coreRager2; + } + else // isSecondAssistTank is always true here + { + myCoreRager = coreRager2; + otherCoreRager = coreRager1; + } + + // Step 3: Select the right target + if (myCoreRager->GetVictim() != bot) + { + // Step 3.1: My Core Rager isn't attacking me. Attack until it does. + if (bot->GetVictim() != myCoreRager) + return Attack(myCoreRager); + return botAI->DoSpecificAction("taunt spell", event, true); + } + + Unit* otherCoreRagerVictim = otherCoreRager->GetVictim(); + if (otherCoreRagerVictim) // Core Rager victim can be NULL + { + // Step 3.2: Check if the other Core Rager isn't attacking its assist tank. + Player* otherCoreRagerPlayerVictim = otherCoreRagerVictim->ToPlayer(); + if (otherCoreRagerPlayerVictim && + !PlayerbotAI::IsAssistTankOfIndex(otherCoreRagerPlayerVictim, 0, true) && + !PlayerbotAI::IsAssistTankOfIndex(otherCoreRagerPlayerVictim, 1, true)) { - // float angle = bot->GetAngle(boss) + M_PI; - // return Move(angle, distToTravel); - return MoveAway(boss, distToTravel); + // Assume we are the only assist tank or the other assist tank is dead => pick up other Core Rager! + if (bot->GetVictim() != otherCoreRager) + return Attack(otherCoreRager); + return botAI->DoSpecificAction("taunt spell", event, true); } } + + if (bot->GetVictim() != myCoreRager) + return Attack(myCoreRager); // Step 3.3: Attack our Core Rager in case we previously switched in 3.2. + + // Step 4: Prevent Golemagg's Trust on Core Ragers + if (myCoreRager->HasAura(SPELL_GOLEMAGGS_TRUST) || + (otherCoreRagerVictim == bot && otherCoreRager->HasAura(SPELL_GOLEMAGGS_TRUST))) + { + // Step 4.1: Move Core Ragers to dedicated tank position (only if Golemagg is far enough away from said position) + float bossDistanceToCoreRagerTankPosition = boss->GetExactDist2d( + CORE_RAGER_TANK_POSITION.GetPositionX(), CORE_RAGER_TANK_POSITION.GetPositionY()); + if (bossDistanceToCoreRagerTankPosition > GOLEMAGGS_TRUST_DISTANCE) + { + float distanceToTankPosition = bot->GetExactDist2d(CORE_RAGER_TANK_POSITION.GetPositionX(), + CORE_RAGER_TANK_POSITION.GetPositionY()); + if (distanceToTankPosition > CORE_RAGER_STEP_DISTANCE) + return MoveUnitToPosition(myCoreRager, CORE_RAGER_TANK_POSITION, CORE_RAGER_STEP_DISTANCE); + } + + // Step 4.2: if boss is too close to tank position, or we are already there, move away from Golemagg to try to out-range Golemagg's Trust + return MoveAway(boss, CORE_RAGER_STEP_DISTANCE, true); + } + return false; } diff --git a/src/strategy/raids/moltencore/RaidMcActions.h b/src/strategy/raids/moltencore/RaidMcActions.h index 6ff18513ff..680b311d3c 100644 --- a/src/strategy/raids/moltencore/RaidMcActions.h +++ b/src/strategy/raids/moltencore/RaidMcActions.h @@ -1,15 +1,16 @@ #ifndef _PLAYERBOT_RAIDMCACTIONS_H #define _PLAYERBOT_RAIDMCACTIONS_H +#include "AttackAction.h" #include "MovementActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -class McCheckShouldMoveFromGroupAction : public Action +class McMoveFromGroupAction : public MovementAction { public: - McCheckShouldMoveFromGroupAction(PlayerbotAI* botAI, std::string const name = "mc check should move from group") - : Action(botAI, name) {} + McMoveFromGroupAction(PlayerbotAI* botAI, std::string const name = "mc move from group") + : MovementAction(botAI, name) {} bool Execute(Event event) override; }; @@ -21,4 +22,46 @@ class McMoveFromBaronGeddonAction : public MovementAction bool Execute(Event event) override; }; +class McShazzrahMoveAwayAction : public MovementAction +{ +public: + McShazzrahMoveAwayAction(PlayerbotAI* botAI, std::string const name = "mc shazzrah move away") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class McGolemaggMarkBossAction : public Action +{ +public: + McGolemaggMarkBossAction(PlayerbotAI* botAI, std::string const name = "mc golemagg mark boss") + : Action(botAI, name) {}; + bool Execute(Event event) override; +}; + +class McGolemaggTankAction : public AttackAction +{ +public: + McGolemaggTankAction(PlayerbotAI* botAI, std::string const name) + : AttackAction(botAI, name) {} +protected: + bool MoveUnitToPosition(Unit* target, const Position& tankPosition, float maxDistance, float stepDistance = 3.0f); + bool FindCoreRagers(Unit*& coreRager1, Unit*& coreRager2) const; +}; + +class McGolemaggMainTankAttackGolemaggAction : public McGolemaggTankAction +{ +public: + McGolemaggMainTankAttackGolemaggAction(PlayerbotAI* botAI, std::string const name = "mc golemagg main tank attack golemagg") + : McGolemaggTankAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class McGolemaggAssistTankAttackCoreRagerAction : public McGolemaggTankAction +{ +public: + McGolemaggAssistTankAttackCoreRagerAction(PlayerbotAI* botAI, std::string const name = "mc golemagg assist tank attack core rager") + : McGolemaggTankAction(botAI, name) {}; + bool Execute(Event event) override; +}; + #endif diff --git a/src/strategy/raids/moltencore/RaidMcHelpers.h b/src/strategy/raids/moltencore/RaidMcHelpers.h new file mode 100644 index 0000000000..5dc821246d --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcHelpers.h @@ -0,0 +1,22 @@ +#ifndef _PLAYERBOT_RAIDMCHELPERS_H +#define _PLAYERBOT_RAIDMCHELPERS_H + +namespace MoltenCoreHelpers +{ +enum MoltenCoreNPCs +{ + // Golemagg + NPC_CORE_RAGER = 11672, +}; +enum MoltenCoreSpells +{ + // Baron Geddon + SPELL_INFERNO = 19695, + SPELL_LIVING_BOMB = 20475, + + // Golemagg + SPELL_GOLEMAGGS_TRUST = 20553, +}; +} + +#endif diff --git a/src/strategy/raids/moltencore/RaidMcMultipliers.cpp b/src/strategy/raids/moltencore/RaidMcMultipliers.cpp new file mode 100644 index 0000000000..d1ee936b0d --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcMultipliers.cpp @@ -0,0 +1,117 @@ +#include "RaidMcMultipliers.h" + +#include "Playerbots.h" +#include "ChooseTargetActions.h" +#include "GenericSpellActions.h" +#include "DruidActions.h" +#include "HunterActions.h" +#include "PaladinActions.h" +#include "ShamanActions.h" +#include "WarriorActions.h" +#include "DKActions.h" +#include "RaidMcActions.h" +#include "RaidMcHelpers.h" + +using namespace MoltenCoreHelpers; + +static bool IsDpsBotWithAoeAction(Player* bot, Action* action) +{ + if (PlayerbotAI::IsDps(bot)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return true; + + if (auto castSpellAction = dynamic_cast(action)) + { + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return true; + } + } + return false; +} + +float GarrDisableDpsAoeMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "garr")) + { + if (IsDpsBotWithAoeAction(bot, action)) + return 0.0f; + } + return 1.0f; +} + +static bool IsAllowedGeddonMovementAction(Action* action) +{ + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return false; + + if (dynamic_cast(action)) + return false; + + return true; +} + +float BaronGeddonAbilityMultiplier::GetValue(Action* action) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + { + if (boss->HasAura(SPELL_INFERNO)) + { + if (!IsAllowedGeddonMovementAction(action)) + return 0.0f; + } + } + + // No check for Baron Geddon, because bots may have the bomb even after Geddon died. + if (bot->HasAura(SPELL_LIVING_BOMB)) + { + if (!IsAllowedGeddonMovementAction(action)) + return 0.0f; + } + + return 1.0f; +} + +static bool IsSingleLivingTankInGroup(Player* bot) +{ + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + if (PlayerbotAI::IsTank(member)) + return false; + } + } + return true; +} + +float GolemaggMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) + { + if (PlayerbotAI::IsTank(bot) && IsSingleLivingTankInGroup(bot)) + { + // Only one tank => Pick up Golemagg and the two Core Ragers + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + if (PlayerbotAI::IsAssistTank(bot)) + { + // The first two assist tanks manage the Core Ragers. The remaining assist tanks attack the boss. + if (dynamic_cast(action)) + return 0.0f; + } + if (IsDpsBotWithAoeAction(bot, action)) + return 0.0f; + } + return 1.0f; +} diff --git a/src/strategy/raids/moltencore/RaidMcMultipliers.h b/src/strategy/raids/moltencore/RaidMcMultipliers.h new file mode 100644 index 0000000000..56dc7a5f80 --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcMultipliers.h @@ -0,0 +1,27 @@ +#ifndef _PLAYERBOT_RAIDMCMULTIPLIERS_H +#define _PLAYERBOT_RAIDMCMULTIPLIERS_H + +#include "Multiplier.h" + +class GarrDisableDpsAoeMultiplier : public Multiplier +{ +public: + GarrDisableDpsAoeMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "garr disable dps aoe multiplier") {} + float GetValue(Action* action) override; +}; + +class BaronGeddonAbilityMultiplier : public Multiplier +{ +public: + BaronGeddonAbilityMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "baron geddon ability multiplier") {} + float GetValue(Action* action) override; +}; + +class GolemaggMultiplier : public Multiplier +{ +public: + GolemaggMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "golemagg multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/strategy/raids/moltencore/RaidMcStrategy.cpp b/src/strategy/raids/moltencore/RaidMcStrategy.cpp index 67793de906..c6a5042eb5 100644 --- a/src/strategy/raids/moltencore/RaidMcStrategy.cpp +++ b/src/strategy/raids/moltencore/RaidMcStrategy.cpp @@ -1,13 +1,81 @@ #include "RaidMcStrategy.h" +#include "RaidMcMultipliers.h" #include "Strategy.h" void RaidMcStrategy::InitTriggers(std::vector& triggers) { + // Lucifron + triggers.push_back( + new TriggerNode("mc lucifron shadow resistance", + NextAction::array(0, new NextAction("mc lucifron shadow resistance", ACTION_RAID), nullptr))); + + // Magmadar + // TODO: Fear ward / tremor totem, or general anti-fear strat development. Same as King Dred (Drak'Tharon) and faction commander (Nexus). + triggers.push_back( + new TriggerNode("mc magmadar fire resistance", + NextAction::array(0, new NextAction("mc magmadar fire resistance", ACTION_RAID), nullptr))); + + // Gehennas + triggers.push_back( + new TriggerNode("mc gehennas shadow resistance", + NextAction::array(0, new NextAction("mc gehennas shadow resistance", ACTION_RAID), nullptr))); + + // Garr + triggers.push_back( + new TriggerNode("mc garr fire resistance", + NextAction::array(0, new NextAction("mc garr fire resistance", ACTION_RAID), nullptr))); + + // Baron Geddon + triggers.push_back( + new TriggerNode("mc baron geddon fire resistance", + NextAction::array(0, new NextAction("mc baron geddon fire resistance", ACTION_RAID), nullptr))); triggers.push_back( new TriggerNode("mc living bomb debuff", - NextAction::array(0, new NextAction("mc check should move from group", ACTION_RAID), nullptr))); + NextAction::array(0, new NextAction("mc move from group", ACTION_RAID), nullptr))); triggers.push_back( new TriggerNode("mc baron geddon inferno", NextAction::array(0, new NextAction("mc move from baron geddon", ACTION_RAID), nullptr))); + + // Shazzrah + triggers.push_back( + new TriggerNode("mc shazzrah ranged", + NextAction::array(0, new NextAction("mc shazzrah move away", ACTION_RAID), nullptr))); + + // Sulfuron Harbinger + // Alternatively, shadow resistance is also possible. + triggers.push_back( + new TriggerNode("mc sulfuron harbinger fire resistance", + NextAction::array(0, new NextAction("mc sulfuron harbinger fire resistance", ACTION_RAID), nullptr))); + + // Golemagg the Incinerator + triggers.push_back( + new TriggerNode("mc golemagg fire resistance", + NextAction::array(0, new NextAction("mc golemagg fire resistance", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg mark boss", + NextAction::array(0, new NextAction("mc golemagg mark boss", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg is main tank", + NextAction::array(0, new NextAction("mc golemagg main tank attack golemagg", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg is assist tank", + NextAction::array(0, new NextAction("mc golemagg assist tank attack core rager", ACTION_RAID), nullptr))); + + // Majordomo Executus + triggers.push_back( + new TriggerNode("mc majordomo shadow resistance", + NextAction::array(0, new NextAction("mc majordomo shadow resistance", ACTION_RAID), nullptr))); + + // Ragnaros + triggers.push_back( + new TriggerNode("mc ragnaros fire resistance", + NextAction::array(0, new NextAction("mc ragnaros fire resistance", ACTION_RAID), nullptr))); +} + +void RaidMcStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new GarrDisableDpsAoeMultiplier(botAI)); + multipliers.push_back(new BaronGeddonAbilityMultiplier(botAI)); + multipliers.push_back(new GolemaggMultiplier(botAI)); } diff --git a/src/strategy/raids/moltencore/RaidMcStrategy.h b/src/strategy/raids/moltencore/RaidMcStrategy.h index 82b7d8f25e..45b503e933 100644 --- a/src/strategy/raids/moltencore/RaidMcStrategy.h +++ b/src/strategy/raids/moltencore/RaidMcStrategy.h @@ -8,10 +8,10 @@ class RaidMcStrategy : public Strategy { public: - RaidMcStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "mc"; } - virtual void InitTriggers(std::vector& triggers) override; - // virtual void InitMultipliers(std::vector &multipliers) override; + RaidMcStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + std::string const getName() override { return "moltencore"; } + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector &multipliers) override; }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcTriggerContext.h b/src/strategy/raids/moltencore/RaidMcTriggerContext.h index 93fe6df0f8..b74958919c 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggerContext.h +++ b/src/strategy/raids/moltencore/RaidMcTriggerContext.h @@ -10,13 +10,39 @@ class RaidMcTriggerContext : public NamedObjectContext public: RaidMcTriggerContext() { + creators["mc lucifron shadow resistance"] = &RaidMcTriggerContext::lucifron_shadow_resistance; + creators["mc magmadar fire resistance"] = &RaidMcTriggerContext::magmadar_fire_resistance; + creators["mc gehennas shadow resistance"] = &RaidMcTriggerContext::gehennas_shadow_resistance; + creators["mc garr fire resistance"] = &RaidMcTriggerContext::garr_fire_resistance; + creators["mc baron geddon fire resistance"] = &RaidMcTriggerContext::baron_geddon_fire_resistance; creators["mc living bomb debuff"] = &RaidMcTriggerContext::living_bomb_debuff; creators["mc baron geddon inferno"] = &RaidMcTriggerContext::baron_geddon_inferno; + creators["mc shazzrah ranged"] = &RaidMcTriggerContext::shazzrah_ranged; + creators["mc sulfuron harbinger fire resistance"] = &RaidMcTriggerContext::sulfuron_harbinger_fire_resistance; + creators["mc golemagg fire resistance"] = &RaidMcTriggerContext::golemagg_fire_resistance; + creators["mc golemagg mark boss"] = &RaidMcTriggerContext::golemagg_mark_boss; + creators["mc golemagg is main tank"] = &RaidMcTriggerContext::golemagg_is_main_tank; + creators["mc golemagg is assist tank"] = &RaidMcTriggerContext::golemagg_is_assist_tank; + creators["mc majordomo shadow resistance"] = &RaidMcTriggerContext::majordomo_shadow_resistance; + creators["mc ragnaros fire resistance"] = &RaidMcTriggerContext::ragnaros_fire_resistance; } private: - static Trigger* living_bomb_debuff(PlayerbotAI* ai) { return new McLivingBombDebuffTrigger(ai); } - static Trigger* baron_geddon_inferno(PlayerbotAI* ai) { return new McBaronGeddonInfernoTrigger(ai); } + static Trigger* lucifron_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "lucifron"); } + static Trigger* magmadar_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "magmadar"); } + static Trigger* gehennas_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "gehennas"); } + static Trigger* garr_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "garr"); } + static Trigger* baron_geddon_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "baron geddon"); } + static Trigger* living_bomb_debuff(PlayerbotAI* botAI) { return new McLivingBombDebuffTrigger(botAI); } + static Trigger* baron_geddon_inferno(PlayerbotAI* botAI) { return new McBaronGeddonInfernoTrigger(botAI); } + static Trigger* shazzrah_ranged(PlayerbotAI* botAI) { return new McShazzrahRangedTrigger(botAI); } + static Trigger* sulfuron_harbinger_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "sulfuron harbinger"); } + static Trigger* golemagg_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "golemagg the incinerator"); } + static Trigger* golemagg_mark_boss(PlayerbotAI* botAI) { return new McGolemaggMarkBossTrigger(botAI); } + static Trigger* golemagg_is_main_tank(PlayerbotAI* botAI) { return new McGolemaggIsMainTankTrigger(botAI); } + static Trigger* golemagg_is_assist_tank(PlayerbotAI* botAI) { return new McGolemaggIsAssistTankTrigger(botAI); } + static Trigger* majordomo_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "majordomo executus"); } + static Trigger* ragnaros_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "ragnaros"); } }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcTriggers.cpp b/src/strategy/raids/moltencore/RaidMcTriggers.cpp index f77b70f20e..834d703d33 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggers.cpp +++ b/src/strategy/raids/moltencore/RaidMcTriggers.cpp @@ -1,22 +1,40 @@ #include "RaidMcTriggers.h" #include "SharedDefines.h" +#include "RaidMcHelpers.h" + +using namespace MoltenCoreHelpers; bool McLivingBombDebuffTrigger::IsActive() { - // if bot has barron geddon's living bomb, we need to add strat, otherwise we need to remove - // only do when fighting baron geddon (to avoid modifying strat set by player outside this fight) - if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) - { - if (boss->IsInCombat()) - return bot->HasAura(20475) != botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT); - } - return false; + // No check for Baron Geddon, because bots may have the bomb even after Geddon died. + return bot->HasAura(SPELL_LIVING_BOMB); } bool McBaronGeddonInfernoTrigger::IsActive() { if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) - return boss->HasAura(19695); + return boss->HasAura(SPELL_INFERNO); return false; } + +bool McShazzrahRangedTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "shazzrah") && PlayerbotAI::IsRanged(bot); +} + +bool McGolemaggMarkBossTrigger::IsActive() +{ + // any tank may mark the boss + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsTank(bot); +} + +bool McGolemaggIsMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsMainTank(bot); +} + +bool McGolemaggIsAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsAssistTank(bot); +} diff --git a/src/strategy/raids/moltencore/RaidMcTriggers.h b/src/strategy/raids/moltencore/RaidMcTriggers.h index 9d2fb985a2..4cd84a2df2 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggers.h +++ b/src/strategy/raids/moltencore/RaidMcTriggers.h @@ -19,4 +19,32 @@ class McBaronGeddonInfernoTrigger : public Trigger bool IsActive() override; }; +class McShazzrahRangedTrigger : public Trigger +{ +public: + McShazzrahRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc shazzrah ranged") {} + bool IsActive() override; +}; + +class McGolemaggMarkBossTrigger : public Trigger +{ +public: + McGolemaggMarkBossTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg mark boss") {} + bool IsActive() override; +}; + +class McGolemaggIsMainTankTrigger : public Trigger +{ +public: + McGolemaggIsMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg is main tank") {} + bool IsActive() override; +}; + +class McGolemaggIsAssistTankTrigger : public Trigger +{ +public: + McGolemaggIsAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg is assist tank") {} + bool IsActive() override; +}; + #endif diff --git a/src/strategy/rogue/RogueTriggers.cpp b/src/strategy/rogue/RogueTriggers.cpp index 16aada0164..fd9901552e 100644 --- a/src/strategy/rogue/RogueTriggers.cpp +++ b/src/strategy/rogue/RogueTriggers.cpp @@ -22,8 +22,8 @@ bool UnstealthTrigger::IsActive() return botAI->HasAura("stealth", bot) && !AI_VALUE(uint8, "attacker count") && (AI_VALUE2(bool, "moving", "self target") && ((botAI->GetMaster() && - sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "master target"), 10.0f) && - AI_VALUE2(bool, "moving", "master target")) || + sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "group leader"), 10.0f) && + AI_VALUE2(bool, "moving", "group leader")) || !AI_VALUE(uint8, "attacker count"))); } diff --git a/src/strategy/triggers/RangeTriggers.cpp b/src/strategy/triggers/RangeTriggers.cpp index 3c60934fd2..af29f984d1 100644 --- a/src/strategy/triggers/RangeTriggers.cpp +++ b/src/strategy/triggers/RangeTriggers.cpp @@ -213,7 +213,7 @@ PartyMemberToHealOutOfSpellRangeTrigger::PartyMemberToHealOutOfSpellRangeTrigger bool FarFromMasterTrigger::IsActive() { - return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "master target"), distance); + return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "group leader"), distance); } bool TooCloseToCreatureTrigger::TooCloseToCreature(uint32 creatureId, float range, bool alive) diff --git a/src/strategy/values/AttackersValue.cpp b/src/strategy/values/AttackersValue.cpp index 745369a3b5..d89f1f8da5 100644 --- a/src/strategy/values/AttackersValue.cpp +++ b/src/strategy/values/AttackersValue.cpp @@ -107,7 +107,6 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set& t { ThreatMgr* threatMgr = ref->GetSource(); Unit* attacker = threatMgr->GetOwner(); - Unit* victim = attacker->GetVictim(); if (player->IsValidAttackTarget(attacker) && player->GetDistance2d(attacker) < sPlayerbotAIConfig->sightDistance) @@ -142,57 +141,107 @@ bool AttackersValue::hasRealThreat(Unit* attacker) (attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast(attacker)); } -bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float range) +bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) { - Creature* c = attacker->ToCreature(); - bool rti = false; - if (attacker && bot->GetGroup()) - rti = bot->GetGroup()->GetTargetIcon(7) == attacker->GetGUID(); - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + if (!botAI) + return false; + + // Basic check + if (!attacker) + return false; + + // bool inCannon = botAI->IsInVehicle(false, true); + // bool enemy = botAI->GetAiObjectContext()->GetValue("enemy player target")->Get(); + + // Validity checks + if (!attacker->IsVisible() || !attacker->IsInWorld() || attacker->GetMapId() != bot->GetMapId()) + return false; - bool leaderHasThreat = false; - if (attacker && bot->GetGroup() && botAI->GetMaster()) - leaderHasThreat = attacker->GetThreatMgr().GetThreat(botAI->GetMaster()); + if (attacker->isDead() || attacker->HasSpiritOfRedemptionAura()) + return false; - bool isMemberBotGroup = false; - if (bot->GetGroup() && botAI->GetMaster()) + // Flag checks + if (attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NON_ATTACKABLE_2)) + return false; + + if (attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) || attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return false; + + // Relationship checks + if (attacker->IsFriendlyTo(bot)) + return false; + + // Critter exception + if (attacker->GetCreatureType() == CREATURE_TYPE_CRITTER && !attacker->IsInCombat()) + return false; + + // Visibility check + if (!bot->CanSeeOrDetect(attacker)) + return false; + + // PvP prohibition checks + if ((attacker->GetGUID().IsPlayer() || attacker->GetGUID().IsPet()) && + (sPlayerbotAIConfig->IsPvpProhibited(attacker->GetZoneId(), attacker->GetAreaId()) || + sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()))) { - PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(botAI->GetMaster()); - if (masterBotAI && !masterBotAI->IsRealPlayer()) - isMemberBotGroup = true; + // This will stop aggresive pets from starting an attack. + // This will stop currently attacking pets from continuing their attack. + // This will first require the bot to change from a combat strat. It will + // not be reached if the bot only switches targets, including NPC targets. + for (Unit::ControlSet::const_iterator itr = bot->m_Controlled.begin(); + itr != bot->m_Controlled.end(); ++itr) + { + Creature* creature = dynamic_cast(*itr); + if (creature && creature->GetVictim() == attacker) + { + creature->AttackStop(); + if (CharmInfo* charmInfo = creature->GetCharmInfo()) + charmInfo->SetIsCommandAttack(false); + } + } + + return false; } - // bool inCannon = botAI->IsInVehicle(false, true); - // bool enemy = botAI->GetAiObjectContext()->GetValue("enemy player target")->Get(); + // Unflagged player check + if (attacker->IsPlayer() && !attacker->IsPvP() && !attacker->IsFFAPvP() && + (!bot->duel || bot->duel->Opponent != attacker)) + return false; + + // Creature-specific checks + Creature* c = attacker->ToCreature(); + if (c) + { + if (c->IsInEvadeMode()) + return false; + + bool leaderHasThreat = false; + if (bot->GetGroup() && botAI->GetMaster()) + leaderHasThreat = attacker->GetThreatMgr().GetThreat(botAI->GetMaster()); + + bool isMemberBotGroup = false; + if (bot->GetGroup() && botAI->GetMaster()) + { + PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(botAI->GetMaster()); + if (masterBotAI && !masterBotAI->IsRealPlayer()) + isMemberBotGroup = true; + } + + bool canAttack = (!isMemberBotGroup && botAI->HasStrategy("attack tagged", BOT_STATE_NON_COMBAT)) || + leaderHasThreat || + (!c->hasLootRecipient() && + (!c->GetVictim() || + (c->GetVictim() && + ((!c->GetVictim()->IsPlayer() || bot->IsInSameGroupWith(c->GetVictim()->ToPlayer())) || + (botAI->GetMaster() && c->GetVictim() == botAI->GetMaster()))))) || + c->isTappedBy(bot); + + if (!canAttack) + return false; + } - return attacker && attacker->IsVisible() && attacker->IsInWorld() && attacker->GetMapId() == bot->GetMapId() && - !attacker->isDead() && - !attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NON_ATTACKABLE_2) && - // (inCannon || !attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) && - // attacker->CanSeeOrDetect(bot) && - // !(attacker->HasUnitState(UNIT_STATE_STUNNED) && botAI->HasAura("shackle undead", attacker)) && - // !((attacker->IsPolymorphed() || botAI->HasAura("sap", attacker) || /*attacker->IsCharmed() ||*/ - // attacker->isFeared()) && !rti) && - /*!sServerFacade->IsInRoots(attacker) &&*/ - !attacker->IsFriendlyTo(bot) && !attacker->HasSpiritOfRedemptionAura() && - // !(attacker->GetGUID().IsPet() && enemy) && - !(attacker->GetCreatureType() == CREATURE_TYPE_CRITTER && !attacker->IsInCombat()) && - !attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) && !attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) && - bot->CanSeeOrDetect(attacker) && - !(sPlayerbotAIConfig->IsPvpProhibited(attacker->GetZoneId(), attacker->GetAreaId()) && - (attacker->GetGUID().IsPlayer() || attacker->GetGUID().IsPet())) && - !(attacker->IsPlayer() && !attacker->IsPvP() && !attacker->IsFFAPvP() && - (!bot->duel || bot->duel->Opponent != attacker)) && - (!c || - (!c->IsInEvadeMode() && - ((!isMemberBotGroup && botAI->HasStrategy("attack tagged", BOT_STATE_NON_COMBAT)) || leaderHasThreat || - (!c->hasLootRecipient() && - (!c->GetVictim() || - (c->GetVictim() && - ((!c->GetVictim()->IsPlayer() || bot->IsInSameGroupWith(c->GetVictim()->ToPlayer())) || - (botAI->GetMaster() && c->GetVictim() == botAI->GetMaster()))))) || - c->isTappedBy(bot)))); + return true; } bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) diff --git a/src/strategy/values/Formations.cpp b/src/strategy/values/Formations.cpp index 21c43f402a..30c738252b 100644 --- a/src/strategy/values/Formations.cpp +++ b/src/strategy/values/Formations.cpp @@ -62,7 +62,7 @@ class MeleeFormation : public FollowFormation public: MeleeFormation(PlayerbotAI* botAI) : FollowFormation(botAI, "melee") {} - std::string const GetTargetName() override { return "master target"; } + std::string const GetTargetName() override { return "group leader"; } }; class QueueFormation : public FollowFormation diff --git a/src/strategy/values/MasterTargetValue.cpp b/src/strategy/values/GroupLeaderValue.cpp similarity index 70% rename from src/strategy/values/MasterTargetValue.cpp rename to src/strategy/values/GroupLeaderValue.cpp index 6fddb6d99b..18ed007648 100644 --- a/src/strategy/values/MasterTargetValue.cpp +++ b/src/strategy/values/GroupLeaderValue.cpp @@ -3,8 +3,8 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#include "MasterTargetValue.h" +#include "GroupLeaderValue.h" #include "Playerbots.h" -Unit* MasterTargetValue::Calculate() { return botAI->GetGroupMaster(); } +Unit* GroupLeaderValue::Calculate() { return botAI->GetGroupLeader(); } diff --git a/src/strategy/values/MasterTargetValue.h b/src/strategy/values/GroupLeaderValue.h similarity index 57% rename from src/strategy/values/MasterTargetValue.h rename to src/strategy/values/GroupLeaderValue.h index a9f8f781df..a359620437 100644 --- a/src/strategy/values/MasterTargetValue.h +++ b/src/strategy/values/GroupLeaderValue.h @@ -3,18 +3,18 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_MASTERTARGETVALUE_H -#define _PLAYERBOT_MASTERTARGETVALUE_H +#ifndef _PLAYERBOT_GROUPLEADERVALUE_H +#define _PLAYERBOT_GROUPLEADERVALUE_H #include "Value.h" class PlayerbotAI; class Unit; -class MasterTargetValue : public UnitCalculatedValue +class GroupLeaderValue : public UnitCalculatedValue { public: - MasterTargetValue(PlayerbotAI* botAI, std::string const name = "master target") : UnitCalculatedValue(botAI, name) + GroupLeaderValue(PlayerbotAI* botAI, std::string const name = "group leader") : UnitCalculatedValue(botAI, name) { } diff --git a/src/strategy/values/GroupValues.cpp b/src/strategy/values/GroupValues.cpp index f9cb21028e..01049a7969 100644 --- a/src/strategy/values/GroupValues.cpp +++ b/src/strategy/values/GroupValues.cpp @@ -28,7 +28,7 @@ GuidVector GroupMembersValue::Calculate() bool IsFollowingPartyValue::Calculate() { - if (botAI->GetGroupMaster() == bot) + if (botAI->GetGroupLeader() == bot) return true; if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) @@ -39,15 +39,15 @@ bool IsFollowingPartyValue::Calculate() bool IsNearLeaderValue::Calculate() { - Player* groupMaster = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); - if (!groupMaster) + if (!groupLeader) return false; - if (groupMaster == bot) + if (groupLeader == bot) return true; - return sServerFacade->GetDistance2d(bot, botAI->GetGroupMaster()) < sPlayerbotAIConfig->sightDistance; + return sServerFacade->GetDistance2d(bot, botAI->GetGroupLeader()) < sPlayerbotAIConfig->sightDistance; } bool BoolANDValue::Calculate() @@ -154,8 +154,8 @@ bool GroupReadyValue::Calculate() // We only wait for members that are in range otherwise we might be waiting for bots stuck in dead loops // forever. - if (botAI->GetGroupMaster() && - sServerFacade->GetDistance2d(member, botAI->GetGroupMaster()) > sPlayerbotAIConfig->sightDistance) + if (botAI->GetGroupLeader() && + sServerFacade->GetDistance2d(member, botAI->GetGroupLeader()) > sPlayerbotAIConfig->sightDistance) continue; if (member->GetHealthPct() < sPlayerbotAIConfig->almostFullHealth) diff --git a/src/strategy/values/ValueContext.h b/src/strategy/values/ValueContext.h index b8e79d6e5a..adf03be8ea 100644 --- a/src/strategy/values/ValueContext.h +++ b/src/strategy/values/ValueContext.h @@ -30,6 +30,7 @@ #include "Formations.h" #include "GrindTargetValue.h" #include "GroupValues.h" +#include "GroupLeaderValue.h" #include "GuildValues.h" #include "HasAvailableLootValue.h" #include "HasTotemValue.h" @@ -51,7 +52,6 @@ #include "LootStrategyValue.h" #include "MaintenanceValues.h" #include "ManaSaveLevelValue.h" -#include "MasterTargetValue.h" #include "NearestAdsValue.h" #include "NearestCorpsesValue.h" #include "NearestFriendlyPlayersValue.h" @@ -130,7 +130,7 @@ class ValueContext : public NamedObjectContext creators["party member to resurrect"] = &ValueContext::party_member_to_resurrect; creators["current target"] = &ValueContext::current_target; creators["self target"] = &ValueContext::self_target; - creators["master target"] = &ValueContext::master; + creators["group leader"] = &ValueContext::group_leader; creators["line target"] = &ValueContext::line_target; creators["tank target"] = &ValueContext::tank_target; creators["dps target"] = &ValueContext::dps_target; @@ -439,7 +439,7 @@ class ValueContext : public NamedObjectContext static UntypedValue* current_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* old_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* self_target(PlayerbotAI* botAI) { return new SelfTargetValue(botAI); } - static UntypedValue* master(PlayerbotAI* botAI) { return new MasterTargetValue(botAI); } + static UntypedValue* group_leader(PlayerbotAI* botAI) { return new GroupLeaderValue(botAI); } static UntypedValue* line_target(PlayerbotAI* botAI) { return new LineTargetValue(botAI); } static UntypedValue* tank_target(PlayerbotAI* botAI) { return new TankTargetValue(botAI); } static UntypedValue* dps_target(PlayerbotAI* botAI) { return new DpsTargetValue(botAI); }