diff --git a/.github/workflows/check_pr_source.yml b/.github/workflows/check_pr_source.yml index 877ab85c7..5b354f6f7 100644 --- a/.github/workflows/check_pr_source.yml +++ b/.github/workflows/check_pr_source.yml @@ -1,13 +1,15 @@ -name: Enforce test-staging → main +name: Enforce test-staging → master on: pull_request: branches: - master + - test-staging jobs: require-test-staging: runs-on: ubuntu-22.04 + if: github.event.pull_request.base.ref == 'master' steps: - name: Ensure PR source is test-staging run: | diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 7b1688fb6..83b78c392 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -2,9 +2,9 @@ name: Codestyle on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "codestyle-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/core_build.yml b/.github/workflows/core_build.yml index 2927716f1..ac6ee60d8 100644 --- a/.github/workflows/core_build.yml +++ b/.github/workflows/core_build.yml @@ -2,9 +2,9 @@ name: ubuntu-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "core-build-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index 30a558c63..ab4d83daa 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -1,9 +1,9 @@ name: macos-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "macos-build-${{ github.event.pull_request.number }}" diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 49678c97a..121d97751 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -1,9 +1,9 @@ name: windows-build on: push: - branches: [ "master" ] + branches: [ "master", "test-staging" ] pull_request: - branches: [ "master" ] + branches: [ "master", "test-staging" ] concurrency: group: "windows-build-${{ github.event.pull_request.number }}" diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 907377316..a2f06885c 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -558,7 +558,7 @@ AiPlayerbot.AutoGearScoreLimit = 0 # "mana" (bots have infinite mana) # "power" (bots have infinite energy, rage, and runic power) # "taxi" (bots may use all flight paths, though they will not actually learn them) -# "raid" (bots use cheats implemented into raid strategies (currently only for Ulduar)) +# "raid" (bots use cheats implemented into raid strategies (currently only for SSC and Ulduar)) # To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,raid,taxi") # Default: food, taxi, and raid are enabled AiPlayerbot.BotCheats = "food,taxi,raid" @@ -2182,4 +2182,4 @@ AiPlayerbot.SummonAtInnkeepersEnabled = 1 # 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for non tank bots, increased threat for tank bots. # Buffs will be applied on PP, Sindragosa and Lich King -AiPlayerbot.EnableICCBuffs = 1 +AiPlayerbot.EnableICCBuffs = 1 \ No newline at end of file diff --git a/src/Ai/Base/Actions/EquipAction.cpp b/src/Ai/Base/Actions/EquipAction.cpp index 32508ef2e..a8b262692 100644 --- a/src/Ai/Base/Actions/EquipAction.cpp +++ b/src/Ai/Base/Actions/EquipAction.cpp @@ -328,7 +328,43 @@ void EquipAction::EquipItem(Item* item) botAI->TellMaster(out); } -bool EquipUpgradesAction::Execute(Event event) +ItemIds EquipAction::SelectInventoryItemsToEquip() +{ + CollectItemsVisitor visitor; + IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); + + ItemIds items; + for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) + { + Item* item = *i; + if (!item) + continue; + + ItemTemplate const* itemTemplate = item->GetTemplate(); + if (!itemTemplate) + continue; + + //TODO Expand to Glyphs and Gems, that can be placed in equipment + //Pre-filter non-equipable items + if (itemTemplate->InventoryType == INVTYPE_NON_EQUIP) + continue; + + int32 randomProperty = item->GetItemRandomPropertyId(); + uint32 itemId = item->GetTemplate()->ItemId; + std::string itemUsageParam; + if (randomProperty != 0) + itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); + else + itemUsageParam = std::to_string(itemId); + + ItemUsage usage = AI_VALUE2(ItemUsage, "item upgrade", itemUsageParam); + if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) + items.insert(itemId); + } + return items; +} + +bool EquipUpgradesTriggeredAction::Execute(Event event) { if (!sPlayerbotAIConfig.autoEquipUpgradeLoot && !sRandomPlayerbotMgr.IsRandomBot(bot)) return false; @@ -361,72 +397,18 @@ bool EquipUpgradesAction::Execute(Event event) p >> itemId; ItemTemplate const* item = sObjectMgr->GetItemTemplate(itemId); - if (item->Class == ITEM_CLASS_TRADE_GOODS && item->SubClass == ITEM_SUBCLASS_MEAT) + if (item->InventoryType == INVTYPE_NON_EQUIP) return false; } - CollectItemsVisitor visitor; - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - - ItemIds items; - for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) - { - Item* item = *i; - if (!item) - break; - int32 randomProperty = item->GetItemRandomPropertyId(); - uint32 itemId = item->GetTemplate()->ItemId; - std::string itemUsageParam; - if (randomProperty != 0) - { - itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } - else - { - itemUsageParam = std::to_string(itemId); - } - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); - - if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { - items.insert(itemId); - } - } - + ItemIds items = SelectInventoryItemsToEquip(); EquipItems(items); return true; } bool EquipUpgradeAction::Execute(Event event) { - CollectItemsVisitor visitor; - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - - ItemIds items; - for (auto i = visitor.items.begin(); i != visitor.items.end(); ++i) - { - Item* item = *i; - if (!item) - break; - int32 randomProperty = item->GetItemRandomPropertyId(); - uint32 itemId = item->GetTemplate()->ItemId; - std::string itemUsageParam; - if (randomProperty != 0) - { - itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } - else - { - itemUsageParam = std::to_string(itemId); - } - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); - - if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { - items.insert(itemId); - } - } - + ItemIds items = SelectInventoryItemsToEquip(); EquipItems(items); return true; } diff --git a/src/Ai/Base/Actions/EquipAction.h b/src/Ai/Base/Actions/EquipAction.h index 518d39c8e..4f84f942b 100644 --- a/src/Ai/Base/Actions/EquipAction.h +++ b/src/Ai/Base/Actions/EquipAction.h @@ -8,6 +8,7 @@ #include "ChatHelper.h" #include "InventoryAction.h" +#include "Item.h" class FindItemVisitor; class Item; @@ -20,6 +21,7 @@ public: bool Execute(Event event) override; void EquipItems(ItemIds ids); + ItemIds SelectInventoryItemsToEquip(); private: void EquipItem(FindItemVisitor* visitor); @@ -27,10 +29,10 @@ private: void EquipItem(Item* item); }; -class EquipUpgradesAction : public EquipAction +class EquipUpgradesTriggeredAction : public EquipAction { public: - EquipUpgradesAction(PlayerbotAI* botAI, std::string const name = "equip upgrades") : EquipAction(botAI, name) {} + explicit EquipUpgradesTriggeredAction(PlayerbotAI* botAI, std::string const name = "equip upgrades") : EquipAction(botAI, name) {} bool Execute(Event event) override; }; @@ -38,7 +40,7 @@ public: class EquipUpgradeAction : public EquipAction { public: - EquipUpgradeAction(PlayerbotAI* botAI, std::string const name = "equip upgrade") : EquipAction(botAI, name) {} + explicit EquipUpgradeAction(PlayerbotAI* botAI, std::string const name = "equip upgrade") : EquipAction(botAI, name) {} bool Execute(Event event) override; }; diff --git a/src/Ai/Base/Actions/LootRollAction.cpp b/src/Ai/Base/Actions/LootRollAction.cpp index e600f1f20..9a21c8139 100644 --- a/src/Ai/Base/Actions/LootRollAction.cpp +++ b/src/Ai/Base/Actions/LootRollAction.cpp @@ -90,6 +90,8 @@ bool LootRollAction::Execute(Event event) } else if (sPlayerbotAIConfig.lootRollLevel == 1) { + // Level 1 = "greed" mode: bots greed on useful items but never need + // Only downgrade NEED to GREED, preserve GREED votes as-is if (vote == NEED) { if (RollUniqueCheck(proto, bot)) @@ -101,10 +103,6 @@ bool LootRollAction::Execute(Event event) vote = GREED; } } - else if (vote == GREED) - { - vote = PASS; - } } switch (group->GetLootMethod()) { diff --git a/src/Ai/Base/Actions/UseMeetingStoneAction.h b/src/Ai/Base/Actions/UseMeetingStoneAction.h index ff1282284..fd2a2c5f2 100644 --- a/src/Ai/Base/Actions/UseMeetingStoneAction.h +++ b/src/Ai/Base/Actions/UseMeetingStoneAction.h @@ -17,9 +17,9 @@ public: SummonAction(PlayerbotAI* botAI, std::string const name = "summon") : MovementAction(botAI, name) {} bool Execute(Event event) override; + bool Teleport(Player* summoner, Player* player, bool preserveAuras); protected: - bool Teleport(Player* summoner, Player* player, bool preserveAuras); bool SummonUsingGos(Player* summoner, Player* player, bool preserveAuras); bool SummonUsingNpcs(Player* summoner, Player* player, bool preserveAuras); }; diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 5e215e123..8e4ed9ccf 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -120,7 +120,7 @@ public: creators["use"] = &ChatActionContext::use; creators["item count"] = &ChatActionContext::item_count; creators["equip"] = &ChatActionContext::equip; - creators["equip upgrades"] = &ChatActionContext::equip_upgrades; + creators["equip upgrades"] = &ChatActionContext::equip_upgrade; creators["unequip"] = &ChatActionContext::unequip; creators["sell"] = &ChatActionContext::sell; creators["buy"] = &ChatActionContext::buy; @@ -258,7 +258,6 @@ private: static Action* talents(PlayerbotAI* botAI) { return new ChangeTalentsAction(botAI); } static Action* equip(PlayerbotAI* botAI) { return new EquipAction(botAI); } - static Action* equip_upgrades(PlayerbotAI* botAI) { return new EquipUpgradesAction(botAI); } static Action* unequip(PlayerbotAI* botAI) { return new UnequipAction(botAI); } static Action* sell(PlayerbotAI* botAI) { return new SellAction(botAI); } static Action* buy(PlayerbotAI* botAI) { return new BuyAction(botAI); } diff --git a/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp b/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp index cb37d2717..2debc44cb 100644 --- a/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp @@ -42,6 +42,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger NextAction("query item usage", relevance), NextAction("equip upgrades", relevance) })); triggers.push_back(new TriggerNode("item push result", { NextAction("quest item push result", relevance) })); + triggers.push_back(new TriggerNode("loot roll won", { NextAction("equip upgrades", relevance) })); triggers.push_back(new TriggerNode("ready check finished", { NextAction("finish ready check", relevance) })); // triggers.push_back(new TriggerNode("often", { NextAction("security check", relevance), NextAction("check mail", relevance) })); triggers.push_back(new TriggerNode("guild invite", { NextAction("guild accept", relevance) })); diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 6f48fa973..c6183e30f 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -19,19 +19,9 @@ ItemUsage ItemUsageValue::Calculate() { - uint32 itemId = 0; - uint32 randomPropertyId = 0; - size_t pos = qualifier.find(","); - if (pos != std::string::npos) - { - itemId = atoi(qualifier.substr(0, pos).c_str()); - randomPropertyId = atoi(qualifier.substr(pos + 1).c_str()); - } - else - { - itemId = atoi(qualifier.c_str()); - } - + ParsedItemUsage const parsed = GetItemIdFromQualifier(); + uint32 itemId = parsed.itemId; + uint32 randomPropertyId = parsed.randomPropertyId; if (!itemId) return ITEM_USAGE_NONE; @@ -142,96 +132,30 @@ ItemUsage ItemUsageValue::Calculate() // If the loot is from an item in the bot’s bags, ignore syncQuestWithPlayer if (isLootFromItem && botNeedsItemForQuest) - { return ITEM_USAGE_QUEST; - } // If the bot is NOT acting alone and the master needs this quest item, defer to the master if (!isSelfBot && masterNeedsItemForQuest) - { return ITEM_USAGE_NONE; - } // If the bot itself needs the item for a quest, allow looting if (botNeedsItemForQuest) - { return ITEM_USAGE_QUEST; - } if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) { - if (bot->getClass() == CLASS_HUNTER || bot->getClass() == CLASS_ROGUE || bot->getClass() == CLASS_WARRIOR) - { - Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); - uint32 requiredSubClass = 0; - - if (rangedWeapon) - { - switch (rangedWeapon->GetTemplate()->SubClass) - { - case ITEM_SUBCLASS_WEAPON_GUN: - requiredSubClass = ITEM_SUBCLASS_BULLET; - break; - case ITEM_SUBCLASS_WEAPON_BOW: - case ITEM_SUBCLASS_WEAPON_CROSSBOW: - requiredSubClass = ITEM_SUBCLASS_ARROW; - break; - } - } - - // Ensure the item is the correct ammo type for the equipped ranged weapon - if (proto->SubClass == requiredSubClass) - { - float ammoCount = BetterStacks(proto, "ammo"); - float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2 - uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID); - - // Check if the bot has an ammo type assigned - if (currentAmmoId == 0) - { - return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo - } - // Compare new ammo vs current equipped ammo - ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId); - if (currentAmmoProto) - { - uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2; - uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2; - - if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition - { - return ITEM_USAGE_EQUIP; - } - if (newAmmoDPS < currentAmmoDPS) // New ammo is worse - { - return ITEM_USAGE_NONE; - } - } - // Ensure we have enough ammo in the inventory - if (ammoCount < requiredAmmo) - { - ammoCount += CurrentStacks(proto); - - if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply - return ITEM_USAGE_AMMO; - else if (ammoCount < requiredAmmo + 1) - return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much. - } - } - } + ItemUsage ammoUsage = QueryItemUsageForAmmo(proto); + if (ammoUsage != ITEM_USAGE_NONE) + return ammoUsage; } - // Need to add something like free bagspace or item value. if (proto->SellPrice > 0) { if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) - { return ITEM_USAGE_AH; - } + else - { return ITEM_USAGE_VENDOR; - } } return ITEM_USAGE_NONE; @@ -480,6 +404,80 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, return ITEM_USAGE_NONE; } +ItemUsage ItemUsageValue::QueryItemUsageForAmmo(ItemTemplate const* proto) +{ + if (bot->getClass() != CLASS_HUNTER || bot->getClass() != CLASS_ROGUE || bot->getClass() != CLASS_WARRIOR) + return ITEM_USAGE_NONE; + + Item* rangedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + uint32 requiredSubClass = 0; + + if (rangedWeapon) + { + switch (rangedWeapon->GetTemplate()->SubClass) + { + case ITEM_SUBCLASS_WEAPON_GUN: + requiredSubClass = ITEM_SUBCLASS_BULLET; + break; + case ITEM_SUBCLASS_WEAPON_BOW: + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + requiredSubClass = ITEM_SUBCLASS_ARROW; + break; + } + } + + // Ensure the item is the correct ammo type for the equipped ranged weapon + if (proto->SubClass == requiredSubClass) + { + float ammoCount = BetterStacks(proto, "ammo"); + float requiredAmmo = (bot->getClass() == CLASS_HUNTER) ? 8 : 2; // Hunters get 8 stacks, others 2 + uint32 currentAmmoId = bot->GetUInt32Value(PLAYER_AMMO_ID); + + // Check if the bot has an ammo type assigned + if (currentAmmoId == 0) + return ITEM_USAGE_EQUIP; // Equip the ammo if no ammo + // Compare new ammo vs current equipped ammo + ItemTemplate const* currentAmmoProto = sObjectMgr->GetItemTemplate(currentAmmoId); + if (currentAmmoProto) + { + uint32 currentAmmoDPS = (currentAmmoProto->Damage[0].DamageMin + currentAmmoProto->Damage[0].DamageMax) * 1000 / 2; + uint32 newAmmoDPS = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2; + + if (newAmmoDPS > currentAmmoDPS) // New ammo meets upgrade condition + return ITEM_USAGE_EQUIP; + + if (newAmmoDPS < currentAmmoDPS) // New ammo is worse + return ITEM_USAGE_NONE; + } + // Ensure we have enough ammo in the inventory + if (ammoCount < requiredAmmo) + { + ammoCount += CurrentStacks(proto); + + if (ammoCount < requiredAmmo) // Buy ammo to reach the proper supply + return ITEM_USAGE_AMMO; + else if (ammoCount < requiredAmmo + 1) + return ITEM_USAGE_KEEP; // Keep the ammo if we don't have too much. + } + } + return ITEM_USAGE_NONE; +} + +ParsedItemUsage ItemUsageValue::GetItemIdFromQualifier() +{ + ParsedItemUsage parsed; + + size_t const pos = qualifier.find(","); + if (pos != std::string::npos) + { + parsed.itemId = atoi(qualifier.substr(0, pos).c_str()); + parsed.randomPropertyId = atoi(qualifier.substr(pos + 1).c_str()); + return parsed; + } + else + parsed.itemId = atoi(qualifier.c_str()); + return parsed; +} // Return smaltest bag size equipped uint32 ItemUsageValue::GetSmallestBagSize() { @@ -913,3 +911,25 @@ std::string const ItemUsageValue::GetConsumableType(ItemTemplate const* proto, b return ""; } + +ItemUsage ItemUpgradeValue::Calculate() +{ + ParsedItemUsage parsed = GetItemIdFromQualifier(); + uint32 itemId = parsed.itemId; + uint32 randomPropertyId = parsed.randomPropertyId; + if (!itemId) + return ITEM_USAGE_NONE; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + return ITEM_USAGE_NONE; + + ItemUsage equip = QueryItemUsageForEquip(proto, randomPropertyId); + if (equip != ITEM_USAGE_NONE) + return equip; + + if (proto->Class == ITEM_CLASS_PROJECTILE && bot->CanUseItem(proto) == EQUIP_ERR_OK) + return QueryItemUsageForAmmo(proto); + + return ITEM_USAGE_NONE; +} diff --git a/src/Ai/Base/Value/ItemUsageValue.h b/src/Ai/Base/Value/ItemUsageValue.h index d50ea1b06..b759bab6a 100644 --- a/src/Ai/Base/Value/ItemUsageValue.h +++ b/src/Ai/Base/Value/ItemUsageValue.h @@ -14,7 +14,11 @@ class Player; class PlayerbotAI; struct ItemTemplate; - +struct ParsedItemUsage +{ + uint32 itemId = 0; + int32 randomPropertyId = 0; +}; enum ItemUsage : uint32 { ITEM_USAGE_NONE = 0, @@ -42,8 +46,12 @@ public: ItemUsage Calculate() override; -private: +protected: ItemUsage QueryItemUsageForEquip(ItemTemplate const* proto, int32 randomPropertyId = 0); + ItemUsage QueryItemUsageForAmmo(ItemTemplate const* proto); + ParsedItemUsage GetItemIdFromQualifier(); + +private: uint32 GetSmallestBagSize(); bool IsItemUsefulForQuest(Player* player, ItemTemplate const* proto); bool IsItemNeededForSkill(ItemTemplate const* proto); @@ -61,4 +69,14 @@ public: static std::string const GetConsumableType(ItemTemplate const* proto, bool hasMana); }; +class ItemUpgradeValue : public ItemUsageValue +{ +public: + ItemUpgradeValue(PlayerbotAI* botAI, std::string const name = "item upgrade") : ItemUsageValue(botAI, name) + { + } + + ItemUsage Calculate() override; +}; + #endif diff --git a/src/Ai/Base/Value/QuestValues.cpp b/src/Ai/Base/Value/QuestValues.cpp index 172477850..f556f443d 100644 --- a/src/Ai/Base/Value/QuestValues.cpp +++ b/src/Ai/Base/Value/QuestValues.cpp @@ -173,7 +173,7 @@ std::vector ActiveQuestGiversValue::Calculate() continue; } - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestGivers.push_back(guidp); @@ -231,7 +231,7 @@ std::vector ActiveQuestTakersValue::Calculate() for (auto& guidp : entry.second) { - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestTakers.push_back(guidp); @@ -298,7 +298,7 @@ std::vector ActiveQuestObjectivesValue::Calculate() { for (auto& guidp : entry.second) { - if (guidp.isDead()) + if (!guidp.IsCreatureOrGOAccessible()) continue; retQuestObjectives.push_back(guidp); diff --git a/src/Ai/Base/ValueContext.h b/src/Ai/Base/ValueContext.h index ab93bcea4..b67aee9bb 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -216,6 +216,7 @@ public: creators["formation"] = &ValueContext::formation; creators["stance"] = &ValueContext::stance; creators["item usage"] = &ValueContext::item_usage; + creators["item upgrade"] = &ValueContext::item_upgrade; creators["speed"] = &ValueContext::speed; creators["last said"] = &ValueContext::last_said; creators["last emote"] = &ValueContext::last_emote; @@ -341,6 +342,7 @@ private: static UntypedValue* already_seen_players(PlayerbotAI* botAI) { return new AlreadySeenPlayersValue(botAI); } static UntypedValue* new_player_nearby(PlayerbotAI* botAI) { return new NewPlayerNearbyValue(botAI); } static UntypedValue* item_usage(PlayerbotAI* botAI) { return new ItemUsageValue(botAI); } + static UntypedValue* item_upgrade(PlayerbotAI* botAI) { return new ItemUpgradeValue(botAI); } static UntypedValue* formation(PlayerbotAI* botAI) { return new FormationValue(botAI); } static UntypedValue* stance(PlayerbotAI* botAI) { return new StanceValue(botAI); } static UntypedValue* mana_save_level(PlayerbotAI* botAI) { return new ManaSaveLevelValue(botAI); } diff --git a/src/Ai/Base/WorldPacketTriggerContext.h b/src/Ai/Base/WorldPacketTriggerContext.h index 1e7309524..62afafd1a 100644 --- a/src/Ai/Base/WorldPacketTriggerContext.h +++ b/src/Ai/Base/WorldPacketTriggerContext.h @@ -46,6 +46,7 @@ public: creators["questgiver quest details"] = &WorldPacketTriggerContext::questgiver_quest_details; creators["item push result"] = &WorldPacketTriggerContext::item_push_result; + creators["loot roll won"] = &WorldPacketTriggerContext::loot_roll_won; creators["party command"] = &WorldPacketTriggerContext::party_command; creators["taxi done"] = &WorldPacketTriggerContext::taxi_done; creators["cast failed"] = &WorldPacketTriggerContext::cast_failed; @@ -92,6 +93,7 @@ private: static Trigger* taxi_done(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "taxi done"); } static Trigger* party_command(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "party command"); } static Trigger* item_push_result(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "item push result"); } + static Trigger* loot_roll_won(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "loot roll won"); } // quest static Trigger* quest_update_add_kill(PlayerbotAI* ai) { return new WorldPacketTrigger(ai, "quest update add kill"); } diff --git a/src/Ai/Class/Warlock/Action/WarlockActions.h b/src/Ai/Class/Warlock/Action/WarlockActions.h index 787b518d6..09f346370 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/src/Ai/Class/Warlock/Action/WarlockActions.h @@ -41,6 +41,18 @@ public: std::string const GetTargetName() override { return "pet target"; } }; +class CastUnendingBreathAction : public CastBuffSpellAction +{ +public: + CastUnendingBreathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "unending breath") {} +}; + +class CastUnendingBreathOnPartyAction : public BuffOnPartyAction +{ +public: + CastUnendingBreathOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "unending breath") {} +}; + class CreateSoulShardAction : public Action { public: diff --git a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp index a78ff0a98..d22abe3ca 100644 --- a/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp +++ b/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp @@ -85,6 +85,8 @@ void GenericWarlockNonCombatStrategy::InitTriggers(std::vector& tr triggers.push_back(new TriggerNode("too many soul shards", { NextAction("destroy soul shard", 60.0f) })); triggers.push_back(new TriggerNode("soul link", { NextAction("soul link", 28.0f) })); triggers.push_back(new TriggerNode("demon armor", { NextAction("fel armor", 27.0f) })); + triggers.push_back(new TriggerNode("unending breath", { NextAction("unending breath", 12.0f) })); + triggers.push_back(new TriggerNode("unending breath on party", { NextAction("unending breath on party", 11.0f) })); triggers.push_back(new TriggerNode("no healthstone", { NextAction("create healthstone", 26.0f) })); triggers.push_back(new TriggerNode("no soulstone", { NextAction("create soulstone", 25.0f) })); triggers.push_back(new TriggerNode("life tap", { NextAction("life tap", 23.0f) })); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp index 53e77669b..1df531e7f 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp @@ -79,6 +79,16 @@ bool SoulLinkTrigger::IsActive() return !botAI->HasAura("soul link", target); } +bool UnendingBreathTrigger::IsActive() +{ + return BuffTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + +bool UnendingBreathOnPartyTrigger::IsActive() +{ + return BuffOnPartyTrigger::IsActive() && AI_VALUE2(bool, "swimming", "self target"); +} + bool DemonicEmpowermentTrigger::IsActive() { Pet* pet = bot->GetPet(); diff --git a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h index 851254901..d66fe537e 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h @@ -32,6 +32,20 @@ public: bool IsActive() override; }; +class UnendingBreathTrigger : public BuffTrigger +{ +public: + UnendingBreathTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "unending breath", 5 * 2000) {} + bool IsActive() override; +}; + +class UnendingBreathOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + UnendingBreathOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "unending breath on party", 2 * 2000) {} + bool IsActive() override; +}; + class OutOfSoulShardsTrigger : public Trigger { public: diff --git a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp index a70327b6c..27b9166c6 100644 --- a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp +++ b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp @@ -143,6 +143,8 @@ public: creators["shadow trance"] = &WarlockTriggerFactoryInternal::shadow_trance; creators["demon armor"] = &WarlockTriggerFactoryInternal::demon_armor; creators["soul link"] = &WarlockTriggerFactoryInternal::soul_link; + creators["unending breath"] = &WarlockTriggerFactoryInternal::unending_breath; + creators["unending breath on party"] = &WarlockTriggerFactoryInternal::unending_breath_on_party; creators["no soul shard"] = &WarlockTriggerFactoryInternal::no_soul_shard; creators["too many soul shards"] = &WarlockTriggerFactoryInternal::too_many_soul_shards; creators["no healthstone"] = &WarlockTriggerFactoryInternal::HasHealthstone; @@ -189,6 +191,8 @@ private: static Trigger* shadow_trance(PlayerbotAI* botAI) { return new ShadowTranceTrigger(botAI); } static Trigger* demon_armor(PlayerbotAI* botAI) { return new DemonArmorTrigger(botAI); } static Trigger* soul_link(PlayerbotAI* botAI) { return new SoulLinkTrigger(botAI); } + static Trigger* unending_breath(PlayerbotAI* botAI) { return new UnendingBreathTrigger(botAI); } + static Trigger* unending_breath_on_party(PlayerbotAI* botAI) { return new UnendingBreathOnPartyTrigger(botAI); } static Trigger* no_soul_shard(PlayerbotAI* botAI) { return new OutOfSoulShardsTrigger(botAI); } static Trigger* too_many_soul_shards(PlayerbotAI* botAI) { return new TooManySoulShardsTrigger(botAI); } static Trigger* HasHealthstone(PlayerbotAI* botAI) { return new HasHealthstoneTrigger(botAI); } @@ -240,6 +244,8 @@ public: creators["demon armor"] = &WarlockAiObjectContextInternal::demon_armor; creators["demon skin"] = &WarlockAiObjectContextInternal::demon_skin; creators["soul link"] = &WarlockAiObjectContextInternal::soul_link; + creators["unending breath"] = &WarlockAiObjectContextInternal::unending_breath; + creators["unending breath on party"] = &WarlockAiObjectContextInternal::unending_breath_on_party; creators["create soul shard"] = &WarlockAiObjectContextInternal::create_soul_shard; creators["destroy soul shard"] = &WarlockAiObjectContextInternal::destroy_soul_shard; creators["create healthstone"] = &WarlockAiObjectContextInternal::create_healthstone; @@ -313,6 +319,8 @@ private: static Action* demon_armor(PlayerbotAI* botAI) { return new CastDemonArmorAction(botAI); } static Action* demon_skin(PlayerbotAI* botAI) { return new CastDemonSkinAction(botAI); } static Action* soul_link(PlayerbotAI* botAI) { return new CastSoulLinkAction(botAI); } + static Action* unending_breath(PlayerbotAI* botAI) { return new CastUnendingBreathAction(botAI); } + static Action* unending_breath_on_party(PlayerbotAI* botAI) { return new CastUnendingBreathOnPartyAction(botAI); } static Action* create_soul_shard(PlayerbotAI* botAI) { return new CreateSoulShardAction(botAI); } static Action* destroy_soul_shard(PlayerbotAI* botAI) { return new DestroySoulShardAction(botAI); } static Action* create_healthstone(PlayerbotAI* botAI) { return new CastCreateHealthstoneAction(botAI); } diff --git a/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp b/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp index 414acc4bc..bbb94d110 100644 --- a/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp +++ b/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp @@ -62,7 +62,7 @@ bool MountDrakeAction::Execute(Event event) break; } - std::vector players = botAI->GetPlayersInGroup(); + std::vector players = botAI->GetAllPlayersInGroup(); for (Player* player : players) { if (!player || !player->IsInWorld() || player->IsDuringRemoveFromWorld()) diff --git a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp index 1a98135ca..2af82ebe9 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp @@ -2,6 +2,7 @@ #include "RaidGruulsLairHelpers.h" #include "CreatureAI.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" #include "Unit.h" using namespace GruulsLairHelpers; @@ -12,6 +13,8 @@ using namespace GruulsLairHelpers; bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; MarkTargetWithSquare(bot, maulgar); SetRtiTarget(botAI, "square", maulgar); @@ -21,31 +24,20 @@ bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) if (maulgar->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::MaulgarTankPosition; + const Position& position = MAULGAR_TANK_POSITION; const float maxDistance = 3.0f; - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceToTankPosition > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); } - - float orientation = atan2(maulgar->GetPositionY() - bot->GetPositionY(), - maulgar->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(maulgar)) - { - return MoveTo(maulgar->GetMapId(), maulgar->GetPositionX(), maulgar->GetPositionY(), - maulgar->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -55,6 +47,8 @@ bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event event) bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) { Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); + if (!olm) + return false; MarkTargetWithCircle(bot, olm); SetRtiTarget(botAI, "circle", olm); @@ -64,29 +58,22 @@ bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) if (olm->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::OlmTankPosition; + const Position& position = OLM_TANK_POSITION; const float maxDistance = 3.0f; const float olmTankLeeway = 30.0f; - float distanceOlmToTankPosition = olm->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceOlmToPosition = olm->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceOlmToTankPosition > olmTankLeeway) + if (distanceOlmToPosition > olmTankLeeway) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceOlmToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceOlmToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } - else if (!bot->IsWithinMeleeRange(olm)) - { - return MoveTo(olm->GetMapId(), olm->GetPositionX(), olm->GetPositionY(), - olm->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } return false; } @@ -95,6 +82,8 @@ bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event event) bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) { Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + if (!blindeye) + return false; MarkTargetWithStar(bot, blindeye); SetRtiTarget(botAI, "star", blindeye); @@ -104,31 +93,20 @@ bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) if (blindeye->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::BlindeyeTankPosition; + const Position& position = BLINDEYE_TANK_POSITION; const float maxDistance = 3.0f; - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distanceToTankPosition > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, tankPosition.z, false, false, false, false, + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - - float orientation = atan2(blindeye->GetPositionY() - bot->GetPositionY(), - blindeye->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(blindeye)) - { - return MoveTo(blindeye->GetMapId(), blindeye->GetPositionX(), blindeye->GetPositionY(), - blindeye->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -138,6 +116,8 @@ bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event event) bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + if (!krosh) + return false; MarkTargetWithTriangle(bot, krosh); SetRtiTarget(botAI, "triangle", krosh); @@ -149,25 +129,22 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) return botAI->CastSpell("fire ward", bot); if (bot->GetTarget() != krosh->GetGUID()) - { - bot->SetSelection(krosh->GetGUID()); - return true; - } + return Attack(krosh); if (krosh->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::KroshTankPosition; - float distanceToKrosh = krosh->GetExactDist2d(tankPosition.x, tankPosition.y); + const Position& position = KROSH_TANK_POSITION; + float distanceToKrosh = krosh->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); const float minDistance = 16.0f; const float maxDistance = 29.0f; const float tankPositionLeeway = 1.0f; if (distanceToKrosh > minDistance && distanceToKrosh < maxDistance) { - if (!bot->IsWithinDist2d(tankPosition.x, tankPosition.y, tankPositionLeeway)) + if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), tankPositionLeeway)) { - return MoveTo(bot->GetMapId(), tankPosition.x, tankPosition.y, tankPosition.z, false, - false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + return MoveTo(GRUULS_LAIR_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } float orientation = atan2(krosh->GetPositionY() - bot->GetPositionY(), @@ -179,7 +156,7 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { - return MoveTo(krosh->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -192,20 +169,19 @@ bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event event) bool HighKingMaulgarMoonkinTankAttackKigglerAction::Execute(Event event) { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); + if (!kiggler) + return false; MarkTargetWithDiamond(bot, kiggler); SetRtiTarget(botAI, "diamond", kiggler); if (bot->GetTarget() != kiggler->GetGUID()) - { - bot->SetSelection(kiggler->GetGUID()); - return true; - } + return Attack(kiggler); Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { - return MoveTo(kiggler->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -216,120 +192,105 @@ bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event event) { // Target priority 1: Blindeye Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - if (blindeye && blindeye->IsAlive()) + if (blindeye) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(blindeye->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "star", blindeye); if (bot->GetTarget() != blindeye->GetGUID()) - { - bot->SetSelection(blindeye->GetGUID()); return Attack(blindeye); - } return false; } // Target priority 2: Olm Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - if (olm && olm->IsAlive()) + if (olm) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(olm->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "circle", olm); if (bot->GetTarget() != olm->GetGUID()) - { - bot->SetSelection(olm->GetGUID()); return Attack(olm); - } return false; } // Target priority 3a: Krosh (ranged only) Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - if (krosh && krosh->IsAlive() && botAI->IsRanged(bot)) + if (krosh && botAI->IsRanged(bot)) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(krosh->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "triangle", krosh); if (bot->GetTarget() != krosh->GetGUID()) - { - bot->SetSelection(krosh->GetGUID()); return Attack(krosh); - } return false; } // Target priority 3b: Kiggler Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - if (kiggler && kiggler->IsAlive()) + if (kiggler) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(kiggler->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "diamond", kiggler); if (bot->GetTarget() != kiggler->GetGUID()) - { - bot->SetSelection(kiggler->GetGUID()); return Attack(kiggler); - } return false; } // Target priority 4: Maulgar Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - if (maulgar && maulgar->IsAlive()) + if (maulgar) { Position safePos; if (TryGetNewSafePosition(botAI, bot, safePos)) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(maulgar->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } SetRtiTarget(botAI, "square", maulgar); if (bot->GetTarget() != maulgar->GetGUID()) - { - bot->SetSelection(maulgar->GetGUID()); return Attack(maulgar); - } } return false; @@ -338,22 +299,22 @@ bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event event) // Avoid Whirlwind and Blast Wave and generally try to stay near the center of the room bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) { - const Location& fightCenter = GruulsLairLocations::MaulgarRoomCenter; - const float maxDistanceFromFight = 50.0f; - float distToFight = bot->GetExactDist2d(fightCenter.x, fightCenter.y); + const Position& center = MAULGAR_ROOM_CENTER; + const float maxDistanceFromCenter = 50.0f; + float distToCenter = bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()); - if (distToFight > maxDistanceFromFight) + if (distToCenter > maxDistanceFromCenter) { - float angle = atan2(bot->GetPositionY() - fightCenter.y, bot->GetPositionX() - fightCenter.x); - float destX = fightCenter.x + 40.0f * cos(angle); - float destY = fightCenter.y + 40.0f * sin(angle); - float destZ = fightCenter.z; + float angle = atan2(bot->GetPositionY() - center.GetPositionY(), bot->GetPositionX() - center.GetPositionX()); + float destX = center.GetPositionX() + 40.0f * cos(angle); + float destY = center.GetPositionY() + 40.0f * sin(angle); + float destZ = center.GetPositionZ(); if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) return false; - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -362,7 +323,7 @@ bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), safePos.m_positionX, safePos.m_positionY, safePos.m_positionZ, + return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -373,6 +334,8 @@ bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event event) bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event event) { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; const float safeDistance = 10.0f; float distance = bot->GetExactDist2d(maulgar); @@ -395,7 +358,7 @@ bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(maulgar->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -439,7 +402,7 @@ bool HighKingMaulgarBanishFelstalkerAction::Execute(Event event) if (warlockIndex >= 0 && warlockIndex < felStalkers.size()) { Unit* assignedFelStalker = felStalkers[warlockIndex]; - if (!assignedFelStalker->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedFelStalker, true)) + if (!botAI->HasAura("banish", assignedFelStalker) && botAI->CanCastSpell("banish", assignedFelStalker)) return botAI->CastSpell("banish", assignedFelStalker); } @@ -528,40 +491,33 @@ bool HighKingMaulgarMisdirectOlmAndBlindeyeAction::Execute(Event event) // Gruul the Dragonkiller Actions // Position in center of the room -bool GruulTheDragonkillerMainTankPositionBossAction::Execute(Event event) +bool GruulTheDragonkillerTanksPositionBossAction::Execute(Event event) { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul) + return false; if (bot->GetVictim() != gruul) return Attack(gruul); if (gruul->GetVictim() == bot) { - const Location& tankPosition = GruulsLairLocations::GruulTankPosition; - const float maxDistance = 3.0f; + const Position& position = GRUUL_TANK_POSITION; + const float maxDistance = 5.0f; - float dX = tankPosition.x - bot->GetPositionX(); - float dY = tankPosition.y - bot->GetPositionY(); - float distanceToTankPosition = bot->GetExactDist2d(tankPosition.x, tankPosition.y); + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distanceToTankPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); if (distanceToTankPosition > maxDistance) { float step = std::min(maxDistance, distanceToTankPosition); float moveX = bot->GetPositionX() + (dX / distanceToTankPosition) * maxDistance; float moveY = bot->GetPositionY() + (dY / distanceToTankPosition) * maxDistance; - const float moveZ = tankPosition.z; - return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + const float moveZ = position.GetPositionZ(); + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - - float orientation = atan2(gruul->GetPositionY() - bot->GetPositionY(), - gruul->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else if (!bot->IsWithinMeleeRange(gruul)) - { - return MoveTo(gruul->GetMapId(), gruul->GetPositionX(), gruul->GetPositionY(), gruul->GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -579,16 +535,16 @@ bool GruulTheDragonkillerSpreadRangedAction::Execute(Event event) static std::unordered_map hasReachedInitialPosition; Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (gruul && gruul->IsAlive() && gruul->GetHealth() == gruul->GetMaxHealth()) + if (gruul && gruul->GetHealth() == gruul->GetMaxHealth()) { - initialPositions.clear(); - hasReachedInitialPosition.clear(); + initialPositions.erase(bot->GetGUID()); + hasReachedInitialPosition.erase(bot->GetGUID()); } - const Location& tankPosition = GruulsLairLocations::GruulTankPosition; - const float centerX = tankPosition.x; - const float centerY = tankPosition.y; - float centerZ = bot->GetPositionZ(); + const Position& position = GRUUL_TANK_POSITION; + const float centerX = position.GetPositionX(); + const float centerY = position.GetPositionY(); + const float centerZ = position.GetPositionZ(); const float minRadius = 25.0f; const float maxRadius = 40.0f; @@ -642,7 +598,7 @@ bool GruulTheDragonkillerSpreadRangedAction::Execute(Event event) bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) return false; - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } diff --git a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h index 6faf7ed3e..525e0ee6d 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h @@ -85,10 +85,10 @@ public: bool Execute(Event event) override; }; -class GruulTheDragonkillerMainTankPositionBossAction : public AttackAction +class GruulTheDragonkillerTanksPositionBossAction : public AttackAction { public: - GruulTheDragonkillerMainTankPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller main tank position boss") : AttackAction(botAI, name) {}; + GruulTheDragonkillerTanksPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller tanks position boss") : AttackAction(botAI, name) {}; bool Execute(Event event) override; }; diff --git a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp index 5ca2de932..6604fc525 100644 --- a/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp +++ b/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp @@ -8,18 +8,11 @@ #include "HunterActions.h" #include "MageActions.h" #include "Playerbots.h" +#include "ReachTargetActions.h" #include "WarriorActions.h" using namespace GruulsLairHelpers; -static bool IsChargeAction(Action* action) -{ - return dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action); -} - float HighKingMaulgarDisableTankAssistMultiplier::GetValue(Action* action) { if (IsAnyOgreBossAlive(botAI) && dynamic_cast(action)) @@ -38,12 +31,10 @@ float HighKingMaulgarAvoidWhirlwindMultiplier::GetValue(Action* action) Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); if (maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && - (!kiggler || !kiggler->IsAlive()) && - (!krosh || !krosh->IsAlive()) && - (!olm || !olm->IsAlive()) && - (!blindeye || !blindeye->IsAlive())) + !kiggler && !krosh && !olm && !blindeye) { - if (IsChargeAction(action) || (dynamic_cast(action) && + if (dynamic_cast(action) || + (dynamic_cast(action) && !dynamic_cast(action))) return 0.0f; } @@ -57,7 +48,8 @@ float HighKingMaulgarDisableArcaneShotOnKroshMultiplier::GetValue(Action* action Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); Unit* target = AI_VALUE(Unit*, "current target"); - if (krosh && target && target->GetGUID() == krosh->GetGUID() && dynamic_cast(action)) + if (krosh && target && target->GetGUID() == krosh->GetGUID() && + dynamic_cast(action)) return 0.0f; return 1.0f; @@ -101,8 +93,9 @@ float GruulTheDragonkillerGroundSlamMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_GROUND_SLAM_1) || bot->HasAura(SPELL_GROUND_SLAM_2)) { - if ((dynamic_cast(action) && !dynamic_cast(action)) || - IsChargeAction(action)) + if ((dynamic_cast(action) && + !dynamic_cast(action)) || + dynamic_cast(action)) return 0.0f; } diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h index 809fadf03..3850f58c6 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h @@ -22,7 +22,7 @@ public: creators["high king maulgar misdirect olm and blindeye"] = &RaidGruulsLairActionContext::high_king_maulgar_misdirect_olm_and_blindeye; // Gruul the Dragonkiller - creators["gruul the dragonkiller main tank position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_main_tank_position_boss; + creators["gruul the dragonkiller tanks position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_tanks_position_boss; creators["gruul the dragonkiller spread ranged"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_spread_ranged; creators["gruul the dragonkiller shatter spread"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_shatter_spread; } @@ -41,7 +41,7 @@ private: static Action* high_king_maulgar_misdirect_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarMisdirectOlmAndBlindeyeAction(botAI); } // Gruul the Dragonkiller - static Action* gruul_the_dragonkiller_main_tank_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerMainTankPositionBossAction(botAI); } + static Action* gruul_the_dragonkiller_tanks_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerTanksPositionBossAction(botAI); } static Action* gruul_the_dragonkiller_spread_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerSpreadRangedAction(botAI); } static Action* gruul_the_dragonkiller_shatter_spread(PlayerbotAI* botAI) { return new GruulTheDragonkillerShatterSpreadAction(botAI); } }; diff --git a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h index fa8e76f58..d12b0ce46 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h @@ -22,8 +22,8 @@ public: creators["high king maulgar pulling olm and blindeye"] = &RaidGruulsLairTriggerContext::high_king_maulgar_pulling_olm_and_blindeye; // Gruul the Dragonkiller - creators["gruul the dragonkiller boss engaged by main tank"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_main_tank; - creators["gruul the dragonkiller boss engaged by range"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_range; + creators["gruul the dragonkiller boss engaged by tanks"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_tanks; + creators["gruul the dragonkiller boss engaged by ranged"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_ranged; creators["gruul the dragonkiller incoming shatter"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_incoming_shatter; } @@ -41,8 +41,8 @@ private: static Trigger* high_king_maulgar_pulling_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarPullingOlmAndBlindeyeTrigger(botAI); } // Gruul the Dragonkiller - static Trigger* gruul_the_dragonkiller_boss_engaged_by_main_tank(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByMainTankTrigger(botAI); } - static Trigger* gruul_the_dragonkiller_boss_engaged_by_range(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangeTrigger(botAI); } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_tanks(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByTanksTrigger(botAI); } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangedTrigger(botAI); } static Trigger* gruul_the_dragonkiller_incoming_shatter(PlayerbotAI* botAI) { return new GruulTheDragonkillerIncomingShatterTrigger(botAI); } }; diff --git a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp index 9ec264ea0..249c8e8a8 100644 --- a/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp +++ b/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp @@ -35,10 +35,10 @@ void RaidGruulsLairStrategy::InitTriggers(std::vector& triggers) NextAction("high king maulgar misdirect olm and blindeye", ACTION_RAID + 2) })); // Gruul the Dragonkiller - triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by main tank", { - NextAction("gruul the dragonkiller main tank position boss", ACTION_RAID + 1) })); + triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by tanks", { + NextAction("gruul the dragonkiller tanks position boss", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by range", { + triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by ranged", { NextAction("gruul the dragonkiller spread ranged", ACTION_RAID + 1) })); triggers.push_back(new TriggerNode("gruul the dragonkiller incoming shatter", { diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp index 35d9f9a1d..4bc5efe99 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp @@ -10,35 +10,35 @@ bool HighKingMaulgarIsMainTankTrigger::IsActive() { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - return botAI->IsMainTank(bot) && maulgar && maulgar->IsAlive(); + return botAI->IsMainTank(bot) && maulgar; } bool HighKingMaulgarIsFirstAssistTankTrigger::IsActive() { Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - return botAI->IsAssistTankOfIndex(bot, 0) && olm && olm->IsAlive(); + return botAI->IsAssistTankOfIndex(bot, 0, false) && olm; } bool HighKingMaulgarIsSecondAssistTankTrigger::IsActive() { Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - return botAI->IsAssistTankOfIndex(bot, 1) && blindeye && blindeye->IsAlive(); + return botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye; } bool HighKingMaulgarIsMageTankTrigger::IsActive() { Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - return IsKroshMageTank(botAI, bot) && krosh && krosh->IsAlive(); + return IsKroshMageTank(botAI, bot) && krosh; } bool HighKingMaulgarIsMoonkinTankTrigger::IsActive() { Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - return IsKigglerMoonkinTank(botAI, bot) && kiggler && kiggler->IsAlive(); + return IsKigglerMoonkinTank(botAI, bot) && kiggler; } bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() @@ -50,11 +50,11 @@ bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); return (botAI->IsDps(bot) || botAI->IsTank(bot)) && - !(botAI->IsMainTank(bot) && maulgar && maulgar->IsAlive()) && - !(botAI->IsAssistTankOfIndex(bot, 0) && olm && olm->IsAlive()) && - !(botAI->IsAssistTankOfIndex(bot, 1) && blindeye && blindeye->IsAlive()) && - !(IsKroshMageTank(botAI, bot) && krosh && krosh->IsAlive()) && - !(IsKigglerMoonkinTank(botAI, bot) && kiggler && kiggler->IsAlive()); + !(botAI->IsMainTank(bot) && maulgar) && + !(botAI->IsAssistTankOfIndex(bot, 0, false) && olm) && + !(botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye) && + !(IsKroshMageTank(botAI, bot) && krosh) && + !(IsKigglerMoonkinTank(botAI, bot) && kiggler); } bool HighKingMaulgarHealerInDangerTrigger::IsActive() @@ -66,7 +66,7 @@ bool HighKingMaulgarBossChannelingWhirlwindTrigger::IsActive() { Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - return maulgar && maulgar->IsAlive() && maulgar->HasAura(SPELL_WHIRLWIND) && + return maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && !botAI->IsMainTank(bot); } @@ -74,7 +74,7 @@ bool HighKingMaulgarWildFelstalkerSpawnedTrigger::IsActive() { Unit* felStalker = AI_VALUE2(Unit*, "find target", "wild fel stalker"); - return felStalker && felStalker->IsAlive() && bot->getClass() == CLASS_WARLOCK; + return felStalker && bot->getClass() == CLASS_WARLOCK; } bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() @@ -120,12 +120,12 @@ bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() switch (hunterIndex) { case 0: - return olm && olm->IsAlive() && olm->GetHealthPct() > 98.0f && - olmTank && olmTank->IsAlive() && botAI->CanCastSpell("misdirection", olmTank); + return olm && olm->GetHealthPct() > 98.0f && + olmTank && botAI->CanCastSpell("misdirection", olmTank); case 1: - return blindeye && blindeye->IsAlive() && blindeye->GetHealthPct() > 90.0f && - blindeyeTank && blindeyeTank->IsAlive() && botAI->CanCastSpell("misdirection", blindeyeTank); + return blindeye && blindeye->GetHealthPct() > 90.0f && + blindeyeTank && botAI->CanCastSpell("misdirection", blindeyeTank); default: break; @@ -136,25 +136,24 @@ bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() // Gruul the Dragonkiller Triggers -bool GruulTheDragonkillerBossEngagedByMainTankTrigger::IsActive() +bool GruulTheDragonkillerBossEngagedByTanksTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && botAI->IsMainTank(bot); + return gruul && botAI->IsTank(bot); } -bool GruulTheDragonkillerBossEngagedByRangeTrigger::IsActive() +bool GruulTheDragonkillerBossEngagedByRangedTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && botAI->IsRanged(bot); + return gruul && botAI->IsRanged(bot); } bool GruulTheDragonkillerIncomingShatterTrigger::IsActive() { Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - return gruul && gruul->IsAlive() && - (bot->HasAura(SPELL_GROUND_SLAM_1) || - bot->HasAura(SPELL_GROUND_SLAM_2)); + return gruul && (bot->HasAura(SPELL_GROUND_SLAM_1) || + bot->HasAura(SPELL_GROUND_SLAM_2)); } diff --git a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h index f3f328536..583b8b75f 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h @@ -73,17 +73,17 @@ public: bool IsActive() override; }; -class GruulTheDragonkillerBossEngagedByMainTankTrigger : public Trigger +class GruulTheDragonkillerBossEngagedByTanksTrigger : public Trigger { public: - GruulTheDragonkillerBossEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by main tank") {} + GruulTheDragonkillerBossEngagedByTanksTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by tanks") {} bool IsActive() override; }; -class GruulTheDragonkillerBossEngagedByRangeTrigger : public Trigger +class GruulTheDragonkillerBossEngagedByRangedTrigger : public Trigger { public: - GruulTheDragonkillerBossEngagedByRangeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by range") {} + GruulTheDragonkillerBossEngagedByRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by ranged") {} bool IsActive() override; }; diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp index 0c8a23a19..27c703aec 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp @@ -6,19 +6,16 @@ namespace GruulsLairHelpers { - namespace GruulsLairLocations - { - // Olm does not chase properly due to the Core's caster movement issues - // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location - // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location - // "MaulgarRoomCenter" is to keep healers in a centralized location - const Location MaulgarTankPosition = { 90.686f, 167.047f, -13.234f }; - const Location OlmTankPosition = { 87.485f, 234.942f, -3.635f }; - const Location BlindeyeTankPosition = { 99.681f, 213.989f, -10.345f }; - const Location KroshTankPosition = { 116.880f, 166.208f, -14.231f }; - const Location MaulgarRoomCenter = { 88.754f, 150.759f, -11.569f }; - const Location GruulTankPosition = { 241.238f, 365.025f, -4.220f }; - } + // Olm does not chase properly due to the Core's caster movement issues + // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location + // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location + // "MaulgarRoomCenter" is to keep healers in a centralized location + const Position MAULGAR_TANK_POSITION = { 90.686f, 167.047f, -13.234f }; + const Position OLM_TANK_POSITION = { 87.485f, 234.942f, -3.635f }; + const Position BLINDEYE_TANK_POSITION = { 99.681f, 213.989f, -10.345f }; + const Position KROSH_TANK_POSITION = { 116.880f, 166.208f, -14.231f }; + const Position MAULGAR_ROOM_CENTER = { 88.754f, 150.759f, -11.569f }; + const Position GRUUL_TANK_POSITION = { 241.238f, 365.025f, -4.220f }; bool IsAnyOgreBossAlive(PlayerbotAI* botAI) { @@ -42,84 +39,43 @@ namespace GruulsLairHelpers return false; } - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - Group* group = bot->GetGroup(); - if (!target || !group) - return; - - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - { - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); if (!group) return false; - Player* highestHpMage = nullptr; - uint32 highestHp = 0; + // (1) First loop: Return the first assistant Mage (real player or bot) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) continue; - if (member->getClass() == CLASS_MAGE) + if (group->IsAssistant(member->GetGUID())) + return member == bot; + } + + // (2) Fall back to bot Mage with highest HP + Player* highestHpMage = nullptr; + uint32 highestHp = 0; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_MAGE) + continue; + + uint32 hp = member->GetMaxHealth(); + if (!highestHpMage || hp > highestHp) { - uint32 hp = member->GetMaxHealth(); - if (!highestHpMage || hp > highestHp) - { - highestHpMage = member; - highestHp = hp; - } + highestHpMage = member; + highestHp = hp; } } + // (3) Return the found Mage tank, or nullptr if none found return highestHpMage == bot; } @@ -129,30 +85,37 @@ namespace GruulsLairHelpers if (!group) return false; - Player* highestHpMoonkin = nullptr; - uint32 highestHp = 0; - + // (1) First loop: Return the first assistant Moonkin (real player or bot) for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID) continue; - if (member->getClass() == CLASS_DRUID) + if (group->IsAssistant(member->GetGUID()) && + AiFactory::GetPlayerSpecTab(member) == DRUID_TAB_BALANCE) + return member == bot; + } + + // (2) Fall back to bot Moonkin with highest HP + Player* highestHpMoonkin = nullptr; + uint32 highestHp = 0; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID || + !GET_PLAYERBOT_AI(member) || AiFactory::GetPlayerSpecTab(member) != DRUID_TAB_BALANCE) + continue; + + uint32 hp = member->GetMaxHealth(); + if (!highestHpMoonkin || hp > highestHp) { - int tab = AiFactory::GetPlayerSpecTab(member); - if (tab == DRUID_TAB_BALANCE) - { - uint32 hp = member->GetMaxHealth(); - if (!highestHpMoonkin || hp > highestHp) - { - highestHpMoonkin = member; - highestHp = hp; - } - } + highestHpMoonkin = member; + highestHp = hp; } } + // (3) Return the found Moonkin tank, or nullptr if none found return highestHpMoonkin == bot; } diff --git a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h index c7becc836..4615a9b7a 100644 --- a/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h +++ b/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h @@ -2,23 +2,19 @@ #define RAID_GRUULSLAIRHELPERS_H #include "PlayerbotAI.h" -#include "RtiTargetValue.h" namespace GruulsLairHelpers { enum GruulsLairSpells { // High King Maulgar - SPELL_WHIRLWIND = 33238, + SPELL_WHIRLWIND = 33238, // Krosh Firehand - SPELL_SPELL_SHIELD = 33054, + SPELL_SPELL_SHIELD = 33054, // Hunter - SPELL_MISDIRECTION = 35079, - - // Warlock - SPELL_BANISH = 18647, // Rank 2 + SPELL_MISDIRECTION = 35079, // Gruul the Dragonkiller SPELL_GROUND_SLAM_1 = 33525, @@ -30,33 +26,20 @@ namespace GruulsLairHelpers NPC_WILD_FEL_STALKER = 18847, }; + constexpr uint32 GRUULS_LAIR_MAP_ID = 565; + bool IsAnyOgreBossAlive(PlayerbotAI* botAI); - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot); bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot); bool IsPositionSafe(PlayerbotAI* botAI, Player* bot, Position pos); bool TryGetNewSafePosition(PlayerbotAI* botAI, Player* bot, Position& outPos); - struct Location - { - float x, y, z; - }; - - namespace GruulsLairLocations - { - extern const Location MaulgarTankPosition; - extern const Location OlmTankPosition; - extern const Location BlindeyeTankPosition; - extern const Location KroshTankPosition; - extern const Location MaulgarRoomCenter; - extern const Location GruulTankPosition; - } + extern const Position MAULGAR_TANK_POSITION; + extern const Position OLM_TANK_POSITION; + extern const Position BLINDEYE_TANK_POSITION; + extern const Position KROSH_TANK_POSITION; + extern const Position MAULGAR_ROOM_CENTER; + extern const Position GRUUL_TANK_POSITION; } #endif diff --git a/src/Ai/Raid/Icecrown/RaidIccScripts.h b/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h similarity index 100% rename from src/Ai/Raid/Icecrown/RaidIccScripts.h rename to src/Ai/Raid/Icecrown/Util/RaidIccScripts.h diff --git a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp index adf0eac48..4d40a9eed 100644 --- a/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp +++ b/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp @@ -2,6 +2,7 @@ #include "RaidKarazhanHelpers.h" #include "Playerbots.h" #include "PlayerbotTextMgr.h" +#include "RaidBossHelpers.h" using namespace KarazhanHelpers; @@ -44,7 +45,7 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); if (attumenMounted) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithStar(bot, attumenMounted); SetRtiTarget(botAI, "star", attumenMounted); @@ -57,7 +58,7 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event) } else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithStar(bot, midnight); if (!botAI->IsAssistTankOfIndex(bot, 0)) @@ -180,7 +181,7 @@ bool MoroesMarkTargetAction::Execute(Event event) if (target) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithSkull(bot, target); SetRtiTarget(botAI, "skull", target); @@ -405,7 +406,7 @@ bool TheCuratorMarkAstralFlareAction::Execute(Event event) if (!flare) return false; - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) MarkTargetWithSkull(bot, flare); SetRtiTarget(botAI, "skull", flare); @@ -469,11 +470,11 @@ bool TheCuratorSpreadRangedAction::Execute(Event event) // Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof bool TerestianIllhoofMarkTargetAction::Execute(Event event) { - Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains"); - Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek"); + Unit* demonChains = GetFirstAliveUnitByEntry(botAI, NPC_DEMON_CHAINS); + Unit* kilrek = GetFirstAliveUnitByEntry(botAI, NPC_KILREK); Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); - Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); + Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); if (target) MarkTargetWithSkull(bot, target); @@ -1007,7 +1008,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) if (netherspite->GetHealth() == netherspite->GetMaxHealth() && !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.insert_or_assign(instanceId, now); if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1018,7 +1019,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) } else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.erase(instanceId); if (botAI->IsTank(bot)) @@ -1029,7 +1030,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event event) } else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) netherspiteDpsWaitTimer.try_emplace(instanceId, now); if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1458,7 +1459,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) nightbaneDpsWaitTimer.erase(instanceId); } // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer @@ -1466,7 +1467,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) { nightbaneRainOfBonesHit.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) { nightbaneFlightPhaseStartTimer.erase(instanceId); nightbaneDpsWaitTimer.try_emplace(instanceId, now); @@ -1482,7 +1483,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event event) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) { nightbaneDpsWaitTimer.erase(instanceId); nightbaneFlightPhaseStartTimer.try_emplace(instanceId, now); diff --git a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp index 117a17f38..06a3335c3 100644 --- a/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp +++ b/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp @@ -10,6 +10,7 @@ #include "MageActions.h" #include "Playerbots.h" #include "PriestActions.h" +#include "RaidBossHelpers.h" #include "ReachTargetActions.h" #include "RogueActions.h" #include "ShamanActions.h" @@ -242,6 +243,9 @@ float PrinceMalchezaarEnfeebleKeepDistanceMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_ENFEEBLE)) { + if (dynamic_cast(action)) + return 0.0f; + if (dynamic_cast(action) && !dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp b/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp index 2fb7d5af0..3c43aa898 100644 --- a/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp +++ b/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp @@ -2,6 +2,7 @@ #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace KarazhanHelpers; @@ -40,7 +41,7 @@ bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); @@ -110,7 +111,7 @@ bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); @@ -126,7 +127,7 @@ bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); @@ -178,7 +179,7 @@ bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); @@ -202,7 +203,7 @@ bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() // Exclusion of Banish is so the player may Banish elementals if they wish bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() { - if (!IsInstanceTimerManager(botAI, bot)) + if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); @@ -279,7 +280,7 @@ bool NetherspiteBossIsBanishedTrigger::IsActive() bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() { - if (!botAI->IsTank(bot) && !IsInstanceTimerManager(botAI, bot)) + if (!botAI->IsTank(bot) && !IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) return false; Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp index 821cc6701..ea989dcf3 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp @@ -1,7 +1,6 @@ #include "RaidKarazhanHelpers.h" #include "RaidKarazhanActions.h" #include "Playerbots.h" -#include "RtiTargetValue.h" namespace KarazhanHelpers { @@ -52,75 +51,6 @@ namespace KarazhanHelpers 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; - - if (Group* group = bot->GetGroup()) - { - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - } - - void MarkTargetWithSkull(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithMoon(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - // Only one bot is needed to set/reset instance-wide timers - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return false; - } - Unit* GetFirstAliveUnit(const std::vector& units) { for (Unit* unit : units) @@ -132,44 +62,6 @@ namespace KarazhanHelpers return nullptr; } - Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) - { - 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; - } - - Unit* GetNearestPlayerInRadius(Player* bot, float radius) - { - Unit* nearestPlayer = nullptr; - float nearestDistance = radius; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - float distance = bot->GetExactDist2d(member); - if (distance < nearestDistance) - { - nearestDistance = distance; - nearestPlayer = member; - } - } - } - - return nearestPlayer; - } - bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) { Unit* aran = botAI->GetAiObjectContext()->GetValue("find target", "shade of aran")->Get(); diff --git a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h index 394693b2e..ad7e8c3d5 100644 --- a/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h +++ b/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h @@ -61,6 +61,11 @@ namespace KarazhanHelpers NPC_ATTUMEN_THE_HUNTSMAN = 15550, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED = 16152, + // Terestian Illhoof + NPC_TERESTIAN_ILLHOOF = 15688, + NPC_DEMON_CHAINS = 17248, + NPC_KILREK = 17229, + // Shade of Aran NPC_CONJURED_ELEMENTAL = 17167, @@ -74,8 +79,8 @@ namespace KarazhanHelpers NPC_NETHERSPITE_INFERNAL = 17646, }; - const uint32 KARAZHAN_MAP_ID = 532; - const float NIGHTBANE_FLIGHT_Z = 95.0f; + constexpr uint32 KARAZHAN_MAP_ID = 532; + constexpr float NIGHTBANE_FLIGHT_Z = 95.0f; // Attumen the Huntsman extern std::unordered_map attumenDpsWaitTimer; @@ -105,17 +110,7 @@ namespace KarazhanHelpers 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 IsInstanceTimerManager(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); diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp index 69fc86244..dab7efab9 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp @@ -4,6 +4,7 @@ #include "ObjectAccessor.h" #include "ObjectGuid.h" #include "Playerbots.h" +#include "RaidBossHelpers.h" using namespace MagtheridonHelpers; @@ -14,46 +15,45 @@ bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) return false; Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) MarkTargetWithSquare(bot, channelerSquare); Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar && channelerStar->IsAlive()) + if (channelerStar) MarkTargetWithStar(bot, channelerStar); Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle && channelerCircle->IsAlive()) + if (channelerCircle) MarkTargetWithCircle(bot, channelerCircle); // After first three channelers are dead, wait for Magtheridon to activate - if ((!channelerSquare || !channelerSquare->IsAlive()) && - (!channelerStar || !channelerStar->IsAlive()) && - (!channelerCircle || !channelerCircle->IsAlive())) + if (!channelerSquare && !channelerStar && !channelerCircle) { - const Location& position = MagtheridonsLairLocations::WaitingForMagtheridonPosition; - if (!bot->IsWithinDist2d(position.x, position.y, 2.0f)) + const Position& position = WAITING_FOR_MAGTHERIDON_POSITION; + if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), 2.0f)) { - return MoveTo(bot->GetMapId(), position.x, position.y, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - bot->SetFacingTo(position.orientation); + bot->SetFacingTo(position.GetOrientation()); return true; } Creature* currentTarget = nullptr; std::string rtiName; - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) { currentTarget = channelerSquare; rtiName = "square"; } - else if (channelerStar && channelerStar->IsAlive()) + else if (channelerStar) { currentTarget = channelerStar; rtiName = "star"; } - else if (channelerCircle && channelerCircle->IsAlive()) + else if (channelerCircle) { currentTarget = channelerCircle; rtiName = "circle"; @@ -70,7 +70,7 @@ bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event event) bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) { Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (!channelerDiamond || !channelerDiamond->IsAlive()) + if (!channelerDiamond) return false; MarkTargetWithDiamond(bot, channelerDiamond); @@ -81,18 +81,18 @@ bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) if (channelerDiamond->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::NWChannelerTankPosition; + const Position& position = NW_CHANNELER_TANK_POSITION; const float maxDistance = 3.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -103,7 +103,7 @@ bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event event) bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) { Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (!channelerTriangle || !channelerTriangle->IsAlive()) + if (!channelerTriangle) return false; MarkTargetWithTriangle(bot, channelerTriangle); @@ -114,18 +114,18 @@ bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event event) if (channelerTriangle->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::NEChannelerTankPosition; + const Position& position = NE_CHANNELER_TANK_POSITION; const float maxDistance = 3.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -175,7 +175,7 @@ bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) switch (hunterIndex) { case 0: - if (mainTank && channelerStar && channelerStar->IsAlive() && + if (mainTank && channelerStar && channelerStar->GetVictim() != mainTank) { if (botAI->CanCastSpell("misdirection", mainTank)) @@ -190,7 +190,7 @@ bool MagtheridonMisdirectHellfireChannelers::Execute(Event event) break; case 1: - if (mainTank && channelerCircle && channelerCircle->IsAlive() && + if (mainTank && channelerCircle && channelerCircle->GetVictim() != mainTank) { if (botAI->CanCastSpell("misdirection", mainTank)) @@ -215,90 +215,69 @@ bool MagtheridonAssignDPSPriorityAction::Execute(Event event) { // Listed in order of priority Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare && channelerSquare->IsAlive()) + if (channelerSquare) { SetRtiTarget(botAI, "square", channelerSquare); if (bot->GetTarget() != channelerSquare->GetGUID()) - { - bot->SetSelection(channelerSquare->GetGUID()); return Attack(channelerSquare); - } return false; } Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar && channelerStar->IsAlive()) + if (channelerStar) { SetRtiTarget(botAI, "star", channelerStar); if (bot->GetTarget() != channelerStar->GetGUID()) - { - bot->SetSelection(channelerStar->GetGUID()); return Attack(channelerStar); - } return false; } Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle && channelerCircle->IsAlive()) + if (channelerCircle) { SetRtiTarget(botAI, "circle", channelerCircle); if (bot->GetTarget() != channelerCircle->GetGUID()) - { - bot->SetSelection(channelerCircle->GetGUID()); return Attack(channelerCircle); - } return false; } Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (channelerDiamond && channelerDiamond->IsAlive()) + if (channelerDiamond) { SetRtiTarget(botAI, "diamond", channelerDiamond); if (bot->GetTarget() != channelerDiamond->GetGUID()) - { - bot->SetSelection(channelerDiamond->GetGUID()); return Attack(channelerDiamond); - } return false; } Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (channelerTriangle && channelerTriangle->IsAlive()) + if (channelerTriangle) { SetRtiTarget(botAI, "triangle", channelerTriangle); if (bot->GetTarget() != channelerTriangle->GetGUID()) - { - bot->SetSelection(channelerTriangle->GetGUID()); return Attack(channelerTriangle); - } return false; } Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); if (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && - (!channelerSquare || !channelerSquare->IsAlive()) && - (!channelerStar || !channelerStar->IsAlive()) && - (!channelerCircle || !channelerCircle->IsAlive()) && - (!channelerDiamond || !channelerDiamond->IsAlive()) && - (!channelerTriangle || !channelerTriangle->IsAlive())) + !channelerSquare && !channelerStar && !channelerCircle && + !channelerDiamond && !channelerTriangle) { SetRtiTarget(botAI, "cross", magtheridon); if (bot->GetTarget() != magtheridon->GetGUID()) - { - bot->SetSelection(magtheridon->GetGUID()); return Attack(magtheridon); - } } return false; @@ -343,15 +322,15 @@ bool MagtheridonWarlockCCBurningAbyssalAction::Execute(Event event) if (warlockIndex >= 0 && warlockIndex < abyssals.size()) { Unit* assignedAbyssal = abyssals[warlockIndex]; - if (!assignedAbyssal->HasAura(SPELL_BANISH) && botAI->CanCastSpell(SPELL_BANISH, assignedAbyssal, true)) + if (!botAI->HasAura("banish", assignedAbyssal) && botAI->CanCastSpell("banish", assignedAbyssal)) return botAI->CastSpell("banish", assignedAbyssal); } for (size_t i = warlocks.size(); i < abyssals.size(); ++i) { Unit* excessAbyssal = abyssals[i]; - if (!excessAbyssal->HasAura(SPELL_BANISH) && !excessAbyssal->HasAura(SPELL_FEAR) && - botAI->CanCastSpell(SPELL_FEAR, excessAbyssal, true)) + if (!botAI->HasAura("banish", excessAbyssal) && !botAI->HasAura("fear", excessAbyssal) && + botAI->CanCastSpell("fear", excessAbyssal)) return botAI->CastSpell("fear", excessAbyssal); } @@ -373,22 +352,20 @@ bool MagtheridonMainTankPositionBossAction::Execute(Event event) if (magtheridon->GetVictim() == bot) { - const Location& position = MagtheridonsLairLocations::MagtheridonTankPosition; + const Position& position = MAGTHERIDON_TANK_POSITION; const float maxDistance = 2.0f; + float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (bot->GetExactDist2d(position.x, position.y) > maxDistance) + if (distanceToPosition > maxDistance) { - float dX = position.x - bot->GetPositionX(); - float dY = position.y - bot->GetPositionY(); - float dist = sqrt(dX * dX + dY * dY); - float moveX = bot->GetPositionX() + (dX / dist) * maxDistance; - float moveY = bot->GetPositionY() + (dY / dist) * maxDistance; + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; + float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(bot->GetMapId(), moveX, moveY, position.z, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); } - - bot->SetFacingTo(position.orientation); } return false; @@ -440,13 +417,13 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) } bool isHealer = botAI->IsHeal(bot); - const Location& center = isHealer - ? MagtheridonsLairLocations::HealerSpreadPosition - : MagtheridonsLairLocations::RangedSpreadPosition; + const Position& center = isHealer + ? HEALER_SPREAD_POSITION + : RANGED_SPREAD_POSITION; float maxSpreadRadius = isHealer ? 15.0f : 20.0f; - float centerX = center.x; - float centerY = center.y; - float centerZ = bot->GetPositionZ(); + float centerX = center.GetPositionX(); + float centerY = center.GetPositionY(); + float centerZ = center.GetPositionZ(); const float radiusBuffer = 3.0f; if (!initialPositions.count(bot->GetGUID())) @@ -479,7 +456,7 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, destX, destY, destZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } hasReachedInitialPosition[bot->GetGUID()] = true; @@ -499,7 +476,7 @@ bool MagtheridonSpreadRangedAction::Execute(Event event) { bot->AttackStop(); bot->InterruptNonMeleeSpells(false); - return MoveTo(bot->GetMapId(), targetX, targetY, centerZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, centerZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -593,7 +570,7 @@ bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeI { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -603,7 +580,7 @@ bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeI float fallbackY = cubeInfo.y + sin(angle) * safeWaitDistance; float fallbackZ = bot->GetPositionZ(); - return MoveTo(bot->GetMapId(), fallbackX, fallbackY, fallbackZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, fallbackX, fallbackY, fallbackZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } @@ -638,7 +615,7 @@ bool MagtheridonUseManticronCubeAction::HandleCubeInteraction(const CubeInfo& cu bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } @@ -663,14 +640,14 @@ bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); bool lastBlastNova = lastBlastNovaState[instanceId]; - if (lastBlastNova && !blastNovaActive && IsInstanceTimerManager(botAI, bot)) + if (lastBlastNova && !blastNovaActive && IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) blastNovaTimer[instanceId] = now; lastBlastNovaState[instanceId] = blastNovaActive; if (!magtheridon->HasAura(SPELL_SHADOW_CAGE)) { - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) { spreadWaitTimer.try_emplace(instanceId, now); blastNovaTimer.try_emplace(instanceId, now); @@ -679,11 +656,12 @@ bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event event) } else { - MagtheridonSpreadRangedAction::initialPositions.clear(); - MagtheridonSpreadRangedAction::hasReachedInitialPosition.clear(); - botToCubeAssignment.clear(); + ObjectGuid guid = bot->GetGUID(); + MagtheridonSpreadRangedAction::initialPositions.erase(guid); + MagtheridonSpreadRangedAction::hasReachedInitialPosition.erase(guid); + botToCubeAssignment.erase(guid); - if (IsInstanceTimerManager(botAI, bot)) + if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) { spreadWaitTimer.erase(instanceId); blastNovaTimer.erase(instanceId); diff --git a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h index 6c4ed84c2..d47d06459 100644 --- a/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h +++ b/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h @@ -6,8 +6,6 @@ #include "AttackAction.h" #include "MovementActions.h" -using namespace MagtheridonHelpers; - class MagtheridonMainTankAttackFirstThreeChannelersAction : public AttackAction { public: @@ -85,8 +83,8 @@ public: private: bool HandleCubeRelease(Unit* magtheridon, GameObject* cube); bool ShouldActivateCubeLogic(Unit* magtheridon); - bool HandleWaitingPhase(const CubeInfo& cubeInfo); - bool HandleCubeInteraction(const CubeInfo& cubeInfo, GameObject* cube); + bool HandleWaitingPhase(const MagtheridonHelpers::CubeInfo& cubeInfo); + bool HandleCubeInteraction(const MagtheridonHelpers::CubeInfo& cubeInfo, GameObject* cube); }; class MagtheridonManageTimersAndAssignmentsAction : public Action diff --git a/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp b/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp index 9580fd923..55aaf90e9 100644 --- a/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp +++ b/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp @@ -8,6 +8,7 @@ #include "GenericSpellActions.h" #include "Playerbots.h" #include "WarlockActions.h" +#include "WipeAction.h" using namespace MagtheridonHelpers; @@ -24,10 +25,10 @@ float MagtheridonUseManticronCubeMultiplier::GetValue(Action* action) auto it = botToCubeAssignment.find(bot->GetGUID()); if (it != botToCubeAssignment.end()) { - if (dynamic_cast(action)) + if (dynamic_cast(action)) return 1.0f; - - return 0.0f; + else if (!dynamic_cast(action)) + return 0.0f; } } @@ -41,28 +42,31 @@ float MagtheridonWaitToAttackMultiplier::GetValue(Action* action) if (!magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) return 1.0f; + if (botAI->IsMainTank(bot)) + return 1.0f; + const uint8 dpsWaitSeconds = 6; auto it = dpsWaitTimer.find(magtheridon->GetMap()->GetInstanceId()); if (it == dpsWaitTimer.end() || (time(nullptr) - it->second) < dpsWaitSeconds) { - if (!botAI->IsMainTank(bot) && (dynamic_cast(action) || - (!botAI->IsHeal(bot) && dynamic_cast(action)))) + if (dynamic_cast(action) || + (!botAI->IsHeal(bot) && dynamic_cast(action))) return 0.0f; } return 1.0f; } -// No tank assist for offtanks during the channeler phase -// So they don't try to pull channelers from each other or the main tank float MagtheridonDisableOffTankAssistMultiplier::GetValue(Action* action) { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); if (!magtheridon) return 1.0f; + if (bot->GetVictim() == nullptr) + return 1.0f; + if ((botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) && dynamic_cast(action)) return 0.0f; diff --git a/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp b/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp index 35442df6e..43aa3361f 100644 --- a/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp +++ b/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp @@ -18,7 +18,7 @@ bool MagtheridonNWChannelerEngagedByFirstAssistTankTrigger::IsActive() Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); return magtheridon && botAI->IsAssistTankOfIndex(bot, 0) && - channelerDiamond && channelerDiamond->IsAlive(); + channelerDiamond; } bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() @@ -27,7 +27,7 @@ bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); return magtheridon && botAI->IsAssistTankOfIndex(bot, 1) && - channelerTriangle && channelerTriangle->IsAlive(); + channelerTriangle; } bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() @@ -38,8 +38,7 @@ bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); return magtheridon && bot->getClass() == CLASS_HUNTER && - ((channelerStar && channelerStar->IsAlive()) || - (channelerCircle && channelerCircle->IsAlive())); + (channelerStar || channelerCircle); } bool MagtheridonDeterminingKillOrderTrigger::IsActive() @@ -51,12 +50,11 @@ bool MagtheridonDeterminingKillOrderTrigger::IsActive() Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); if (!magtheridon || botAI->IsHeal(bot) || botAI->IsMainTank(bot) || - (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond && channelerDiamond->IsAlive()) || - (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle && channelerTriangle->IsAlive())) + (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond) || + (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle)) return false; - return (channeler && channeler->IsAlive()) || (magtheridon && - !magtheridon->HasAura(SPELL_SHADOW_CAGE)); + return channeler || (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE)); } bool MagtheridonBurningAbyssalSpawnedTrigger::IsActive() @@ -84,10 +82,8 @@ bool MagtheridonBossEngagedByMainTankTrigger::IsActive() bool MagtheridonBossEngagedByRangedTrigger::IsActive() { Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); - return magtheridon && botAI->IsRanged(bot) && - !(channeler && channeler->IsAlive()); + return magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && botAI->IsRanged(bot); } bool MagtheridonIncomingBlastNovaTrigger::IsActive() @@ -122,7 +118,5 @@ bool MagtheridonIncomingBlastNovaTrigger::IsActive() bool MagtheridonNeedToManageTimersAndAssignmentsTrigger::IsActive() { - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - return magtheridon; + return AI_VALUE2(Unit*, "find target", "magtheridon"); } diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp index dc88d2a19..a56d4b85b 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp @@ -1,22 +1,18 @@ #include "RaidMagtheridonHelpers.h" #include "Creature.h" #include "GameObject.h" -#include "GroupReference.h" #include "Map.h" #include "ObjectGuid.h" #include "Playerbots.h" namespace MagtheridonHelpers { - namespace MagtheridonsLairLocations - { - const Location WaitingForMagtheridonPosition = { 1.359f, 2.048f, -0.406f, 3.135f }; - const Location MagtheridonTankPosition = { 22.827f, 2.105f, -0.406f, 3.135f }; - const Location NWChannelerTankPosition = { -11.764f, 30.818f, -0.411f, 0.0f }; - const Location NEChannelerTankPosition = { -12.490f, -26.211f, -0.411f, 0.0f }; - const Location RangedSpreadPosition = { -14.890f, 1.995f, -0.406f, 0.0f }; - const Location HealerSpreadPosition = { -2.265f, 1.874f, -0.404f, 0.0f }; - } + const Position WAITING_FOR_MAGTHERIDON_POSITION = { 1.359f, 2.048f, -0.406f, 3.135f }; + const Position MAGTHERIDON_TANK_POSITION = { 22.827f, 2.105f, -0.406f, 3.135f }; + const Position NW_CHANNELER_TANK_POSITION = { -11.764f, 30.818f, -0.411f, 0.0f }; + const Position NE_CHANNELER_TANK_POSITION = { -12.490f, -26.211f, -0.411f, 0.0f }; + const Position RANGED_SPREAD_POSITION = { -14.890f, 1.995f, -0.406f, 0.0f }; + const Position HEALER_SPREAD_POSITION = { -2.265f, 1.874f, -0.404f, 0.0f }; // Identify channelers by their database GUIDs Creature* GetChanneler(Player* bot, uint32 dbGuid) @@ -29,63 +25,11 @@ namespace MagtheridonHelpers if (it == map->GetCreatureBySpawnIdStore().end()) return nullptr; - return it->second; - } + Creature* channeler = it->second; + if (!channeler->IsAlive()) + return nullptr; - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) - { - Group* group = bot->GetGroup(); - if (!target || !group) - return; - - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); - } - - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) - { - if (!target) - return; - - std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); - Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); - - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); - } - } - - void MarkTargetWithSquare(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); - } - - void MarkTargetWithStar(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); - } - - void MarkTargetWithCircle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); - } - - void MarkTargetWithDiamond(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); - } - - void MarkTargetWithTriangle(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); - } - - void MarkTargetWithCross(Player* bot, Unit* target) - { - MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); + return channeler; } const std::vector MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 }; @@ -208,19 +152,4 @@ namespace MagtheridonHelpers return true; } - - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsDps(member) && GET_PLAYERBOT_AI(member)) - return member == bot; - } - } - - return true; - } } diff --git a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h index f9f514de3..1335aff46 100644 --- a/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h +++ b/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h @@ -8,7 +8,6 @@ #include "Group.h" #include "ObjectGuid.h" #include "PlayerbotAI.h" -#include "RtiTargetValue.h" namespace MagtheridonHelpers { @@ -19,10 +18,6 @@ namespace MagtheridonHelpers SPELL_BLAST_NOVA = 30616, SPELL_SHADOW_GRASP = 30410, - // Warlock - SPELL_BANISH = 18647, - SPELL_FEAR = 6215, - // Hunter SPELL_MISDIRECTION = 35079, }; @@ -38,6 +33,7 @@ namespace MagtheridonHelpers GO_BLAZE = 181832, }; + constexpr uint32 MAGTHERIDON_MAP_ID = 544; constexpr uint32 SOUTH_CHANNELER = 90978; constexpr uint32 WEST_CHANNELER = 90979; constexpr uint32 NORTHWEST_CHANNELER = 90980; @@ -45,31 +41,14 @@ namespace MagtheridonHelpers constexpr uint32 NORTHEAST_CHANNELER = 90981; Creature* GetChanneler(Player* bot, uint32 dbGuid); - void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); - void MarkTargetWithSquare(Player* bot, Unit* target); - void MarkTargetWithStar(Player* bot, Unit* target); - void MarkTargetWithCircle(Player* bot, Unit* target); - void MarkTargetWithDiamond(Player* bot, Unit* target); - void MarkTargetWithTriangle(Player* bot, Unit* target); - void MarkTargetWithCross(Player* bot, Unit* target); - void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z); - bool IsInstanceTimerManager(PlayerbotAI* botAI, Player* bot); - struct Location - { - float x, y, z, orientation; - }; - - namespace MagtheridonsLairLocations - { - extern const Location WaitingForMagtheridonPosition; - extern const Location MagtheridonTankPosition; - extern const Location NWChannelerTankPosition; - extern const Location NEChannelerTankPosition; - extern const Location RangedSpreadPosition; - extern const Location HealerSpreadPosition; - } + extern const Position WAITING_FOR_MAGTHERIDON_POSITION; + extern const Position MAGTHERIDON_TANK_POSITION; + extern const Position NW_CHANNELER_TANK_POSITION; + extern const Position NE_CHANNELER_TANK_POSITION; + extern const Position RANGED_SPREAD_POSITION; + extern const Position HEALER_SPREAD_POSITION; struct CubeInfo { diff --git a/src/Ai/Raid/MoltenCore/RaidMcActionContext.h b/src/Ai/Raid/MoltenCore/RaidMcActionContext.h index 79a4a95a8..aaccb80d3 100644 --- a/src/Ai/Raid/MoltenCore/RaidMcActionContext.h +++ b/src/Ai/Raid/MoltenCore/RaidMcActionContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_RAIDMCACTIONCONTEXT_H #include "Action.h" +#include "BossAuraActions.h" #include "NamedObjectContext.h" #include "RaidMcActions.h" diff --git a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h index b74958919..a62d851dc 100644 --- a/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h +++ b/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H #include "AiObjectContext.h" +#include "BossAuraTriggers.h" #include "NamedObjectContext.h" #include "RaidMcTriggers.h" diff --git a/src/Ai/Raid/MoltenCore/RaidMcHelpers.h b/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h similarity index 100% rename from src/Ai/Raid/MoltenCore/RaidMcHelpers.h rename to src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h diff --git a/src/Ai/Raid/RaidBossHelpers.cpp b/src/Ai/Raid/RaidBossHelpers.cpp new file mode 100644 index 000000000..bcb48294f --- /dev/null +++ b/src/Ai/Raid/RaidBossHelpers.cpp @@ -0,0 +1,142 @@ +#include "RaidBossHelpers.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" + +// Functions to mark targets with raid target icons +// Note that these functions do not allow the player to change the icon during the encounter +void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) +{ + if (!target) + return; + + if (Group* group = bot->GetGroup()) + { + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + } +} + +void MarkTargetWithSkull(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); +} + +void MarkTargetWithSquare(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); +} + +void MarkTargetWithStar(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); +} + +void MarkTargetWithCircle(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); +} + +void MarkTargetWithDiamond(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); +} + +void MarkTargetWithTriangle(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); +} + +void MarkTargetWithCross(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); +} + +void MarkTargetWithMoon(Player* bot, Unit* target) +{ + MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); +} + +// For bots to set their raid target icon to the specified icon on the specified target +void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) +{ + if (!target) + return; + + std::string currentRti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("rti target")->Get(); + + if (currentRti != rtiName || currentTarget != target) + { + botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); + botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + } +} + +// Return the first alive DPS bot in the specified instance map, excluding any specified bot +// Intended for purposes of storing and erasing timers and trackers in associative containers +bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude) +{ + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->GetMapId() != mapId || + !GET_PLAYERBOT_AI(member) || !botAI->IsDps(member)) + continue; + + if (member != exclude) + return member == bot; + } + } + + return false; +} + +// Return the first matching alive unit from a cell search of nearby npcs +// More responsive than "find target," but performance cost is much higher +// Re: using the third parameter (false by default), some units are never considered +// to be in combat (e.g., totems) +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat) +{ + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + { + if (!requireInCombat || unit->IsInCombat()) + return unit; + } + } + + return nullptr; +} + +// Return the nearest alive player (human or bot) within the specified radius +Unit* GetNearestPlayerInRadius(Player* bot, float radius) +{ + Unit* nearestPlayer = nullptr; + float nearestDistance = radius; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + } + + return nearestPlayer; +} diff --git a/src/Ai/Raid/RaidBossHelpers.h b/src/Ai/Raid/RaidBossHelpers.h new file mode 100644 index 000000000..15c60353e --- /dev/null +++ b/src/Ai/Raid/RaidBossHelpers.h @@ -0,0 +1,21 @@ +#ifndef _PLAYERBOT_RAIDBOSSHELPERS_H_ +#define _PLAYERBOT_RAIDBOSSHELPERS_H_ + +#include "AiObject.h" +#include "Unit.h" + +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 MarkTargetWithDiamond(Player* bot, Unit* target); +void MarkTargetWithTriangle(Player* bot, Unit* target); +void MarkTargetWithCross(Player* bot, Unit* target); +void MarkTargetWithMoon(Player* bot, Unit* target); +void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); +bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude = nullptr); +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat = false); +Unit* GetNearestPlayerInRadius(Player* bot, float radius); + +#endif diff --git a/src/Ai/Raid/RaidStrategyContext.h b/src/Ai/Raid/RaidStrategyContext.h index 4f7a63c7a..4a040985e 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -8,6 +8,7 @@ #include "RaidKarazhanStrategy.h" #include "RaidMagtheridonStrategy.h" #include "RaidGruulsLairStrategy.h" +#include "RaidSSCStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" #include "RaidVoAStrategy.h" @@ -26,10 +27,11 @@ public: creators["karazhan"] = &RaidStrategyContext::karazhan; creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["ssc"] = &RaidStrategyContext::ssc; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; - creators["uld"] = &RaidStrategyContext::uld; + creators["ulduar"] = &RaidStrategyContext::ulduar; creators["onyxia"] = &RaidStrategyContext::onyxia; creators["icc"] = &RaidStrategyContext::icc; } @@ -41,11 +43,12 @@ private: static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } + static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } static Strategy* onyxia(PlayerbotAI* botAI) { return new RaidOnyxiaStrategy(botAI); } - static Strategy* uld(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } + static Strategy* ulduar(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } static Strategy* icc(PlayerbotAI* botAI) { return new RaidIccStrategy(botAI); } }; diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp new file mode 100644 index 000000000..7aa3eda35 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp @@ -0,0 +1,3128 @@ +#include "RaidSSCActions.h" +#include "RaidSSCHelpers.h" +#include "AiFactory.h" +#include "Corpse.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" +#include "RtiTargetValue.h" + +using namespace SerpentShrineCavernHelpers; + +// General + +bool SerpentShrineCavernEraseTimersAndTrackersAction::Execute(Event /*event*/) +{ + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + const ObjectGuid guid = bot->GetGUID(); + + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + { + if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) + erased = true; + if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) + erased = true; + if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "the lurker below")) + { + if (lurkerRangedPositions.erase(guid) > 0) + erased = true; + if (lurkerSpoutTimer.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + { + if (karathressDpsWaitTimer.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + { + if (tidewalkerTankStep.erase(guid) > 0) + erased = true; + if (tidewalkerRangedStep.erase(guid) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + { + if (vashjRangedPositions.erase(guid) > 0) + erased = true; + if (hasReachedVashjRangedPosition.erase(guid) > 0) + erased = true; + } + + return erased; +} + +// Trash Mobs + +// Move out of toxic pool left behind by some colossi upon death +bool UnderbogColossusEscapeToxicPoolAction::Execute(Event /*event*/) +{ + Aura* aura = bot->GetAura(SPELL_TOXIC_POOL); + if (!aura) + return false; + + DynamicObject* dynObj = aura->GetDynobjOwner(); + if (!dynObj) + return false; + + float radius = dynObj->GetRadius(); + if (radius <= 0.0f) + { + const SpellInfo* sInfo = sSpellMgr->GetSpellInfo(dynObj->GetSpellId()); + if (sInfo) + { + for (int e = 0; e < MAX_SPELL_EFFECTS; ++e) + { + auto const& eff = sInfo->Effects[e]; + if (eff.Effect == SPELL_EFFECT_SCHOOL_DAMAGE || + (eff.Effect == SPELL_EFFECT_APPLY_AURA && + eff.ApplyAuraName == SPELL_AURA_PERIODIC_DAMAGE)) + { + radius = eff.CalcRadius(); + break; + } + } + } + } + + if (radius <= 0.0f) + return false; + + constexpr float bufferDist = 3.0f; + constexpr float centerThreshold = 1.0f; + + float dx = bot->GetPositionX() - dynObj->GetPositionX(); + float dy = bot->GetPositionY() - dynObj->GetPositionY(); + + float distToObj = bot->GetExactDist2d(dynObj->GetPositionX(), dynObj->GetPositionY()); + const float insideThresh = radius + centerThreshold; + + if (distToObj > insideThresh) + return false; + + float safeDist = radius + bufferDist; + float moveX, moveY; + + if (distToObj == 0.0f) + { + float angle = frand(0.0f, static_cast(M_PI * 2.0)); + moveX = dynObj->GetPositionX() + std::cos(angle) * safeDist; + moveY = dynObj->GetPositionY() + std::sin(angle) * safeDist; + } + else + { + float invDist = 1.0f / distToObj; + moveX = dynObj->GetPositionX() + (dx * invDist) * safeDist; + moveY = dynObj->GetPositionY() + (dy * invDist) * safeDist; + } + + botAI->Reset(); + return MoveTo(SSC_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, false, + true, MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool GreyheartTidecallerMarkWaterElementalTotemAction::Execute(Event /*event*/) +{ + if (Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM)) + MarkTargetWithSkull(bot, totem); + + return false; +} + +// Hydross the Unstable + +// (1) When tanking, move to designated tanking spot on frost side +// (2) 1 second after 100% Mark of Hydross, move to nature tank's spot to hand off boss +// (3) When Hydross is in nature form, move back to frost tank spot and wait for transition +bool HydrossTheUnstablePositionFrostTankAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + if (!hydross->HasAura(SPELL_CORRUPTION) && !HasMarkOfHydrossAt100Percent(bot)) + { + MarkTargetWithSquare(bot, hydross); + SetRtiTarget(botAI, "square", hydross); + + if (bot->GetTarget() != hydross->GetGUID()) + return Attack(hydross); + + if (hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const Position& position = HYDROSS_FROST_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + if (!hydross->HasAura(SPELL_CORRUPTION) && HasMarkOfHydrossAt100Percent(bot) && + hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const time_t now = std::time(nullptr); + auto it = hydrossChangeToNaturePhaseTimer.find(hydross->GetMap()->GetInstanceId()); + + if (it != hydrossChangeToNaturePhaseTimer.end() && (now - it->second) >= 1) + { + const Position& position = HYDROSS_NATURE_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + else + { + botAI->Reset(); + return true; + } + } + } + + if (hydross->HasAura(SPELL_CORRUPTION)) + { + const Position& position = HYDROSS_FROST_TANK_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// (1) When tanking, move to designated tanking spot on nature side +// (2) 1 second after 100% Mark of Corruption, move to frost tank's spot to hand off boss +// (3) When Hydross is in frost form, move back to nature tank spot and wait for transition +bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + if (hydross->HasAura(SPELL_CORRUPTION) && !HasMarkOfCorruptionAt100Percent(bot)) + { + MarkTargetWithTriangle(bot, hydross); + SetRtiTarget(botAI, "triangle", hydross); + + if (bot->GetTarget() != hydross->GetGUID()) + return Attack(hydross); + + if (hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const Position& position = HYDROSS_NATURE_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + if (hydross->HasAura(SPELL_CORRUPTION) && HasMarkOfCorruptionAt100Percent(bot) && + hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) + { + const time_t now = std::time(nullptr); + auto it = hydrossChangeToFrostPhaseTimer.find(hydross->GetMap()->GetInstanceId()); + + if (it != hydrossChangeToFrostPhaseTimer.end() && (now - it->second) >= 1) + { + const Position& position = HYDROSS_FROST_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + else + { + botAI->Reset(); + return true; + } + } + } + + if (!hydross->HasAura(SPELL_CORRUPTION)) + { + const Position& position = HYDROSS_NATURE_TANK_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool HydrossTheUnstablePrioritizeElementalAddsAction::Execute(Event /*event*/) +{ + Unit* waterElemental = GetFirstAliveUnitByEntry(botAI, NPC_PURE_SPAWN_OF_HYDROSS); + if (waterElemental) + { + if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) + MarkTargetWithSkull(bot, waterElemental); + + SetRtiTarget(botAI, "skull", waterElemental); + + if (bot->GetTarget() != waterElemental->GetGUID()) + return Attack(waterElemental); + } + else if (Unit* natureElemental = GetFirstAliveUnitByEntry(botAI, NPC_TAINTED_SPAWN_OF_HYDROSS)) + { + if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) + MarkTargetWithSkull(bot, natureElemental); + + SetRtiTarget(botAI, "skull", natureElemental); + + if (bot->GetTarget() != natureElemental->GetGUID()) + return Attack(natureElemental); + } + + return false; +} + +// To mitigate the effect of Water Tomb +bool HydrossTheUnstableFrostPhaseSpreadOutAction::Execute(Event /*event*/) +{ + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + constexpr float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetExactDist2d(member) < safeDistance) + return FleePosition(member->GetPosition(), safeDistance, minInterval); + } + } + + return false; +} + +bool HydrossTheUnstableMisdirectBossToTankAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + if (Group* group = bot->GetGroup()) + { + if (TryMisdirectToFrostTank(hydross, group)) + return true; + + if (TryMisdirectToNatureTank(hydross, group)) + return true; + } + + return false; +} + +bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToFrostTank( + Unit* hydross, Group* group) +{ + Player* frostTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + frostTank = member; + break; + } + } + + if (HasNoMarkOfHydross(bot) && !hydross->HasAura(SPELL_CORRUPTION) && frostTank) + { + if (botAI->CanCastSpell("misdirection", frostTank)) + return botAI->CastSpell("misdirection", frostTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", hydross)) + return botAI->CastSpell("steady shot", hydross); + } + + return false; +} + +bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToNatureTank( + Unit* hydross, Group* group) +{ + Player* natureTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsAssistTankOfIndex(member, 0, true)) + { + natureTank = member; + break; + } + } + + if (HasNoMarkOfCorruption(bot) && hydross->HasAura(SPELL_CORRUPTION) && natureTank) + { + if (botAI->CanCastSpell("misdirection", natureTank)) + return botAI->CastSpell("misdirection", natureTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", hydross)) + return botAI->CastSpell("steady shot", hydross); + } + + return false; +} + +bool HydrossTheUnstableStopDpsUponPhaseChangeAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + const uint32 instanceId = hydross->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + constexpr uint8 phaseStartStopSeconds = 5; + constexpr uint8 phaseEndStopSeconds = 1; + + bool shouldStopDps = false; + + // 1 second after 100% Mark of Hydross, stop DPS + auto itNature = hydrossChangeToNaturePhaseTimer.find(instanceId); + if (itNature != hydrossChangeToNaturePhaseTimer.end() && + (now - itNature->second) >= phaseEndStopSeconds) + shouldStopDps = true; + + // Keep DPS stopped for 5 seconds after transition into nature phase + auto itNatureDps = hydrossNatureDpsWaitTimer.find(instanceId); + if (itNatureDps != hydrossNatureDpsWaitTimer.end() && + (now - itNatureDps->second) < phaseStartStopSeconds) + shouldStopDps = true; + + // 1 second after 100% Mark of Corruption, stop DPS + auto itFrost = hydrossChangeToFrostPhaseTimer.find(instanceId); + if (itFrost != hydrossChangeToFrostPhaseTimer.end() && + (now - itFrost->second) >= phaseEndStopSeconds) + shouldStopDps = true; + + // Keep DPS stopped for 5 seconds after transition into frost phase + auto itFrostDps = hydrossFrostDpsWaitTimer.find(instanceId); + if (itFrostDps != hydrossFrostDpsWaitTimer.end() && + (now - itFrostDps->second) < phaseStartStopSeconds) + shouldStopDps = true; + + if (shouldStopDps) + { + botAI->Reset(); + return true; + } + + return false; +} + +bool HydrossTheUnstableManageTimersAction::Execute(Event /*event*/) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return false; + + const uint32 instanceId = hydross->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + bool changed = false; + if (!hydross->HasAura(SPELL_CORRUPTION)) + { + if (hydrossFrostDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) + changed = true; + if (HasMarkOfHydrossAt100Percent(bot)) + { + if (hydrossChangeToNaturePhaseTimer.try_emplace(instanceId, now).second) + changed = true; + } + } + else + { + if (hydrossNatureDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) + changed = true; + if (HasMarkOfCorruptionAt100Percent(bot)) + { + if (hydrossChangeToFrostPhaseTimer.try_emplace(instanceId, now).second) + changed = true; + } + } + + return changed; +} + +// The Lurker Below + +// Run around behind Lurker during Spout +bool TheLurkerBelowRunAroundBehindBossAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + float radius = frand(20.0f, 21.0f); + float botAngle = std::atan2( + bot->GetPositionY() - lurker->GetPositionY(), bot->GetPositionX() - lurker->GetPositionX()); + float relativeAngle = Position::NormalizeOrientation(botAngle - lurker->GetOrientation()); + constexpr float safeArc = M_PI / 2.0f; + + if (std::fabs(Position::NormalizeOrientation(relativeAngle - M_PI)) > safeArc / 2.0f) + { + float tangentAngle = botAngle + (relativeAngle > M_PI ? -0.1f : 0.1f); + float moveX = lurker->GetPositionX() + radius * std::cos(tangentAngle); + float moveY = lurker->GetPositionY() + radius * std::sin(tangentAngle); + botAI->Reset(); + return MoveTo(SSC_MAP_ID, moveX, moveY, lurker->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else + { + float behindAngle = lurker->GetOrientation() + M_PI + frand(-0.5f, 0.5f) * safeArc; + float targetX = lurker->GetPositionX() + radius * std::cos(behindAngle); + float targetY = lurker->GetPositionY() + radius * std::sin(behindAngle); + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + botAI->Reset(); + return MoveTo(SSC_MAP_ID, targetX, targetY, lurker->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool TheLurkerBelowPositionMainTankAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + if (bot->GetTarget() != lurker->GetGUID()) + return Attack(lurker); + + const Position& position = LURKER_MAIN_TANK_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 0.2f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// Assign ranged positions within a 120-degree arc behind Lurker +bool TheLurkerBelowSpreadRangedInArcAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + std::vector rangedMembers; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + } + + if (rangedMembers.empty()) + return false; + + const ObjectGuid guid = bot->GetGUID(); + + auto it = lurkerRangedPositions.find(guid); + if (it == lurkerRangedPositions.end()) + { + size_t count = rangedMembers.size(); + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = (findIt != rangedMembers.end()) ? + std::distance(rangedMembers.begin(), findIt) : 0; + + constexpr float arcSpan = 2.0f * M_PI / 3.0f; + constexpr float arcCenter = 2.262f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + float radius = 28.0f; + + float targetX = lurker->GetPositionX() + radius * std::sin(angle); + float targetY = lurker->GetPositionY() + radius * std::cos(angle); + + lurkerRangedPositions.try_emplace(guid, Position(targetX, targetY, lurker->GetPositionZ())); + it = lurkerRangedPositions.find(guid); + } + + if (it == lurkerRangedPositions.end()) + return false; + + const Position& position = it->second; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// During the submerge phase, if there are >= 3 tanks in the raid, +// the first 3 will each pick up 1 Guardian +bool TheLurkerBelowTanksPickUpAddsAction::Execute(Event /*event*/) +{ + Player* mainTank = nullptr; + Player* firstAssistTank = nullptr; + Player* secondAssistTank = nullptr; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (!mainTank && botAI->IsMainTank(member)) + mainTank = member; + else if (!firstAssistTank && botAI->IsAssistTankOfIndex(member, 0, true)) + firstAssistTank = member; + else if (!secondAssistTank && botAI->IsAssistTankOfIndex(member, 1, true)) + secondAssistTank = member; + } + } + + if (!mainTank || !firstAssistTank || !secondAssistTank) + return false; + + std::vector guardians; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_COILFANG_GUARDIAN) + guardians.push_back(unit); + } + + if (guardians.size() < 3) + return false; + + std::vector tanks = { mainTank, firstAssistTank, secondAssistTank }; + std::vector rtiIndices = + { + RtiTargetValue::starIndex, + RtiTargetValue::circleIndex, + RtiTargetValue::diamondIndex + }; + std::vector rtiNames = { "star", "circle", "diamond" }; + + for (size_t i = 0; i < 3; ++i) + { + Player* tank = tanks[i]; + Unit* guardian = guardians[i]; + if (bot == tank) + { + MarkTargetWithIcon(bot, guardian, rtiIndices[i]); + SetRtiTarget(botAI, rtiNames[i], guardian); + if (bot->GetTarget() != guardian->GetGUID()) + return Attack(guardian); + } + } + + return false; +} + +bool TheLurkerBelowManageSpoutTimerAction::Execute(Event /*event*/) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + const uint32 instanceId = lurker->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(instanceId); + if (it != lurkerSpoutTimer.end() && it->second <= now) + { + lurkerSpoutTimer.erase(it); + it = lurkerSpoutTimer.end(); + } + + const time_t spoutCastTime = 20; + if (IsLurkerCastingSpout(lurker) && it == lurkerSpoutTimer.end()) + lurkerSpoutTimer.try_emplace(instanceId, now + spoutCastTime); + + return false; +} + +// Leotheras the Blind + +bool LeotherasTheBlindTargetSpellbindersAction::Execute(Event /*event*/) +{ + if (Unit* spellbinder = GetFirstAliveUnitByEntry(botAI, NPC_GREYHEART_SPELLBINDER)) + MarkTargetWithSkull(bot, spellbinder); + + return false; +} + +// Warlock tank action--see GetLeotherasDemonFormTank in RaidSSCHelpers.cpp +// Use tank strategy for Demon Form and DPS strategy for Human Form +bool LeotherasTheBlindDemonFormTankAttackBossAction::Execute(Event /*event*/) +{ + Unit* innerDemon = nullptr; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (creature && creature->GetEntry() == NPC_INNER_DEMON + && creature->GetSummonerGUID() == bot->GetGUID()) + { + innerDemon = creature; + break; + } + } + + if (innerDemon) + return false; + + if (Unit* leotherasDemon = GetActiveLeotherasDemon(botAI)) + { + MarkTargetWithSquare(bot, leotherasDemon); + SetRtiTarget(botAI, "square", leotherasDemon); + + if (botAI->CanCastSpell("searing pain", leotherasDemon)) + return botAI->CastSpell("searing pain", leotherasDemon); + } + + return false; +} + +// Stop melee tanks from attacking upon transformation so they don't take aggro +// Applies only if there is a Warlock tank present +bool LeotherasTheBlindMeleeTanksDontAttackDemonFormAction::Execute(Event /*event*/) +{ + bot->AttackStop(); + botAI->Reset(); + return true; +} + +// Intent is to keep enough distance from Leotheras and spread to prepare for Whirlwind +// And stay away from the Warlock tank to avoid Chaos Blasts +bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) +{ + constexpr float safeDistFromBoss = 15.0f; + Unit* leotherasHuman = GetLeotherasHuman(botAI); + if (leotherasHuman && bot->GetExactDist2d(leotherasHuman) < safeDistFromBoss && + leotherasHuman->GetVictim() != bot) + { + constexpr uint32 minInterval = 500; + return FleePosition(leotherasHuman->GetPosition(), safeDistFromBoss, minInterval); + } + + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (GetActiveLeotherasDemon(botAI)) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + constexpr uint32 minInterval = 0; + if (GetLeotherasDemonFormTank(bot) == member) + { + constexpr float safeDistFromTank = 10.0f; + if (bot->GetExactDist2d(member) < safeDistFromTank) + return FleePosition(member->GetPosition(), safeDistFromTank, minInterval); + } + else + { + constexpr float safeDistFromMember = 6.0f; + if (bot->GetExactDist2d(member) < safeDistFromMember) + return FleePosition(member->GetPosition(), safeDistFromMember, minInterval); + } + } + } + + return false; +} + +bool LeotherasTheBlindRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + if (Unit* leotherasHuman = GetLeotherasHuman(botAI)) + { + float currentDistance = bot->GetExactDist2d(leotherasHuman); + constexpr float safeDistance = 25.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(leotherasHuman, safeDistance - currentDistance); + } + } + + return false; +} + +// This method is likely unnecessary unless the player does not use a Warlock tank +// If a melee tank is used, other melee needs to run away after too many Chaos Blast stacks +bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) +{ + if (botAI->CanCastSpell("cloak of shadows", bot)) + return botAI->CastSpell("cloak of shadows", bot); + + Unit* leotheras = GetPhase2LeotherasDemon(botAI); + if (!leotheras) + return false; + + Unit* demonVictim = leotheras->GetVictim(); + if (!demonVictim) + return false; + + float currentDistance = bot->GetExactDist2d(demonVictim); + constexpr float safeDistance = 10.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + if (demonVictim != bot) + return MoveAway(demonVictim, safeDistance - currentDistance); + } + + return false; +} + +// Hardcoded actions for healers and bear tanks to kill Inner Demons +bool LeotherasTheBlindDestroyInnerDemonAction::Execute(Event /*event*/) +{ + Unit* innerDemon = nullptr; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (creature && creature->GetEntry() == NPC_INNER_DEMON + && creature->GetSummonerGUID() == bot->GetGUID()) + { + innerDemon = creature; + break; + } + } + + if (innerDemon) + { + if (botAI->IsTank(bot) && bot->getClass() == CLASS_DRUID) + return HandleFeralTankStrategy(innerDemon); + + if (botAI->IsHeal(bot)) + return HandleHealerStrategy(innerDemon); + + // Roles without a strategy need to affirmatively attack their Inner Demons + // Because DPS assist is disabled via multipliers + if (bot->GetTarget() != innerDemon->GetGUID()) + return Attack(innerDemon); + } + + return false; +} + +// At 50% nerfed damage, bears have trouble killing their Inner Demons without a specific strategy +// Warrior and Paladin tanks have no trouble in my experience (Prot Warriors have high DPS, and +// Prot Paladins have an advantage in that Inner Demons are weak to Holy) +bool LeotherasTheBlindDestroyInnerDemonAction::HandleFeralTankStrategy(Unit* innerDemon) +{ + if (bot->HasAura(SPELL_DIRE_BEAR_FORM)) + bot->RemoveAura(SPELL_DIRE_BEAR_FORM); + + if (bot->HasAura(SPELL_BEAR_FORM)) + bot->RemoveAura(SPELL_BEAR_FORM); + + bool casted = false; + if (!bot->HasAura(SPELL_CAT_FORM) && botAI->CanCastSpell("cat form", bot)) + { + if (botAI->CastSpell("cat form", bot)) + casted = true; + } + if (botAI->CanCastSpell("berserk", bot)) + { + if (botAI->CastSpell("berserk", bot)) + casted = true; + } + if (bot->GetPower(POWER_ENERGY) < 30 && botAI->CanCastSpell("tiger's fury", bot)) + { + if (botAI->CastSpell("tiger's fury", bot)) + casted = true; + } + if (bot->GetComboPoints() >= 4 && botAI->CanCastSpell("ferocious bite", innerDemon)) + { + if (botAI->CastSpell("ferocious bite", innerDemon)) + casted = true; + } + if (bot->GetComboPoints() == 0 && innerDemon->GetHealthPct() > 25.0f && + botAI->CanCastSpell("rake", innerDemon)) + { + if (botAI->CastSpell("rake", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("mangle (cat)", innerDemon)) + { + if (botAI->CastSpell("mangle (cat)", innerDemon)) + casted = true; + } + + return casted; +} + +bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerDemon) +{ + if (bot->getClass() == CLASS_DRUID) + { + if (bot->HasAura(SPELL_TREE_OF_LIFE)) + bot->RemoveAura(SPELL_TREE_OF_LIFE); + + bool casted = false; + if (botAI->CanCastSpell("barkskin", bot)) + { + if (botAI->CastSpell("barkskin", bot)) + casted = true; + } + if (botAI->CanCastSpell("wrath", innerDemon)) + { + if (botAI->CastSpell("wrath", innerDemon)) + casted = true; + } + + return casted; + } + else if (bot->getClass() == CLASS_PALADIN) + { + bool casted = false; + if (botAI->CanCastSpell("avenging wrath", bot)) + { + if (botAI->CastSpell("avenging wrath", bot)) + casted = true; + } + if (botAI->CanCastSpell("consecration", bot)) + { + if (botAI->CastSpell("consecration", bot)) + casted = true; + } + if (botAI->CanCastSpell("exorcism", innerDemon)) + { + if (botAI->CastSpell("exorcism", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("hammer of wrath", innerDemon)) + { + if (botAI->CastSpell("hammer of wrath", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("holy shock", innerDemon)) + { + if (botAI->CastSpell("holy shock", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("judgment of light", innerDemon)) + { + if (botAI->CastSpell("judgment of light", innerDemon)) + casted = true; + } + + return casted; + } + else if (bot->getClass() == CLASS_PRIEST) + { + if (botAI->CanCastSpell("smite", innerDemon)) + return botAI->CastSpell("smite", innerDemon); + } + else if (bot->getClass() == CLASS_SHAMAN) + { + bool casted = false; + if (botAI->CanCastSpell("earth shock", innerDemon)) + { + if (botAI->CastSpell("earth shock", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("chain lightning", innerDemon)) + { + if (botAI->CastSpell("chain lightning", innerDemon)) + casted = true; + } + if (botAI->CanCastSpell("lightning bolt", innerDemon)) + { + if (botAI->CastSpell("lightning bolt", innerDemon)) + casted = true; + } + + return casted; + } + + return false; +} + +// Everybody except the Warlock tank should focus on Leotheras in Phase 3 +bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/) +{ + Unit* leotherasHuman = GetLeotherasHuman(botAI); + if (!leotherasHuman) + return false; + + MarkTargetWithStar(bot, leotherasHuman); + SetRtiTarget(botAI, "star", leotherasHuman); + + if (bot->GetTarget() != leotherasHuman->GetGUID()) + return Attack(leotherasHuman); + + Unit* leotherasDemon = GetPhase3LeotherasDemon(botAI); + if (leotherasDemon) + { + if (leotherasHuman->GetVictim() == bot) + { + Unit* demonTarget = leotherasDemon->GetVictim(); + if (demonTarget && leotherasHuman->GetExactDist2d(demonTarget) < 20.0f) + { + float angle = atan2(bot->GetPositionY() - demonTarget->GetPositionY(), + bot->GetPositionX() - demonTarget->GetPositionX()); + float targetX = bot->GetPositionX() + 25.0f * std::cos(angle); + float targetY = bot->GetPositionY() + 25.0f * std::sin(angle); + + return MoveTo(SSC_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + return false; +} + +// Misdirect to Warlock tank or to main tank if there is no Warlock tank +bool LeotherasTheBlindMisdirectBossToDemonFormTankAction::Execute(Event /*event*/) +{ + Unit* leotherasDemon = GetActiveLeotherasDemon(botAI); + if (!leotherasDemon) + return false; + + Player* demonFormTank = GetLeotherasDemonFormTank(bot); + Player* targetTank = demonFormTank; + + if (!targetTank) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + targetTank = member; + break; + } + } + } + } + + if (!targetTank) + return false; + + if (botAI->CanCastSpell("misdirection", targetTank)) + return botAI->CastSpell("misdirection", targetTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", leotherasDemon)) + return botAI->CastSpell("steady shot", leotherasDemon); + + return false; +} + +// This does not pause DPS after a Whirlwind, which is also an aggro wipe +bool LeotherasTheBlindManageDpsWaitTimersAction::Execute(Event /*event*/) +{ + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) + return false; + + const uint32 instanceId = leotheras->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + bool changed = false; + // Encounter start/reset: clear all timers + if (leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + { + if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (leotherasFinalPhaseDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + + // Human Phase + Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); + if (leotherasHuman && !leotherasPhase3Demon) + { + if (leotherasHumanFormDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + // Demon Phase + else if (Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI)) + { + if (leotherasDemonFormDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + // Final Phase (<15% HP) + else if (leotherasHuman && leotherasPhase3Demon) + { + if (leotherasFinalPhaseDpsWaitTimer.try_emplace(instanceId, now).second) + changed = true; + if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) + changed = true; + } + + return changed; +} + +// Fathom-Lord Karathress +// Note: 4 tanks are required for the full strategy, and having at least 2 +// is crucial to separate Caribdis from the others + +// Karathress is tanked near his starting position +bool FathomLordKarathressMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (!karathress) + return false; + + MarkTargetWithTriangle(bot, karathress); + SetRtiTarget(botAI, "triangle", karathress); + + if (bot->GetTarget() != karathress->GetGUID()) + return Attack(karathress); + + if (karathress->GetVictim() == bot && bot->IsWithinMeleeRange(karathress)) + { + const Position& position = KARATHRESS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Caribdis is pulled far to the West in the corner +// Best to use a Warrior or Druid tank for interrupts +bool FathomLordKarathressFirstAssistTankPositionCaribdisAction::Execute(Event /*event*/) +{ + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (!caribdis) + return false; + + MarkTargetWithDiamond(bot, caribdis); + SetRtiTarget(botAI, "diamond", caribdis); + + if (bot->GetTarget() != caribdis->GetGUID()) + return Attack(caribdis); + + if (caribdis->GetVictim() == bot) + { + const Position& position = CARIBDIS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// Sharkkis is pulled North to the other side of the ramp +bool FathomLordKarathressSecondAssistTankPositionSharkkisAction::Execute(Event /*event*/) +{ + Unit* sharkkis = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + if (!sharkkis) + return false; + + MarkTargetWithStar(bot, sharkkis); + SetRtiTarget(botAI, "star", sharkkis); + + if (bot->GetTarget() != sharkkis->GetGUID()) + return Attack(sharkkis); + + if (sharkkis->GetVictim() == bot && bot->IsWithinMeleeRange(sharkkis)) + { + const Position& position = SHARKKIS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Tidalvess is pulled Northwest near the pillar +bool FathomLordKarathressThirdAssistTankPositionTidalvessAction::Execute(Event /*event*/) +{ + Unit* tidalvess = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + if (!tidalvess) + return false; + + MarkTargetWithCircle(bot, tidalvess); + SetRtiTarget(botAI, "circle", tidalvess); + + if (bot->GetTarget() != tidalvess->GetGUID()) + return Attack(tidalvess); + + if (tidalvess->GetVictim() == bot && bot->IsWithinMeleeRange(tidalvess)) + { + const Position& position = TIDALVESS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Caribdis's tank spot is far away so a dedicated healer is needed +// Use the assistant flag to select the healer +bool FathomLordKarathressPositionCaribdisTankHealerAction::Execute(Event /*event*/) +{ + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (!caribdis) + return false; + + const Position& position = CARIBDIS_HEALER_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Misdirect priority: (1) Caribdis tank, (2) Tidalvess tank, (3) Sharkkis tank +bool FathomLordKarathressMisdirectBossesToTanksAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && + GET_PLAYERBOT_AI(member)) + hunters.push_back(member); + + if (hunters.size() >= 3) + break; + } + + int hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* bossTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 0, false)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 1) + { + bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 2, false)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 1, false)) + { + tankTarget = member; + break; + } + } + } + + if (!bossTarget || !tankTarget) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", bossTarget)) + return botAI->CastSpell("steady shot", bossTarget); + + return false; +} + +// Kill order is non-standard because bots handle Cyclones poorly and need more time +// to get her down than real players (standard is ranged DPS help with Sharkkis first) +bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Spitfire Totems for melee dps + Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_SPITFIRE_TOTEM); + if (totem && botAI->IsMelee(bot) && botAI->IsDps(bot)) + { + MarkTargetWithSkull(bot, totem); + SetRtiTarget(botAI, "skull", totem); + + if (bot->GetTarget() != totem->GetGUID()) + return Attack(totem); + + // Direct movement order due to path between Sharkkis and totem sometimes being screwy + if (!bot->IsWithinMeleeRange(totem)) + { + return MoveTo(SSC_MAP_ID, totem->GetPositionX(), totem->GetPositionY(), + totem->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; + } + + // Target priority 2: Tidalvess for all dps + Unit* tidalvess = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + if (tidalvess) + { + MarkTargetWithCircle(bot, tidalvess); + SetRtiTarget(botAI, "circle", tidalvess); + + if (bot->GetTarget() != tidalvess->GetGUID()) + return Attack(tidalvess); + + return false; + } + + // Target priority 3: Caribdis for ranged dps + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (botAI->IsRangedDps(bot) && caribdis) + { + MarkTargetWithDiamond(bot, caribdis); + SetRtiTarget(botAI, "diamond", caribdis); + + const Position& position = CARIBDIS_RANGED_DPS_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveInside(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), 8.0f, MovementPriority::MOVEMENT_COMBAT); + } + + if (bot->GetTarget() != caribdis->GetGUID()) + return Attack(caribdis); + + return false; + } + + // Target priority 4: Sharkkis for melee dps and, after Caribdis is down, ranged dps also + Unit* sharkkis = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + if (sharkkis) + { + MarkTargetWithStar(bot, sharkkis); + SetRtiTarget(botAI, "star", sharkkis); + + if (bot->GetTarget() != sharkkis->GetGUID()) + return Attack(sharkkis); + + return false; + } + + // Target priority 5: Sharkkis pets for all dps + Unit* fathomSporebat = AI_VALUE2(Unit*, "find target", "fathom sporebat"); + if (fathomSporebat && botAI->IsMelee(bot)) + { + MarkTargetWithCross(bot, fathomSporebat); + SetRtiTarget(botAI, "cross", fathomSporebat); + + if (bot->GetTarget() != fathomSporebat->GetGUID()) + return Attack(fathomSporebat); + + return false; + } + + Unit* fathomLurker = AI_VALUE2(Unit*, "find target", "fathom lurker"); + if (fathomLurker && botAI->IsMelee(bot)) + { + MarkTargetWithSquare(bot, fathomLurker); + SetRtiTarget(botAI, "square", fathomLurker); + + if (bot->GetTarget() != fathomLurker->GetGUID()) + return Attack(fathomLurker); + + return false; + } + + // Target priority 6: Karathress for all dps + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (karathress) + { + MarkTargetWithTriangle(bot, karathress); + SetRtiTarget(botAI, "triangle", karathress); + + if (bot->GetTarget() != karathress->GetGUID()) + return Attack(karathress); + } + + return false; +} + +bool FathomLordKarathressManageDpsTimerAction::Execute(Event /*event*/) +{ + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (!karathress) + return false; + + karathressDpsWaitTimer.try_emplace( + karathress->GetMap()->GetInstanceId(), std::time(nullptr)); + + return false; +} + +// Morogrim Tidewalker + +bool MorogrimTidewalkerMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + 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 && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", tidewalker)) + return botAI->CastSpell("steady shot", tidewalker); + + return false; +} + +// Separate tanking positions are used for phase 1 and phase 2 to address the +// Water Globule mechanic in phase 2 +bool MorogrimTidewalkerMoveBossToTankPositionAction::Execute(Event /*event*/) +{ + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return false; + + if (bot->GetTarget() != tidewalker->GetGUID()) + return Attack(tidewalker); + + if (tidewalker->GetVictim() == bot && bot->IsWithinMeleeRange(tidewalker)) + { + if (tidewalker->GetHealthPct() > 26.0f) + return MoveToPhase1TankPosition(tidewalker); + else + return MoveToPhase2TankPosition(tidewalker); + } + + return false; +} + +// Phase 1: tank position is up against the Northeast pillar +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition(Unit* tidewalker) +{ + const Position& phase1 = TIDEWALKER_PHASE_1_TANK_POSITION; + float distToPhase1 = bot->GetExactDist2d(phase1.GetPositionX(), phase1.GetPositionY()); + if (distToPhase1 > 1.0f) + { + float dX = phase1.GetPositionX() - bot->GetPositionX(); + float dY = phase1.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPhase1); + float moveX = bot->GetPositionX() + (dX / distToPhase1) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPhase1) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, phase1.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +// Phase 2: move in two steps to get around the pillar and back up into the Northeast corner +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase2TankPosition(Unit* tidewalker) +{ + const Position& phase2 = TIDEWALKER_PHASE_2_TANK_POSITION; + const Position& transition = TIDEWALKER_PHASE_TRANSITION_WAYPOINT; + + auto itStep = tidewalkerTankStep.find(bot->GetGUID()); + uint8 step = (itStep != tidewalkerTankStep.end()) ? itStep->second : 0; + + if (step == 0) + { + float distToTransition = + bot->GetExactDist2d(transition.GetPositionX(), transition.GetPositionY()); + + if (distToTransition > 2.0f) + { + float dX = transition.GetPositionX() - bot->GetPositionX(); + float dY = transition.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToTransition); + float moveX = bot->GetPositionX() + (dX / distToTransition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToTransition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, transition.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + else + tidewalkerTankStep.try_emplace(bot->GetGUID(), 1); + } + + if (step == 1) + { + float distToPhase2 = + bot->GetExactDist2d(phase2.GetPositionX(), phase2.GetPositionY()); + + if (distToPhase2 > 1.0f) + { + float dX = phase2.GetPositionX() - bot->GetPositionX(); + float dY = phase2.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPhase2); + float moveX = bot->GetPositionX() + (dX / distToPhase2) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPhase2) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, phase2.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Ranged stack behind the boss in the Northeast corner in phase 2 +// No corresponding method for melee since they will do so anyway +bool MorogrimTidewalkerPhase2RepositionRangedAction::Execute(Event /*event*/) +{ + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return false; + + const Position& phase2 = TIDEWALKER_PHASE_2_RANGED_POSITION; + const Position& transition = TIDEWALKER_PHASE_TRANSITION_WAYPOINT; + + auto itStep = tidewalkerRangedStep.find(bot->GetGUID()); + uint8 step = (itStep != tidewalkerRangedStep.end()) ? itStep->second : 0; + + if (step == 0) + { + float distToTransition = + bot->GetExactDist2d(transition.GetPositionX(), transition.GetPositionY()); + + if (distToTransition > 2.0f) + { + float dX = transition.GetPositionX() - bot->GetPositionX(); + float dY = transition.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToTransition); + float moveX = bot->GetPositionX() + (dX / distToTransition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToTransition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, transition.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + tidewalkerRangedStep.try_emplace(bot->GetGUID(), 1); + step = 1; + } + } + + if (step == 1) + { + float distToPhase2 = + bot->GetExactDist2d(phase2.GetPositionX(), phase2.GetPositionY()); + + if (distToPhase2 > 1.0f) + { + float dX = phase2.GetPositionX() - bot->GetPositionX(); + float dY = phase2.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPhase2); + float moveX = bot->GetPositionX() + (dX / distToPhase2) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPhase2) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, phase2.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// Lady Vashj + +bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + if (bot->GetTarget() != vashj->GetGUID()) + return Attack(vashj); + + if (vashj->GetVictim() == bot && bot->IsWithinMeleeRange(vashj)) + { + // Phase 1: Position Vashj in the center of the platform + if (IsLadyVashjInPhase1(botAI)) + { + const Position& position = VASHJ_PLATFORM_CENTER_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(SSC_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + // Phase 3: No fixed position, but move Vashj away from Enchanted Elementals + else if (IsLadyVashjInPhase3(botAI)) + { + Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); + if (enchanted) + { + float currentDistance = bot->GetExactDist2d(enchanted); + constexpr float safeDistance = 10.0f; + if (currentDistance < safeDistance) + return MoveAway(enchanted, safeDistance - currentDistance); + } + } + } + + return false; +} + +// Semicircle around center of the room (to allow escape paths by Static Charged bots) +bool LadyVashjPhase1SpreadRangedInArcAction::Execute(Event /*event*/) +{ + std::vector spreadMembers; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member)) + { + if (botAI->IsRanged(member)) + spreadMembers.push_back(member); + } + } + } + + const ObjectGuid guid = bot->GetGUID(); + + auto itPos = vashjRangedPositions.find(guid); + auto itReached = hasReachedVashjRangedPosition.find(guid); + if (itPos == vashjRangedPositions.end()) + { + auto it = std::find(spreadMembers.begin(), spreadMembers.end(), bot); + size_t botIndex = (it != spreadMembers.end()) ? + std::distance(spreadMembers.begin(), it) : 0; + size_t count = spreadMembers.size(); + if (count == 0) + return false; + + const Position& center = VASHJ_PLATFORM_CENTER_POSITION; + constexpr float minRadius = 20.0f; + constexpr float maxRadius = 30.0f; + + constexpr float arcCenter = M_PI / 2.0f; // North + constexpr float arcSpan = M_PI; // 180° + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + float angle; + if (count == 1) + angle = arcCenter; + else + angle = arcStart + (static_cast(botIndex) / (count - 1)) * arcSpan; + + float radius = frand(minRadius, maxRadius); + float targetX = center.GetPositionX() + radius * std::cos(angle); + float targetY = center.GetPositionY() + radius * std::sin(angle); + + auto res = vashjRangedPositions.try_emplace(guid, Position(targetX, targetY, center.GetPositionZ())); + itPos = res.first; + hasReachedVashjRangedPosition.try_emplace(guid, false); + itReached = hasReachedVashjRangedPosition.find(guid); + } + + if (itPos == vashjRangedPositions.end()) + return false; + + Position position = itPos->second; + if (itReached == hasReachedVashjRangedPosition.end() || !(itReached->second)) + { + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + if (itReached != hasReachedVashjRangedPosition.end()) + itReached->second = true; + } + + return false; +} + +// For absorbing Shock Burst +bool LadyVashjSetGroundingTotemInMainTankGroupAction::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 && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (!mainTank) + return false; + + if (bot->GetExactDist2d(mainTank) > 25.0f) + { + return MoveInside(SSC_MAP_ID, mainTank->GetPositionX(), mainTank->GetPositionY(), + mainTank->GetPositionZ(), 20.0f, MovementPriority::MOVEMENT_COMBAT); + } + + if (!botAI->HasStrategy("grounding", BotState::BOT_STATE_COMBAT)) + botAI->ChangeStrategy("+grounding", BotState::BOT_STATE_COMBAT); + + if (!bot->HasAura(SPELL_GROUNDING_TOTEM_EFFECT) && + botAI->CanCastSpell("grounding totem", bot)) + return botAI->CastSpell("grounding totem", bot); + + return false; +} + +bool LadyVashjMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + 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 && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", vashj)) + return botAI->CastSpell("steady shot", vashj); + + return false; +} + +bool LadyVashjStaticChargeMoveAwayFromGroupAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // If the main tank has Static Charge, other group members should move away + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member) && + member->HasAura(SPELL_STATIC_CHARGE)) + { + mainTank = member; + break; + } + } + + if (mainTank && bot != mainTank) + { + float currentDistance = bot->GetExactDist2d(mainTank); + constexpr float safeDistance = 11.0f; + if (currentDistance < safeDistance) + return MoveAway(mainTank, safeDistance - currentDistance); + } + + // If any other bot has Static Charge, it should move away from other group members + if (!botAI->IsMainTank(bot) && bot->HasAura(SPELL_STATIC_CHARGE)) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float currentDistance = bot->GetExactDist2d(member); + constexpr float safeDistance = 11.0f; + if (currentDistance < safeDistance) + return MoveFromGroup(safeDistance); + } + } + + return false; +} + +bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + const Position& center = VASHJ_PLATFORM_CENTER_POSITION; + float platformZ = center.GetPositionZ(); + if (bot->GetPositionZ() - platformZ > 2.0f) + { + // This block is needed to prevent bots from floating into the air to attack sporebats + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->StopMoving(); + bot->GetMotionMaster()->Clear(); + bot->TeleportTo(SSC_MAP_ID, bot->GetPositionX(), bot->GetPositionY(), + platformZ, bot->GetOrientation()); + return true; + } + + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + Unit* target = nullptr; + Unit* enchanted = nullptr; + Unit* elite = nullptr; + Unit* strider = nullptr; + Unit* sporebat = nullptr; + + // Search and attack radius are intended to keep bots from going down the stairs + const float maxSearchRange = + botAI->IsRanged(bot) ? 60.0f : 55.0f; + const float maxPursueRange = maxSearchRange - 5.0f; + + for (auto guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!IsValidLadyVashjCombatNpc(unit, botAI)) + continue; + + float distFromCenter = unit->GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + if (IsLadyVashjInPhase2(botAI) && distFromCenter > maxSearchRange) + continue; + + switch (unit->GetEntry()) + { + case NPC_ENCHANTED_ELEMENTAL: + if (!enchanted || vashj->GetExactDist2d(unit) < vashj->GetExactDist2d(enchanted)) + enchanted = unit; + break; + + case NPC_COILFANG_ELITE: + if (!elite || unit->GetHealthPct() < elite->GetHealthPct()) + elite = unit; + break; + + case NPC_COILFANG_STRIDER: + if (!strider || unit->GetHealthPct() < strider->GetHealthPct()) + strider = unit; + break; + + case NPC_TOXIC_SPOREBAT: + if (!sporebat || bot->GetExactDist2d(unit) < bot->GetExactDist2d(sporebat)) + sporebat = unit; + break; + + case NPC_LADY_VASHJ: + vashj = unit; + break; + + default: + break; + } + } + + std::vector targets; + if (IsLadyVashjInPhase2(botAI)) + { + if (botAI->IsRanged(bot)) + { + // Hunters and Mages prioritize Enchanted Elementals, + // while other ranged DPS prioritize Striders + if (bot->getClass() == CLASS_HUNTER || bot->getClass() == CLASS_MAGE) + targets = { enchanted, strider, elite }; + else + targets = { strider, elite, enchanted }; + } + else if (botAI->IsMelee(bot) && botAI->IsDps(bot)) + targets = { enchanted, elite }; + else if (botAI->IsTank(bot)) + { + if (botAI->HasCheat(BotCheatMask::raid) && botAI->IsAssistTankOfIndex(bot, 0, true)) + targets = { strider, elite, enchanted }; + else + targets = { elite, strider, enchanted }; + } + else + targets = { enchanted, elite, strider }; + } + + if (IsLadyVashjInPhase3(botAI)) + { + if (botAI->IsTank(bot)) + { + if (botAI->IsMainTank(bot)) + { + MarkTargetWithDiamond(bot, vashj); + SetRtiTarget(botAI, "diamond", vashj); + targets = { vashj }; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (botAI->HasCheat(BotCheatMask::raid)) + targets = { strider, elite, enchanted, vashj }; + } + else + targets = { elite, strider, enchanted, vashj }; + } + else if (botAI->IsRanged(bot)) + { + // Hunters are assigned to kill Sporebats in Phase 3 + if (bot->getClass() == CLASS_HUNTER) + targets = { sporebat, enchanted, strider, elite, vashj }; + else + targets = { enchanted, strider, elite, vashj }; + } + else if (botAI->IsMelee(bot) && botAI->IsDps(bot)) + targets = { enchanted, elite, vashj }; + else + targets = { enchanted, elite, strider, vashj }; + } + + for (Unit* candidate : targets) + { + if (candidate && bot->GetExactDist2d(candidate) <= maxPursueRange) + { + target = candidate; + break; + } + } + + Unit* currentTarget = context->GetValue("current target")->Get(); + + if (currentTarget && !IsValidLadyVashjCombatNpc(currentTarget, botAI)) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + context->GetValue("current target")->Set(nullptr); + bot->SetTarget(ObjectGuid::Empty); + bot->SetSelection(ObjectGuid()); + currentTarget = nullptr; + } + + if (target && currentTarget != target && bot->GetTarget() != target->GetGUID()) + return Attack(target); + + // If bots have wandered too far from the center, move them back + if (bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 55.0f) + { + Player* designatedLooter = GetDesignatedCoreLooter(bot->GetGroup(), botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(bot->GetGroup(), botAI); + // A bot will not move back to the middle if (1) there is a Tainted Elemental, and + // (2) the bot is either the designated looter or the first core passer + if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) + { + if ((designatedLooter && designatedLooter == bot) || + (firstCorePasser && firstCorePasser == bot)) + return false; + } + + return MoveInside(SSC_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), 40.0f, MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool LadyVashjMisdirectStriderToFirstAssistTankAction::Execute(Event /*event*/) +{ + // Striders are not tankable without a cheat to block Fear so there is + // no point in misdirecting if raid cheats are not enabled + if (!botAI->HasCheat(BotCheatMask::raid)) + return false; + + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + if (!strider) + return false; + + Player* firstAssistTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsAssistTankOfIndex(member, 0, true)) + { + firstAssistTank = member; + break; + } + } + } + + if (!firstAssistTank || strider->GetVictim() == firstAssistTank) + return false; + + if (botAI->CanCastSpell("misdirection", firstAssistTank)) + return botAI->CastSpell("misdirection", firstAssistTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", strider)) + return botAI->CastSpell("steady shot", strider); + + return false; +} + +bool LadyVashjTankAttackAndMoveAwayStriderAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + if (!strider) + return false; + + // Raid cheat automatically applies Fear Ward to tanks to make Strider tankable + // This simulates the real-life strategy where the Strider can be meleed by + // players wearing an Ogre Suit (due to the extended combat reach) + if (botAI->HasCheat(BotCheatMask::raid) && botAI->IsTank(bot)) + { + if (!bot->HasAura(SPELL_FEAR_WARD)) + bot->AddAura(SPELL_FEAR_WARD, bot); + + if (botAI->IsAssistTankOfIndex(bot, 0, true) && + bot->GetTarget() != strider->GetGUID()) + return Attack(strider); + + if (strider->GetVictim() == bot) + { + float currentDistance = bot->GetExactDist2d(vashj); + constexpr float safeDistance = 28.0f; + + if (currentDistance < safeDistance) + return MoveAway(vashj, safeDistance - currentDistance); + } + + return false; + } + + // Don't move away if raid cheats are enabled, or in any case if the bot is a tank + if (!botAI->HasCheat(BotCheatMask::raid) || !botAI->IsTank(bot)) + { + float currentDistance = bot->GetExactDist2d(strider); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + return MoveAway(strider, safeDistance - currentDistance); + } + + // Try to root/slow the Strider if it is not tankable (poor man's kiting strategy) + if (!botAI->HasCheat(BotCheatMask::raid)) + { + if (!strider->HasAura(SPELL_HEAVY_NETHERWEAVE_NET)) + { + Item* net = bot->GetItemByEntry(ITEM_HEAVY_NETHERWEAVE_NET); + if (net && botAI->HasItemInInventory(ITEM_HEAVY_NETHERWEAVE_NET) && + botAI->CanCastSpell("heavy netherweave net", strider)) + return botAI->CastSpell("heavy netherweave net", strider); + } + + if (!botAI->HasAura("frost shock", strider) && bot->getClass() == CLASS_SHAMAN && + botAI->CanCastSpell("frost shock", strider)) + return botAI->CastSpell("frost shock", strider); + + if (!strider->HasAura(SPELL_CURSE_OF_EXHAUSTION) && bot->getClass() == CLASS_WARLOCK && + botAI->CanCastSpell("curse of exhaustion", strider)) + return botAI->CastSpell("curse of exhaustion", strider); + + if (!strider->HasAura(SPELL_SLOW) && bot->getClass() == CLASS_MAGE && + botAI->CanCastSpell("slow", strider)) + return botAI->CastSpell("slow", strider); + } + + return false; +} + +// If cheats are enabled, the first returned melee DPS bot will teleport to Tainted Elementals +// Such bot will recover HP and remove the Poison Bolt debuff while attacking the elemental +bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) +{ + Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (!tainted) + return false; + + if (bot->GetExactDist2d(tainted) >= 10.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->TeleportTo(SSC_MAP_ID, tainted->GetPositionX(), tainted->GetPositionY(), + tainted->GetPositionZ(), tainted->GetOrientation()); + } + + if (bot->GetTarget() != tainted->GetGUID()) + { + MarkTargetWithStar(bot, tainted); + SetRtiTarget(botAI, "star", tainted); + return Attack(tainted); + } + + if (bot->GetExactDist2d(tainted) < 5.0f) + { + bot->SetFullHealth(); + bot->RemoveAura(SPELL_POISON_BOLT); + } + + return false; +} + +bool LadyVashjLootTaintedCoreAction::Execute(Event) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + auto const& corpses = context->GetValue("nearest corpses")->Get(); + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + + for (auto const& guid : corpses) + { + LootObject loot(bot, guid); + if (!loot.IsLootPossible(bot)) + continue; + + WorldObject* object = loot.GetWorldObject(bot); + if (!object) + continue; + + Creature* creature = object->ToCreature(); + if (!creature || creature->GetEntry() != NPC_TAINTED_ELEMENTAL || creature->IsAlive()) + continue; + + context->GetValue("loot target")->Set(loot); + + float dist = bot->GetDistance(object); + if (dist > maxLootRange) + return MoveTo(object, 2.0f, MovementPriority::MOVEMENT_FORCED); + + OpenLootAction open(botAI); + if (!open.Execute(Event())) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return true; + } + } + + const ObjectGuid botGuid = bot->GetGUID(); + const ObjectGuid corpseGuid = guid; + constexpr uint8 coreIndex = 0; + + botAI->AddTimedEvent([botGuid, corpseGuid, coreIndex, vashj]() + { + Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); + if (!receiver) + return; + + if (Group* group = receiver->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return; + } + } + + receiver->SetLootGUID(corpseGuid); + + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << coreIndex; + receiver->GetSession()->QueuePacket(packet); + + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + }, 600); + + return true; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + if (!designatedLooter) + return false; + + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + + Unit* closestTrigger = nullptr; + if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) + { + closestTrigger = GetNearestActiveShieldGeneratorTriggerByEntry(tainted); + if (closestTrigger) + nearestTriggerGuid.insert_or_assign(instanceId, closestTrigger->GetGUID()); + } + + auto itSnap = nearestTriggerGuid.find(instanceId); + if (itSnap != nearestTriggerGuid.end() && !itSnap->second.IsEmpty()) + { + Unit* snapUnit = botAI->GetUnit(itSnap->second); + if (snapUnit) + closestTrigger = snapUnit; + else + nearestTriggerGuid.erase(instanceId); + } + + if (!firstCorePasser || !secondCorePasser || !thirdCorePasser || + !fourthCorePasser || !closestTrigger) + return false; + + // Not gated behind CheatMask because the auto application of Fear Ward is necessary + // to address an issue with bot movement, which is that bots cannot be rooted and + // therefore will move when feared while holding the Tainted Core + if (!bot->HasAura(SPELL_FEAR_WARD)) + bot->AddAura(SPELL_FEAR_WARD, bot); + + Item* item = bot->GetItemByEntry(ITEM_TAINTED_CORE); + if (!item || !botAI->HasItemInInventory(ITEM_TAINTED_CORE)) + { + // Passer order: HealAssistantOfIndex 0, 1, 2, then RangedDpsAssistantOfIndex 0 + if (bot == firstCorePasser) + { + if (LineUpFirstCorePasser(designatedLooter, closestTrigger)) + return true; + } + else if (bot == secondCorePasser) + { + if (LineUpSecondCorePasser(firstCorePasser, closestTrigger)) + return true; + } + else if (bot == thirdCorePasser) + { + if (LineUpThirdCorePasser(designatedLooter, firstCorePasser, + secondCorePasser, closestTrigger)) + return true; + } + else if (bot == fourthCorePasser) + { + if (LineUpFourthCorePasser(firstCorePasser, secondCorePasser, + thirdCorePasser, closestTrigger)) + return true; + } + } + else if (item && botAI->HasItemInInventory(ITEM_TAINTED_CORE)) + { + // Designated core looter logic + // Applicable only if cheat mode is on and thus looter is a bot + if (bot == designatedLooter) + { + if (IsFirstCorePasserInIntendedPosition( + designatedLooter, firstCorePasser, closestTrigger)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, firstCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, firstCorePasser); + return true; + } + } + } + // First core passer: receive core from looter at the top of the stairs, + // pass to second core passer + else if (bot == firstCorePasser) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, secondCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, secondCorePasser); + return true; + } + } + // Second core passer: if closest usable generator is within passing distance + // of the first passer, move to the generator; otherwise, move as close as + // possible to the generator while staying in passing range + else if (bot == secondCorePasser) + { + if (!UseCoreOnNearestGenerator(instanceId)) + { + if (IsThirdCorePasserInIntendedPosition( + secondCorePasser, thirdCorePasser, closestTrigger)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, thirdCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, thirdCorePasser); + return true; + } + } + } + } + // Third core passer: if closest usable generator is within passing distance + // of the second passer, move to the generator; otherwise, move as close as + // possible to the generator while staying in passing range + else if (bot == thirdCorePasser) + { + if (!UseCoreOnNearestGenerator(instanceId)) + { + if (IsFourthCorePasserInIntendedPosition( + thirdCorePasser, fourthCorePasser, closestTrigger)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + lastCoreInInventoryTime.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, fourthCorePasser); + intendedLineup.erase(bot->GetGUID()); + ScheduleTransferCoreAfterImbue(botAI, bot, fourthCorePasser); + return true; + } + } + } + } + // Fourth core passer: the fourth passer is rarely needed and no more than + // four ever should be, so it should use the Core on the nearest generator + else if (bot == fourthCorePasser) + UseCoreOnNearestGenerator(instanceId); + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( + Player* designatedLooter, Unit* closestTrigger) +{ + const float centerX = VASHJ_PLATFORM_CENTER_POSITION.GetPositionX(); + const float centerY = VASHJ_PLATFORM_CENTER_POSITION.GetPositionY(); + constexpr float radius = 57.5f; + + float mx = designatedLooter->GetPositionX(); + float my = designatedLooter->GetPositionY(); + float angle = atan2(my - centerY, mx - centerX); + + float targetX = centerX + radius * std::cos(angle); + float targetY = centerY + radius * std::sin(angle); + constexpr float targetZ = 41.097f; + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, targetZ)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpSecondCorePasser( + Player* firstCorePasser, Unit* closestTrigger) +{ + float fx = firstCorePasser->GetPositionX(); + float fy = firstCorePasser->GetPositionY(); + + float dx = closestTrigger->GetPositionX() - fx; + float dy = closestTrigger->GetPositionY() - fy; + float distToTrigger = firstCorePasser->GetExactDist2d(closestTrigger); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + // Target is on a line between firstCorePasser and closestTrigger + float targetX, targetY, targetZ; + // If firstCorePasser is within thresholdDist of closestTrigger, + // go to nearTriggerDist short of closestTrigger + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + // If firstCorePasser is not thresholdDist yards from closestTrigger, + // go to farDistance from firstCorePasser + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = fx + dx * moveDist; + targetY = fy + dy * moveDist; + } + else + { + targetX = fx + dx * farDistance; + targetY = fy + dy * farDistance; + } + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpThirdCorePasser( + Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) +{ + // Wait to move until it is clear that a third passer is needed + bool needThird = + (IsFirstCorePasserInIntendedPosition(designatedLooter, firstCorePasser, closestTrigger) && + firstCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || + (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + secondCorePasser->GetExactDist2d(closestTrigger) > 4.0f); + + if (!needThird) + return false; + + float sx = secondCorePasser->GetPositionX(); + float sy = secondCorePasser->GetPositionY(); + + float dx = closestTrigger->GetPositionX() - sx; + float dy = closestTrigger->GetPositionY() - sy; + float distToTrigger = secondCorePasser->GetExactDist2d(closestTrigger); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + float targetX, targetY, targetZ; + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = sx + dx * moveDist; + targetY = sy + dy * moveDist; + } + else + { + targetX = sx + dx * farDistance; + targetY = sy + dy * farDistance; + } + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::LineUpFourthCorePasser( + Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) +{ + // Wait to move until it is clear that a fourth passer is needed + bool needFourth = + (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + secondCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || + (IsThirdCorePasserInIntendedPosition(secondCorePasser, thirdCorePasser, closestTrigger) && + thirdCorePasser->GetExactDist2d(closestTrigger) > 4.0f); + + if (!needFourth) + return false; + + float sx = thirdCorePasser->GetPositionX(); + float sy = thirdCorePasser->GetPositionY(); + + float tx = closestTrigger->GetPositionX(); + float ty = closestTrigger->GetPositionY(); + + float dx = tx - sx; + float dy = ty - sy; + float distToTrigger = thirdCorePasser->GetExactDist2d(closestTrigger); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + constexpr float nearTriggerDist = 1.5f; + float targetX = tx - dx * nearTriggerDist; + float targetY = ty - dy * nearTriggerDist; + + intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +// The next four functions check if the respective passer is <= 2 yards of their intended +// position and are used to determine when the prior bot in the chain can pass the core +bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInIntendedPosition( + Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(firstCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = firstCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInIntendedPosition( + Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(secondCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = secondCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInIntendedPosition( + Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(thirdCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = thirdCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInIntendedPosition( + Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger) +{ + auto itSnap = intendedLineup.find(fourthCorePasser->GetGUID()); + if (itSnap != intendedLineup.end()) + { + float dist2d = fourthCorePasser->GetExactDist2d(itSnap->second.GetPositionX(), + itSnap->second.GetPositionY()); + return dist2d <= 2.0f; + } + + return false; +} + +// ImbueItem() is inconsistent in causing the receiver bot to receive the core and the giver +// bot to remove the core, so ScheduleTransferCoreAfterImbue() creates the core on the receiver +// and removes it from the giver, with ImbueItem() called primarily for the throwing animation +void LadyVashjPassTheTaintedCoreAction::ScheduleTransferCoreAfterImbue( + PlayerbotAI* botAI, Player* giver, Player* receiver) +{ + if (!receiver || !giver) + return; + + constexpr uint32 delayMs = 1500; + const ObjectGuid receiverGuid = receiver->GetGUID(); + const ObjectGuid giverGuid = giver->GetGUID(); + + botAI->AddTimedEvent([receiverGuid, giverGuid]() + { + Player* receiverPlayer = + receiverGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(receiverGuid); + Player* giverPlayer = + giverGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(giverGuid); + + if (!receiverPlayer) + return; + + if (!receiverPlayer->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + { + ItemPosCountVec dest; + uint32 count = 1; + int canStore = + receiverPlayer->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, ITEM_TAINTED_CORE, count); + + if (canStore == EQUIP_ERR_OK) + { + receiverPlayer->StoreNewItem(dest, ITEM_TAINTED_CORE, true, + Item::GenerateItemRandomPropertyId(ITEM_TAINTED_CORE)); + } + } + + if (giverPlayer) + { + Item* item = giverPlayer->GetItemByEntry(ITEM_TAINTED_CORE); + if (item && giverPlayer->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + giverPlayer->DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + } + }, delayMs); +} + +bool LadyVashjPassTheTaintedCoreAction::UseCoreOnNearestGenerator(const uint32 instanceId) +{ + auto const& generators = + GetAllGeneratorInfosByDbGuids(bot->GetMap(), SHIELD_GENERATOR_DB_GUIDS); + const GeneratorInfo* nearestGen = GetNearestGeneratorToBot(bot, generators); + if (!nearestGen) + return false; + + GameObject* generator = botAI->GetGameObject(nearestGen->guid); + if (!generator) + return false; + + if (bot->GetExactDist2d(generator) > 4.5f) + return false; + + Item* core = bot->GetItemByEntry(ITEM_TAINTED_CORE); + if (!core) + return false; + + if (bot->CanUseItem(core) != EQUIP_ERR_OK) + return false; + + if (bot->IsNonMeleeSpellCast(false)) + return false; + + const uint8 bagIndex = core->GetBagSlot(); + const uint8 slot = core->GetSlot(); + constexpr uint8 cast_count = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (core->GetTemplate()->Spells[i].SpellId > 0) + { + spellId = core->GetTemplate()->Spells[i].SpellId; + break; + } + } + + const ObjectGuid item_guid = core->GetGUID(); + constexpr uint32 glyphIndex = 0; + constexpr uint8 castFlags = 0; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex; + packet << slot; + packet << cast_count; + packet << spellId; + packet << item_guid; + packet << glyphIndex; + packet << castFlags; + packet << (uint32)TARGET_FLAG_GAMEOBJECT; + packet << generator->GetGUID().WriteAsPacked(); + + bot->GetSession()->HandleUseItemOpcode(packet); + nearestTriggerGuid.erase(instanceId); + lastImbueAttempt.erase(instanceId); + lastCoreInInventoryTime.erase(instanceId); + return true; +} + +// Fallback for residual cores to be destroyed in Phase 3 in case +// ScheduleTransferCoreAfterImbue() fails to remove the core from the giver +bool LadyVashjDestroyTaintedCoreAction::Execute(Event /*event*/) +{ + if (Item* core = bot->GetItemByEntry(ITEM_TAINTED_CORE)) + { + bot->DestroyItem(core->GetBagSlot(), core->GetSlot(), true); + return true; + } + + return false; +} + +// This needs to be separate from the general map erasing logic because +// Bots may end up out of combat during the Vashj encounter +bool LadyVashjEraseCorePassingTrackersAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + + bool erased = false; + if (nearestTriggerGuid.erase(instanceId) > 0) + erased = true; + if (lastImbueAttempt.erase(instanceId) > 0) + erased = true; + if (lastCoreInInventoryTime.erase(instanceId) > 0) + erased = true; + if (intendedLineup.erase(bot->GetGUID()) > 0) + erased = true; + + return erased; +} + +// The standard "avoid aoe" strategy does work for Toxic Spores, but this method +// provides more buffer distance and limits the area in which bots can move +// so that they do not go down the stairs +bool LadyVashjAvoidToxicSporesAction::Execute(Event /*event*/) +{ + auto const& spores = GetAllSporeDropTriggers(botAI, bot); + if (spores.empty()) + return false; + + constexpr float hazardRadius = 7.0f; + bool inDanger = false; + for (Unit* spore : spores) + { + if (bot->GetExactDist2d(spore) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + const Position& vashjCenter = VASHJ_PLATFORM_CENTER_POSITION; + constexpr float maxRadius = 60.0f; + + Position safestPos = FindSafestNearbyPosition(spores, vashjCenter, maxRadius, hazardRadius); + + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + bool backwards = (vashj && vashj->GetVictim() == bot); + return MoveTo(SSC_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, backwards); +} + +Position LadyVashjAvoidToxicSporesAction::FindSafestNearbyPosition( + const std::vector& spores, const Position& vashjCenter, + float maxRadius, float hazardRadius) +{ + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 40.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; + distance <= maxDistance; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + float x = bot->GetPositionX() + distance * std::cos(angle); + float y = bot->GetPositionY() + distance * std::sin(angle); + float z = bot->GetPositionZ(); + + if (vashjCenter.GetExactDist2d(x, y) > maxRadius) + continue; + + bool isSafe = true; + for (Unit* spore : spores) + { + if (spore->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, z); + + bool pathSafe = + IsPathSafeFromSpores(bot->GetPosition(), testPos, spores, hazardRadius); + if (pathSafe || !foundSafe) + { + float moveDistance = bot->GetExactDist2d(x, y); + + if (pathSafe && (!foundSafe || moveDistance < minMoveDistance)) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = true; + } + else if (!foundSafe && moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + } + } + } + + if (foundSafe) + break; + } + + return bestPos; +} + +bool LadyVashjAvoidToxicSporesAction::IsPathSafeFromSpores(const Position& start, + const Position& end, const std::vector& spores, float hazardRadius) +{ + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* spore : spores) + { + float distToSpore = spore->GetExactDist2d(checkX, checkY); + if (distToSpore < hazardRadius) + return false; + } + } + + return true; +} + +// When Toxic Sporebats spit poison, they summon "Spore Drop Trigger" NPCs +// that create the toxic pools +std::vector LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers( + PlayerbotAI* botAI, Player* bot) +{ + std::vector sporeDropTriggers; + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + constexpr float maxSearchRadius = 40.0f; + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_SPORE_DROP_TRIGGER && + bot->GetExactDist2d(unit) < maxSearchRadius) + sporeDropTriggers.push_back(unit); + } + + return sporeDropTriggers; +} + +bool LadyVashjUseFreeActionAbilitiesAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto const& spores = + LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(botAI, bot); + constexpr float toxicSporeRadius = 6.0f; + + // If Rogues are Entangled and either have Static Charge or + // are near a spore, use Cloak of Shadows + if (bot->getClass() == CLASS_ROGUE && bot->HasAura(SPELL_ENTANGLE)) + { + bool nearSpore = false; + for (Unit* spore : spores) + { + if (bot->GetExactDist2d(spore) < toxicSporeRadius) + { + nearSpore = true; + break; + } + } + if (bot->HasAura(SPELL_STATIC_CHARGE) || nearSpore) + { + if (botAI->CanCastSpell("cloak of shadows", bot)) + return botAI->CastSpell("cloak of shadows", bot); + } + } + + // The remainder of the logic is for Paladins to use Hand of Freedom + Player* mainTankToxic = nullptr; + Player* anyToxic = nullptr; + Player* mainTankStatic = nullptr; + Player* anyStatic = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !member->HasAura(SPELL_ENTANGLE) || + !botAI->IsMelee(member)) + continue; + + bool nearToxicSpore = false; + for (Unit* spore : spores) + { + if (member->GetExactDist2d(spore) < toxicSporeRadius) + { + nearToxicSpore = true; + break; + } + } + + if (nearToxicSpore) + { + if (botAI->IsMainTank(member)) + mainTankToxic = member; + + if (!anyToxic) + anyToxic = member; + } + + if (member->HasAura(SPELL_STATIC_CHARGE)) + { + if (botAI->IsMainTank(member)) + mainTankStatic = member; + + if (!anyStatic) + anyStatic = member; + } + } + + if (bot->getClass() == CLASS_PALADIN) + { + // Priority 1: Entangled in Toxic Spores (prefer main tank) + Player* toxicTarget = mainTankToxic ? mainTankToxic : anyToxic; + if (toxicTarget) + { + if (botAI->CanCastSpell("hand of freedom", toxicTarget)) + return botAI->CastSpell("hand of freedom", toxicTarget); + } + + // Priority 2: Entangled with Static Charge (prefer main tank) + Player* staticTarget = mainTankStatic ? mainTankStatic : anyStatic; + if (staticTarget) + { + if (botAI->CanCastSpell("hand of freedom", staticTarget)) + return botAI->CastSpell("hand of freedom", staticTarget); + } + } + + return false; +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h new file mode 100644 index 000000000..cbd237402 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h @@ -0,0 +1,457 @@ +#ifndef _PLAYERBOT_RAIDSSCACTIONS_H +#define _PLAYERBOT_RAIDSSCACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +// General + +class SerpentShrineCavernEraseTimersAndTrackersAction : public Action +{ +public: + SerpentShrineCavernEraseTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "serpent shrine cavern erase timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Trash + +class UnderbogColossusEscapeToxicPoolAction : public MovementAction +{ +public: + UnderbogColossusEscapeToxicPoolAction( + PlayerbotAI* botAI, std::string const name = "underbog colossus escape toxic pool") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class GreyheartTidecallerMarkWaterElementalTotemAction : public Action +{ +public: + GreyheartTidecallerMarkWaterElementalTotemAction( + PlayerbotAI* botAI, std::string const name = "greyheart tidecaller mark water elemental totem") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Hydross the Unstable + +class HydrossTheUnstablePositionFrostTankAction : public AttackAction +{ +public: + HydrossTheUnstablePositionFrostTankAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable position frost tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstablePositionNatureTankAction : public AttackAction +{ +public: + HydrossTheUnstablePositionNatureTankAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable position nature tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstablePrioritizeElementalAddsAction : public AttackAction +{ +public: + HydrossTheUnstablePrioritizeElementalAddsAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable prioritize elemental adds") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstableFrostPhaseSpreadOutAction : public MovementAction +{ +public: + HydrossTheUnstableFrostPhaseSpreadOutAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable frost phase spread out") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstableMisdirectBossToTankAction : public Action +{ +public: + HydrossTheUnstableMisdirectBossToTankAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable misdirect boss to tank") : Action(botAI, name) {} + bool Execute(Event event) override; + +private: + bool TryMisdirectToFrostTank(Unit* hydross, Group* group); + bool TryMisdirectToNatureTank(Unit* hydross, Group* group); +}; + +class HydrossTheUnstableStopDpsUponPhaseChangeAction : public Action +{ +public: + HydrossTheUnstableStopDpsUponPhaseChangeAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable stop dps upon phase change") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class HydrossTheUnstableManageTimersAction : public Action +{ +public: + HydrossTheUnstableManageTimersAction( + PlayerbotAI* botAI, std::string const name = "hydross the unstable manage timers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// The Lurker Below + +class TheLurkerBelowRunAroundBehindBossAction : public MovementAction +{ +public: + TheLurkerBelowRunAroundBehindBossAction( + PlayerbotAI* botAI, std::string const name = "the lurker below run around behind boss") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowPositionMainTankAction : public AttackAction +{ +public: + TheLurkerBelowPositionMainTankAction( + PlayerbotAI* botAI, std::string const name = "the lurker below position main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowSpreadRangedInArcAction : public MovementAction +{ +public: + TheLurkerBelowSpreadRangedInArcAction( + PlayerbotAI* botAI, std::string const name = "the lurker below spread ranged in arc") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowTanksPickUpAddsAction : public AttackAction +{ +public: + TheLurkerBelowTanksPickUpAddsAction( + PlayerbotAI* botAI, std::string const name = "the lurker below tanks pick up adds") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class TheLurkerBelowManageSpoutTimerAction : public Action +{ +public: + TheLurkerBelowManageSpoutTimerAction( + PlayerbotAI* botAI, std::string const name = "the lurker below manage spout timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Leotheras the Blind + +class LeotherasTheBlindTargetSpellbindersAction : public Action +{ +public: + LeotherasTheBlindTargetSpellbindersAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind target spellbinders") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindPositionRangedAction : public MovementAction +{ +public: + LeotherasTheBlindPositionRangedAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind position ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindDemonFormTankAttackBossAction : public AttackAction +{ +public: + LeotherasTheBlindDemonFormTankAttackBossAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind demon form tank attack boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindMeleeTanksDontAttackDemonFormAction : public Action +{ +public: + LeotherasTheBlindMeleeTanksDontAttackDemonFormAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind melee tanks don't attack demon form") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindRunAwayFromWhirlwindAction : public MovementAction +{ +public: + LeotherasTheBlindRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind run away from whirlwind") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindMeleeDpsRunAwayFromBossAction : public MovementAction +{ +public: + LeotherasTheBlindMeleeDpsRunAwayFromBossAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind melee dps run away from boss") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindDestroyInnerDemonAction : public AttackAction +{ +public: + LeotherasTheBlindDestroyInnerDemonAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind destroy inner demon") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandleFeralTankStrategy(Unit* innerDemon); + bool HandleHealerStrategy(Unit* innerDemon); +}; + +class LeotherasTheBlindFinalPhaseAssignDpsPriorityAction : public AttackAction +{ +public: + LeotherasTheBlindFinalPhaseAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind final phase assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindMisdirectBossToDemonFormTankAction : public AttackAction +{ +public: + LeotherasTheBlindMisdirectBossToDemonFormTankAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind misdirect boss to demon form tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LeotherasTheBlindManageDpsWaitTimersAction : public Action +{ +public: + LeotherasTheBlindManageDpsWaitTimersAction( + PlayerbotAI* botAI, std::string const name = "leotheras the blind manage dps wait timers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Fathom-Lord Karathress + +class FathomLordKarathressMainTankPositionBossAction : public AttackAction +{ +public: + FathomLordKarathressMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressFirstAssistTankPositionCaribdisAction : public AttackAction +{ +public: + FathomLordKarathressFirstAssistTankPositionCaribdisAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress first assist tank position caribdis") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressSecondAssistTankPositionSharkkisAction : public AttackAction +{ +public: + FathomLordKarathressSecondAssistTankPositionSharkkisAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress second assist tank position sharkkis") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressThirdAssistTankPositionTidalvessAction : public AttackAction +{ +public: + FathomLordKarathressThirdAssistTankPositionTidalvessAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress third assist tank position tidalvess") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressPositionCaribdisTankHealerAction : public MovementAction +{ +public: + FathomLordKarathressPositionCaribdisTankHealerAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress position caribdis tank healer") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressMisdirectBossesToTanksAction : public AttackAction +{ +public: + FathomLordKarathressMisdirectBossesToTanksAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress misdirect bosses to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressAssignDpsPriorityAction : public AttackAction +{ +public: + FathomLordKarathressAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class FathomLordKarathressManageDpsTimerAction : public Action +{ +public: + FathomLordKarathressManageDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "fathom-lord karathress manage dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Morogrim Tidewalker + +class MorogrimTidewalkerMisdirectBossToMainTankAction : public AttackAction +{ +public: + MorogrimTidewalkerMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "morogrim tidewalker misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class MorogrimTidewalkerMoveBossToTankPositionAction : public AttackAction +{ +public: + MorogrimTidewalkerMoveBossToTankPositionAction( + PlayerbotAI* botAI, std::string const name = "morogrim tidewalker move boss to tank position") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool MoveToPhase1TankPosition(Unit* tidewalker); + bool MoveToPhase2TankPosition(Unit* tidewalker); +}; + +class MorogrimTidewalkerPhase2RepositionRangedAction : public MovementAction +{ +public: + MorogrimTidewalkerPhase2RepositionRangedAction( + PlayerbotAI* botAI, std::string const name = "morogrim tidewalker phase 2 reposition ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Lady Vashj + +class LadyVashjMainTankPositionBossAction : public AttackAction +{ +public: + LadyVashjMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "lady vashj main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjPhase1SpreadRangedInArcAction : public MovementAction +{ +public: + LadyVashjPhase1SpreadRangedInArcAction( + PlayerbotAI* botAI, std::string const name = "lady vashj phase 1 spread ranged in arc") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjSetGroundingTotemInMainTankGroupAction : public MovementAction +{ +public: + LadyVashjSetGroundingTotemInMainTankGroupAction( + PlayerbotAI* botAI, std::string const name = "lady vashj set grounding totem in main tank group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjStaticChargeMoveAwayFromGroupAction : public MovementAction +{ +public: + LadyVashjStaticChargeMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "lady vashj static charge move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjMisdirectBossToMainTankAction : public AttackAction +{ +public: + LadyVashjMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "lady vashj misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjAssignPhase2AndPhase3DpsPriorityAction : public AttackAction +{ +public: + LadyVashjAssignPhase2AndPhase3DpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "lady vashj assign phase 2 and phase 3 dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjMisdirectStriderToFirstAssistTankAction : public AttackAction +{ +public: + LadyVashjMisdirectStriderToFirstAssistTankAction( + PlayerbotAI* botAI, std::string const name = "lady vashj misdirect strider to first assist tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjTankAttackAndMoveAwayStriderAction : public AttackAction +{ +public: + LadyVashjTankAttackAndMoveAwayStriderAction( + PlayerbotAI* botAI, std::string const name = "lady vashj tank attack and move away strider") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjTeleportToTaintedElementalAction : public AttackAction +{ +public: + LadyVashjTeleportToTaintedElementalAction( + PlayerbotAI* botAI, std::string const name = "lady vashj teleport to tainted elemental") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjLootTaintedCoreAction : public MovementAction +{ +public: + LadyVashjLootTaintedCoreAction( + PlayerbotAI* botAI, std::string const name = "lady vashj loot tainted core") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjPassTheTaintedCoreAction : public MovementAction +{ +public: + LadyVashjPassTheTaintedCoreAction( + PlayerbotAI* botAI, std::string const name = "lady vashj pass the tainted core") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool LineUpFirstCorePasser(Player* designatedLooter, Unit* closestTrigger); + bool LineUpSecondCorePasser(Player* firstCorePasser, Unit* closestTrigger); + bool LineUpThirdCorePasser(Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); + bool LineUpFourthCorePasser(Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); + bool IsFirstCorePasserInIntendedPosition(Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger); + bool IsSecondCorePasserInIntendedPosition(Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); + bool IsThirdCorePasserInIntendedPosition(Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); + bool IsFourthCorePasserInIntendedPosition(Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger); + void ScheduleTransferCoreAfterImbue(PlayerbotAI* botAI, Player* giver, Player* receiver); + bool UseCoreOnNearestGenerator(const uint32 instanceId); +}; + +class LadyVashjDestroyTaintedCoreAction : public Action +{ +public: + LadyVashjDestroyTaintedCoreAction(PlayerbotAI* botAI, std::string const name = "lady vashj destroy tainted core") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjEraseCorePassingTrackersAction : public Action +{ +public: + LadyVashjEraseCorePassingTrackersAction(PlayerbotAI* botAI, std::string const name = "lady vashj erase core passing trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class LadyVashjAvoidToxicSporesAction : public MovementAction +{ +public: + LadyVashjAvoidToxicSporesAction(PlayerbotAI* botAI, std::string const name = "lady vashj avoid toxic spores") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + static std::vector GetAllSporeDropTriggers(PlayerbotAI* botAI, Player* bot); + +private: + Position FindSafestNearbyPosition(const std::vector& spores, const Position& position, float maxRadius, float hazardRadius); + bool IsPathSafeFromSpores(const Position& start, const Position& end, const std::vector& spores, float hazardRadius); +}; + +class LadyVashjUseFreeActionAbilitiesAction : public Action +{ +public: + LadyVashjUseFreeActionAbilitiesAction(PlayerbotAI* botAI, std::string const name = "lady vashj use free action abilities") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp new file mode 100644 index 000000000..c99cafa3c --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp @@ -0,0 +1,799 @@ +#include "RaidSSCMultipliers.h" +#include "RaidSSCActions.h" +#include "RaidSSCHelpers.h" +#include "ChooseTargetActions.h" +#include "DestroyItemAction.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "DruidCatActions.h" +#include "DruidShapeshiftActions.h" +#include "FollowActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "LootAction.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" +#include "WipeAction.h" + +using namespace SerpentShrineCavernHelpers; + +// Trash + +float UnderbogColossusEscapeToxicPoolMultiplier::GetValue(Action* action) +{ + if (bot->HasAura(SPELL_TOXIC_POOL)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Hydross the Unstable + +float HydrossTheUnstableDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return 1.0f; + + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action))) + { + if ((botAI->IsMainTank(bot) && hydross->HasAura(SPELL_CORRUPTION)) || + (botAI->IsAssistTankOfIndex(bot, 0, true) && !hydross->HasAura(SPELL_CORRUPTION))) + return 0.0f; + } + + return 1.0f; +} + +float HydrossTheUnstableWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (!hydross) + return 1.0f; + + Unit* waterElemental = AI_VALUE2(Unit*, "find target", "pure spawn of hydross"); + Unit* natureElemental = AI_VALUE2(Unit*, "find target", "tainted spawn of hydross"); + if (botAI->IsAssistTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true) && + (waterElemental || natureElemental)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const uint32 instanceId = hydross->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + constexpr uint8 phaseChangeWaitSeconds = 1; + constexpr uint8 dpsWaitSeconds = 5; + + if (!hydross->HasAura(SPELL_CORRUPTION) && !botAI->IsMainTank(bot)) + { + auto itDps = hydrossFrostDpsWaitTimer.find(instanceId); + auto itPhase = hydrossChangeToFrostPhaseTimer.find(instanceId); + + bool justChanged = (itDps == hydrossFrostDpsWaitTimer.end() || + (now - itDps->second) < dpsWaitSeconds); + bool aboutToChange = (itPhase != hydrossChangeToFrostPhaseTimer.end() && + (now - itPhase->second) > phaseChangeWaitSeconds); + + if (justChanged || aboutToChange) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + if (hydross->HasAura(SPELL_CORRUPTION) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + { + auto itDps = hydrossNatureDpsWaitTimer.find(instanceId); + auto itPhase = hydrossChangeToNaturePhaseTimer.find(instanceId); + + bool justChanged = (itDps == hydrossNatureDpsWaitTimer.end() || + (now - itDps->second) < dpsWaitSeconds); + bool aboutToChange = (itPhase != hydrossChangeToNaturePhaseTimer.end() && + (now - itPhase->second) > phaseChangeWaitSeconds); + + if (justChanged || aboutToChange) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + return 1.0f; +} + +float HydrossTheUnstableControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "hydross the unstable")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// The Lurker Below + +float TheLurkerBelowStayAwayFromSpoutMultiplier::GetValue(Action* action) +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return 1.0f; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + if (it != lurkerSpoutTimer.end() && it->second > now) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float TheLurkerBelowMaintainRangedSpreadMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "the lurker below")) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Disable tank assist during Submerge only if there are 3 or more tanks in the raid +float TheLurkerBelowDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + uint8 tankCount = 0; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsTank(member)) + ++tankCount; + } + + if (tankCount >= 3) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Leotheras the Blind + +float LeotherasTheBlindAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot)) + return 1.0f; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + Unit* leotherasHuman = GetLeotherasHuman(botAI); + if (!leotherasHuman) + return 1.0f; + + if (!leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && + (leotherasHuman->HasAura(SPELL_WHIRLWIND) || + leotherasHuman->HasAura(SPELL_WHIRLWIND_CHANNEL))) + { + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LeotherasTheBlindDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return 1.0f; + + if (GetPhase2LeotherasDemon(botAI) && dynamic_cast(action)) + return 0.0f; + + if (!GetPhase3LeotherasDemon(botAI) && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float LeotherasTheBlindFocusOnInnerDemonMultiplier::GetValue(Action* action) +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + { + 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) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier::GetValue(Action* action) +{ + if (botAI->IsRanged(bot) || botAI->IsTank(bot)) + return 1.0f; + + if (!GetPhase2LeotherasDemon(botAI)) + return 1.0f; + + Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); + if (chaosBlast && chaosBlast->GetStackAmount() >= 5) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) + return 1.0f; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const uint32 instanceId = leotheras->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + constexpr uint8 dpsWaitSecondsPhase1 = 5; + Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); + if (leotherasHuman && !leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && + !leotherasPhase3Demon) + { + if (botAI->IsTank(bot)) + return 1.0f; + + auto it = leotherasHumanFormDpsWaitTimer.find(instanceId); + if (it == leotherasHumanFormDpsWaitTimer.end() || + (now - it->second) < dpsWaitSecondsPhase1) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + constexpr uint8 dpsWaitSecondsPhase2 = 12; + Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI); + Player* demonFormTank = GetLeotherasDemonFormTank(bot); + if (leotherasPhase2Demon) + { + if (demonFormTank && demonFormTank == bot) + return 1.0f; + + if (!demonFormTank && botAI->IsTank(bot)) + return 1.0f; + + auto it = leotherasDemonFormDpsWaitTimer.find(instanceId); + if (it == leotherasDemonFormDpsWaitTimer.end() || + (now - it->second) < dpsWaitSecondsPhase2) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + constexpr uint8 dpsWaitSecondsPhase3 = 8; + if (leotherasPhase3Demon) + { + if ((demonFormTank && demonFormTank == bot) || botAI->IsTank(bot)) + return 1.0f; + + auto it = leotherasFinalPhaseDpsWaitTimer.find(instanceId); + if (it == leotherasFinalPhaseDpsWaitTimer.end() || + (now - it->second) < dpsWaitSecondsPhase3) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + return 1.0f; +} + +// Don't use Bloodlust/Heroism during the Channeler phase +float LeotherasTheBlindDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (leotheras && leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Fathom-Lord Karathress + +float FathomLordKarathressDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return 1.0f; + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + 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) || + 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 FathomLordKarathressDisableAoeMultiplier::GetValue(Action* action) +{ + if (!botAI->IsDps(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + { + if (auto castSpellAction = dynamic_cast(action)) + { + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return 0.0f; + } + } + + return 1.0f; +} + +float FathomLordKarathressControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float FathomLordKarathressWaitForDpsMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot)) + return 1.0f; + + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + if (!karathress) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 12; + + auto it = karathressDpsWaitTimer.find(karathress->GetMap()->GetInstanceId()); + if (it == karathressDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + + return 1.0f; +} + +float FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier::GetValue(Action* action) +{ + if (!botAI->IsAssistHealOfIndex(bot, 0, true)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "fathom-guard caribdis")) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Morogrim Tidewalker + +// Use Bloodlust/Heroism after the first Murloc spawn +float MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "tidewalker lurker")) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float MorogrimTidewalkerDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsMainTank(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float MorogrimTidewalkerMaintainPhase2StackingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + if (!tidewalker) + return 1.0f; + + if (tidewalker->GetHealthPct() < 25.0f) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Lady Vashj + +// Wait until phase 3 to use Bloodlust/Heroism +// Don't use other major cooldowns in Phase 1, either +float LadyVashjDelayCooldownsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (bot->getClass() == CLASS_SHAMAN) + { + if (IsLadyVashjInPhase3(botAI)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsDps(bot) && IsLadyVashjInPhase1(botAI)) + { + 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) || + 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) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LadyVashjMaintainPhase1RangedSpreadMultiplier::GetValue(Action* action) +{ + if (!botAI->IsRanged(bot)) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "lady vashj") && + IsLadyVashjInPhase1(botAI)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LadyVashjStaticChargeStayAwayFromGroupMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot) || !bot->HasAura(SPELL_STATIC_CHARGE)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bots should not loot the core with normal looting logic +float LadyVashjDoNotLootTheTaintedCoreMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "lady vashj")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || + !IsLadyVashjInPhase2(botAI)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + + auto hasCore = [](Player* player) + { + return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); + }; + + if (hasCore(bot)) + { + if (!dynamic_cast(action)) + return 0.0f; + } + + if (bot == designatedLooter) + { + if (!hasCore(bot)) + return 1.0f; + } + else if (bot == firstCorePasser) + { + if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || + hasCore(fourthCorePasser)) + return 1.0f; + } + else if (bot == secondCorePasser) + { + if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) + return 1.0f; + } + else if (bot == thirdCorePasser) + { + if (hasCore(fourthCorePasser)) + return 1.0f; + } + else if (bot != fourthCorePasser) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "tainted elemental") && + (bot == firstCorePasser || bot == secondCorePasser)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + if (AnyRecentCoreInInventory(group, botAI)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// All of phases 2 and 3 require a custom movement and targeting system +// So the standard target selection system must be disabled +float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *action) +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (IsLadyVashjInPhase2(botAI)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (!botAI->IsHeal(bot) && dynamic_cast(action)) + return 0.0f; + + Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); + if (enchanted && bot->GetVictim() == enchanted) + { + if (dynamic_cast(action)) + return 0.0f; + } + } + + if (IsLadyVashjInPhase3(botAI)) + { + if (dynamic_cast(action)) + return 0.0f; + + Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); + Unit* strider = AI_VALUE2(Unit*, "find target", "coilfang strider"); + Unit* elite = AI_VALUE2(Unit*, "find target", "coilfang elite"); + if (enchanted || strider || elite) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (enchanted && bot->GetVictim() == enchanted) + { + if (dynamic_cast(action)) + return 0.0f; + } + } + else if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h new file mode 100644 index 000000000..6630dc206 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h @@ -0,0 +1,236 @@ +#ifndef _PLAYERBOT_RAIDSSCMULTIPLIERS_H +#define _PLAYERBOT_RAIDSSCMULTIPLIERS_H + +#include "Multiplier.h" + +// Trash + +class UnderbogColossusEscapeToxicPoolMultiplier : public Multiplier +{ +public: + UnderbogColossusEscapeToxicPoolMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "underbog colossus escape toxic pool") {} + virtual float GetValue(Action* action); +}; + +// Hydross the Unstable + +class HydrossTheUnstableDisableTankActionsMultiplier : public Multiplier +{ +public: + HydrossTheUnstableDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hydross the unstable disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class HydrossTheUnstableWaitForDpsMultiplier : public Multiplier +{ +public: + HydrossTheUnstableWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hydross the unstable wait for dps") {} + virtual float GetValue(Action* action); +}; + +class HydrossTheUnstableControlMisdirectionMultiplier : public Multiplier +{ +public: + HydrossTheUnstableControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hydross the unstable control misdirection") {} + virtual float GetValue(Action* action); +}; + +// The Lurker Below + +class TheLurkerBelowStayAwayFromSpoutMultiplier : public Multiplier +{ +public: + TheLurkerBelowStayAwayFromSpoutMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the lurker below stay away from spout") {} + virtual float GetValue(Action* action); +}; + +class TheLurkerBelowMaintainRangedSpreadMultiplier : public Multiplier +{ +public: + TheLurkerBelowMaintainRangedSpreadMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the lurker below maintain ranged spread") {} + virtual float GetValue(Action* action); +}; + +class TheLurkerBelowDisableTankAssistMultiplier : public Multiplier +{ +public: + TheLurkerBelowDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the lurker below disable tank assist") {} + virtual float GetValue(Action* action); +}; + +// Leotheras the Blind + +class LeotherasTheBlindAvoidWhirlwindMultiplier : public Multiplier +{ +public: + LeotherasTheBlindAvoidWhirlwindMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind avoid whirlwind") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindDisableTankActionsMultiplier : public Multiplier +{ +public: + LeotherasTheBlindDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier : public Multiplier +{ +public: + LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind melee dps avoid chaos blast") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindFocusOnInnerDemonMultiplier : public Multiplier +{ +public: + LeotherasTheBlindFocusOnInnerDemonMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind focus on inner demon") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindWaitForDpsMultiplier : public Multiplier +{ +public: + LeotherasTheBlindWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind wait for dps") {} + virtual float GetValue(Action* action); +}; + +class LeotherasTheBlindDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + LeotherasTheBlindDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "leotheras the blind delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +// Fathom-Lord Karathress + +class FathomLordKarathressDisableTankActionsMultiplier : public Multiplier +{ +public: + FathomLordKarathressDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressDisableAoeMultiplier : public Multiplier +{ +public: + FathomLordKarathressDisableAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress disable aoe") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressControlMisdirectionMultiplier : public Multiplier +{ +public: + FathomLordKarathressControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress control misdirection") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressWaitForDpsMultiplier : public Multiplier +{ +public: + FathomLordKarathressWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress wait for dps") {} + virtual float GetValue(Action* action); +}; + +class FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier : public Multiplier +{ +public: + FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "fathom-lord karathress caribdis tank healer maintain position") {} + virtual float GetValue(Action* action); +}; + +// Morogrim Tidewalker + +class MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "morogrim tidewalker delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +class MorogrimTidewalkerDisableTankActionsMultiplier : public Multiplier +{ +public: + MorogrimTidewalkerDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "morogrim tidewalker disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class MorogrimTidewalkerMaintainPhase2StackingMultiplier : public Multiplier +{ +public: + MorogrimTidewalkerMaintainPhase2StackingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "morogrim tidewalker maintain phase2 stacking") {} + virtual float GetValue(Action* action); +}; + +// Lady Vashj + +class LadyVashjDelayCooldownsMultiplier : public Multiplier +{ +public: + LadyVashjDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj delay cooldowns") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjMaintainPhase1RangedSpreadMultiplier : public Multiplier +{ +public: + LadyVashjMaintainPhase1RangedSpreadMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj maintain phase1 ranged spread") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjStaticChargeStayAwayFromGroupMultiplier : public Multiplier +{ +public: + LadyVashjStaticChargeStayAwayFromGroupMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj static charge stay away from group") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjDoNotLootTheTaintedCoreMultiplier : public Multiplier +{ +public: + LadyVashjDoNotLootTheTaintedCoreMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj do not loot the tainted core") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjCorePassersPrioritizePositioningMultiplier : public Multiplier +{ +public: + LadyVashjCorePassersPrioritizePositioningMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj core passers prioritize positioning") {} + virtual float GetValue(Action* action); +}; + +class LadyVashjDisableAutomaticTargetingAndMovementModifier : public Multiplier +{ +public: + LadyVashjDisableAutomaticTargetingAndMovementModifier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj disable automatic targeting and movement") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h new file mode 100644 index 000000000..e6dce1694 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h @@ -0,0 +1,337 @@ +#ifndef _PLAYERBOT_RAIDSSCACTIONCONTEXT_H +#define _PLAYERBOT_RAIDSSCACTIONCONTEXT_H + +#include "RaidSSCActions.h" +#include "NamedObjectContext.h" + +class RaidSSCActionContext : public NamedObjectContext +{ +public: + RaidSSCActionContext() + { + // General + creators["serpent shrine cavern erase timers and trackers"] = + &RaidSSCActionContext::serpent_shrine_cavern_erase_timers_and_trackers; + + // Trash + creators["underbog colossus escape toxic pool"] = + &RaidSSCActionContext::underbog_colossus_escape_toxic_pool; + + creators["greyheart tidecaller mark water elemental totem"] = + &RaidSSCActionContext::greyheart_tidecaller_mark_water_elemental_totem; + + // Hydross the Unstable + creators["hydross the unstable position frost tank"] = + &RaidSSCActionContext::hydross_the_unstable_position_frost_tank; + + creators["hydross the unstable position nature tank"] = + &RaidSSCActionContext::hydross_the_unstable_position_nature_tank; + + creators["hydross the unstable prioritize elemental adds"] = + &RaidSSCActionContext::hydross_the_unstable_prioritize_elemental_adds; + + creators["hydross the unstable frost phase spread out"] = + &RaidSSCActionContext::hydross_the_unstable_frost_phase_spread_out; + + creators["hydross the unstable misdirect boss to tank"] = + &RaidSSCActionContext::hydross_the_unstable_misdirect_boss_to_tank; + + creators["hydross the unstable stop dps upon phase change"] = + &RaidSSCActionContext::hydross_the_unstable_stop_dps_upon_phase_change; + + creators["hydross the unstable manage timers"] = + &RaidSSCActionContext::hydross_the_unstable_manage_timers; + + // The Lurker Below + creators["the lurker below run around behind boss"] = + &RaidSSCActionContext::the_lurker_below_run_around_behind_boss; + + creators["the lurker below position main tank"] = + &RaidSSCActionContext::the_lurker_below_position_main_tank; + + creators["the lurker below spread ranged in arc"] = + &RaidSSCActionContext::the_lurker_below_spread_ranged_in_arc; + + creators["the lurker below tanks pick up adds"] = + &RaidSSCActionContext::the_lurker_below_tanks_pick_up_adds; + + creators["the lurker below manage spout timer"] = + &RaidSSCActionContext::the_lurker_below_manage_spout_timer; + + // Leotheras the Blind + creators["leotheras the blind target spellbinders"] = + &RaidSSCActionContext::leotheras_the_blind_target_spellbinders; + + creators["leotheras the blind demon form tank attack boss"] = + &RaidSSCActionContext::leotheras_the_blind_demon_form_tank_attack_boss; + + creators["leotheras the blind melee tanks don't attack demon form"] = + &RaidSSCActionContext::leotheras_the_blind_melee_tanks_dont_attack_demon_form; + + creators["leotheras the blind position ranged"] = + &RaidSSCActionContext::leotheras_the_blind_position_ranged; + + creators["leotheras the blind run away from whirlwind"] = + &RaidSSCActionContext::leotheras_the_blind_run_away_from_whirlwind; + + creators["leotheras the blind melee dps run away from boss"] = + &RaidSSCActionContext::leotheras_the_blind_melee_dps_run_away_from_boss; + + creators["leotheras the blind destroy inner demon"] = + &RaidSSCActionContext::leotheras_the_blind_destroy_inner_demon; + + creators["leotheras the blind final phase assign dps priority"] = + &RaidSSCActionContext::leotheras_the_blind_final_phase_assign_dps_priority; + + creators["leotheras the blind misdirect boss to demon form tank"] = + &RaidSSCActionContext::leotheras_the_blind_misdirect_boss_to_demon_form_tank; + + creators["leotheras the blind manage dps wait timers"] = + &RaidSSCActionContext::leotheras_the_blind_manage_dps_wait_timers; + + // Fathom-Lord Karathress + creators["fathom-lord karathress main tank position boss"] = + &RaidSSCActionContext::fathom_lord_karathress_main_tank_position_boss; + + creators["fathom-lord karathress first assist tank position caribdis"] = + &RaidSSCActionContext::fathom_lord_karathress_first_assist_tank_position_caribdis; + + creators["fathom-lord karathress second assist tank position sharkkis"] = + &RaidSSCActionContext::fathom_lord_karathress_second_assist_tank_position_sharkkis; + + creators["fathom-lord karathress third assist tank position tidalvess"] = + &RaidSSCActionContext::fathom_lord_karathress_third_assist_tank_position_tidalvess; + + creators["fathom-lord karathress position caribdis tank healer"] = + &RaidSSCActionContext::fathom_lord_karathress_position_caribdis_tank_healer; + + creators["fathom-lord karathress misdirect bosses to tanks"] = + &RaidSSCActionContext::fathom_lord_karathress_misdirect_bosses_to_tanks; + + creators["fathom-lord karathress assign dps priority"] = + &RaidSSCActionContext::fathom_lord_karathress_assign_dps_priority; + + creators["fathom-lord karathress manage dps timer"] = + &RaidSSCActionContext::fathom_lord_karathress_manage_dps_timer; + + // Morogrim Tidewalker + creators["morogrim tidewalker misdirect boss to main tank"] = + &RaidSSCActionContext::morogrim_tidewalker_misdirect_boss_to_main_tank; + + creators["morogrim tidewalker move boss to tank position"] = + &RaidSSCActionContext::morogrim_tidewalker_move_boss_to_tank_position; + + creators["morogrim tidewalker phase 2 reposition ranged"] = + &RaidSSCActionContext::morogrim_tidewalker_phase_2_reposition_ranged; + + // Lady Vashj + creators["lady vashj main tank position boss"] = + &RaidSSCActionContext::lady_vashj_main_tank_position_boss; + + creators["lady vashj phase 1 spread ranged in arc"] = + &RaidSSCActionContext::lady_vashj_phase_1_spread_ranged_in_arc; + + creators["lady vashj set grounding totem in main tank group"] = + &RaidSSCActionContext::lady_vashj_set_grounding_totem_in_main_tank_group; + + creators["lady vashj static charge move away from group"] = + &RaidSSCActionContext::lady_vashj_static_charge_move_away_from_group; + + creators["lady vashj misdirect boss to main tank"] = + &RaidSSCActionContext::lady_vashj_misdirect_boss_to_main_tank; + + creators["lady vashj assign phase 2 and phase 3 dps priority"] = + &RaidSSCActionContext::lady_vashj_assign_phase_2_and_phase_3_dps_priority; + + creators["lady vashj misdirect strider to first assist tank"] = + &RaidSSCActionContext::lady_vashj_misdirect_strider_to_first_assist_tank; + + creators["lady vashj tank attack and move away strider"] = + &RaidSSCActionContext::lady_vashj_tank_attack_and_move_away_strider; + + creators["lady vashj loot tainted core"] = + &RaidSSCActionContext::lady_vashj_loot_tainted_core; + + creators["lady vashj teleport to tainted elemental"] = + &RaidSSCActionContext::lady_vashj_teleport_to_tainted_elemental; + + creators["lady vashj pass the tainted core"] = + &RaidSSCActionContext::lady_vashj_pass_the_tainted_core; + + creators["lady vashj destroy tainted core"] = + &RaidSSCActionContext::lady_vashj_destroy_tainted_core; + + creators["lady vashj erase core passing trackers"] = + &RaidSSCActionContext::lady_vashj_erase_core_passing_trackers; + + creators["lady vashj avoid toxic spores"] = + &RaidSSCActionContext::lady_vashj_avoid_toxic_spores; + + creators["lady vashj use free action abilities"] = + &RaidSSCActionContext::lady_vashj_use_free_action_abilities; + } + +private: + // General + static Action* serpent_shrine_cavern_erase_timers_and_trackers( + PlayerbotAI* botAI) { return new SerpentShrineCavernEraseTimersAndTrackersAction(botAI); } + + // Trash + static Action* underbog_colossus_escape_toxic_pool( + PlayerbotAI* botAI) { return new UnderbogColossusEscapeToxicPoolAction(botAI); } + + static Action* greyheart_tidecaller_mark_water_elemental_totem( + PlayerbotAI* botAI) { return new GreyheartTidecallerMarkWaterElementalTotemAction(botAI); } + + // Hydross the Unstable + static Action* hydross_the_unstable_position_frost_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstablePositionFrostTankAction(botAI); } + + static Action* hydross_the_unstable_position_nature_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstablePositionNatureTankAction(botAI); } + + static Action* hydross_the_unstable_prioritize_elemental_adds( + PlayerbotAI* botAI) { return new HydrossTheUnstablePrioritizeElementalAddsAction(botAI); } + + static Action* hydross_the_unstable_frost_phase_spread_out( + PlayerbotAI* botAI) { return new HydrossTheUnstableFrostPhaseSpreadOutAction(botAI); } + + static Action* hydross_the_unstable_misdirect_boss_to_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstableMisdirectBossToTankAction(botAI); } + + static Action* hydross_the_unstable_stop_dps_upon_phase_change( + PlayerbotAI* botAI) { return new HydrossTheUnstableStopDpsUponPhaseChangeAction(botAI); } + + static Action* hydross_the_unstable_manage_timers( + PlayerbotAI* botAI) { return new HydrossTheUnstableManageTimersAction(botAI); } + + // The Lurker Below + static Action* the_lurker_below_run_around_behind_boss( + PlayerbotAI* botAI) { return new TheLurkerBelowRunAroundBehindBossAction(botAI); } + + static Action* the_lurker_below_position_main_tank( + PlayerbotAI* botAI) { return new TheLurkerBelowPositionMainTankAction(botAI); } + + static Action* the_lurker_below_spread_ranged_in_arc( + PlayerbotAI* botAI) { return new TheLurkerBelowSpreadRangedInArcAction(botAI); } + + static Action* the_lurker_below_tanks_pick_up_adds( + PlayerbotAI* botAI) { return new TheLurkerBelowTanksPickUpAddsAction(botAI); } + + static Action* the_lurker_below_manage_spout_timer( + PlayerbotAI* botAI) { return new TheLurkerBelowManageSpoutTimerAction(botAI); } + + // Leotheras the Blind + static Action* leotheras_the_blind_target_spellbinders( + PlayerbotAI* botAI) { return new LeotherasTheBlindTargetSpellbindersAction(botAI); } + + static Action* leotheras_the_blind_demon_form_tank_attack_boss( + PlayerbotAI* botAI) { return new LeotherasTheBlindDemonFormTankAttackBossAction(botAI); } + + static Action* leotheras_the_blind_melee_tanks_dont_attack_demon_form( + PlayerbotAI* botAI) { return new LeotherasTheBlindMeleeTanksDontAttackDemonFormAction(botAI); } + + static Action* leotheras_the_blind_position_ranged( + PlayerbotAI* botAI) { return new LeotherasTheBlindPositionRangedAction(botAI); } + + static Action* leotheras_the_blind_run_away_from_whirlwind( + PlayerbotAI* botAI) { return new LeotherasTheBlindRunAwayFromWhirlwindAction(botAI); } + + static Action* leotheras_the_blind_melee_dps_run_away_from_boss( + PlayerbotAI* botAI) { return new LeotherasTheBlindMeleeDpsRunAwayFromBossAction(botAI); } + + static Action* leotheras_the_blind_destroy_inner_demon( + PlayerbotAI* botAI) { return new LeotherasTheBlindDestroyInnerDemonAction(botAI); } + + static Action* leotheras_the_blind_misdirect_boss_to_demon_form_tank( + PlayerbotAI* botAI) { return new LeotherasTheBlindMisdirectBossToDemonFormTankAction(botAI); } + + static Action* leotheras_the_blind_final_phase_assign_dps_priority( + PlayerbotAI* botAI) { return new LeotherasTheBlindFinalPhaseAssignDpsPriorityAction(botAI); } + + static Action* leotheras_the_blind_manage_dps_wait_timers( + PlayerbotAI* botAI) { return new LeotherasTheBlindManageDpsWaitTimersAction(botAI); } + + // Fathom-Lord Karathress + static Action* fathom_lord_karathress_main_tank_position_boss( + PlayerbotAI* botAI) { return new FathomLordKarathressMainTankPositionBossAction(botAI); } + + static Action* fathom_lord_karathress_first_assist_tank_position_caribdis( + PlayerbotAI* botAI) { return new FathomLordKarathressFirstAssistTankPositionCaribdisAction(botAI); } + + static Action* fathom_lord_karathress_second_assist_tank_position_sharkkis( + PlayerbotAI* botAI) { return new FathomLordKarathressSecondAssistTankPositionSharkkisAction(botAI); } + + static Action* fathom_lord_karathress_third_assist_tank_position_tidalvess( + PlayerbotAI* botAI) { return new FathomLordKarathressThirdAssistTankPositionTidalvessAction(botAI); } + + static Action* fathom_lord_karathress_position_caribdis_tank_healer( + PlayerbotAI* botAI) { return new FathomLordKarathressPositionCaribdisTankHealerAction(botAI); } + + static Action* fathom_lord_karathress_misdirect_bosses_to_tanks( + PlayerbotAI* botAI) { return new FathomLordKarathressMisdirectBossesToTanksAction(botAI); } + + static Action* fathom_lord_karathress_assign_dps_priority( + PlayerbotAI* botAI) { return new FathomLordKarathressAssignDpsPriorityAction(botAI); } + + static Action* fathom_lord_karathress_manage_dps_timer( + PlayerbotAI* botAI) { return new FathomLordKarathressManageDpsTimerAction(botAI); } + + // Morogrim Tidewalker + static Action* morogrim_tidewalker_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new MorogrimTidewalkerMisdirectBossToMainTankAction(botAI); } + + static Action* morogrim_tidewalker_move_boss_to_tank_position( + PlayerbotAI* botAI) { return new MorogrimTidewalkerMoveBossToTankPositionAction(botAI); } + + static Action* morogrim_tidewalker_phase_2_reposition_ranged( + PlayerbotAI* botAI) { return new MorogrimTidewalkerPhase2RepositionRangedAction(botAI); } + + // Lady Vashj + static Action* lady_vashj_main_tank_position_boss( + PlayerbotAI* botAI) { return new LadyVashjMainTankPositionBossAction(botAI); } + + static Action* lady_vashj_phase_1_spread_ranged_in_arc( + PlayerbotAI* botAI) { return new LadyVashjPhase1SpreadRangedInArcAction(botAI); } + + static Action* lady_vashj_set_grounding_totem_in_main_tank_group( + PlayerbotAI* botAI) { return new LadyVashjSetGroundingTotemInMainTankGroupAction(botAI); } + + static Action* lady_vashj_static_charge_move_away_from_group( + PlayerbotAI* botAI) { return new LadyVashjStaticChargeMoveAwayFromGroupAction(botAI); } + + static Action* lady_vashj_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new LadyVashjMisdirectBossToMainTankAction(botAI); } + + static Action* lady_vashj_assign_phase_2_and_phase_3_dps_priority( + PlayerbotAI* botAI) { return new LadyVashjAssignPhase2AndPhase3DpsPriorityAction(botAI); } + + static Action* lady_vashj_misdirect_strider_to_first_assist_tank( + PlayerbotAI* botAI) { return new LadyVashjMisdirectStriderToFirstAssistTankAction(botAI); } + + static Action* lady_vashj_tank_attack_and_move_away_strider( + PlayerbotAI* botAI) { return new LadyVashjTankAttackAndMoveAwayStriderAction(botAI); } + + static Action* lady_vashj_teleport_to_tainted_elemental( + PlayerbotAI* botAI) { return new LadyVashjTeleportToTaintedElementalAction(botAI); } + + static Action* lady_vashj_loot_tainted_core( + PlayerbotAI* botAI) { return new LadyVashjLootTaintedCoreAction(botAI); } + + static Action* lady_vashj_pass_the_tainted_core( + PlayerbotAI* botAI) { return new LadyVashjPassTheTaintedCoreAction(botAI); } + + static Action* lady_vashj_destroy_tainted_core( + PlayerbotAI* botAI) { return new LadyVashjDestroyTaintedCoreAction(botAI); } + + static Action* lady_vashj_erase_core_passing_trackers( + PlayerbotAI* botAI) { return new LadyVashjEraseCorePassingTrackersAction(botAI); } + + static Action* lady_vashj_avoid_toxic_spores( + PlayerbotAI* botAI) { return new LadyVashjAvoidToxicSporesAction(botAI); } + + static Action* lady_vashj_use_free_action_abilities( + PlayerbotAI* botAI) { return new LadyVashjUseFreeActionAbilitiesAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h new file mode 100644 index 000000000..13135bf3e --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h @@ -0,0 +1,325 @@ +#ifndef _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H + +#include "RaidSSCTriggers.h" +#include "AiObjectContext.h" + +class RaidSSCTriggerContext : public NamedObjectContext +{ +public: + RaidSSCTriggerContext() + { + // General + creators["serpent shrine cavern bot is not in combat"] = + &RaidSSCTriggerContext::serpent_shrine_cavern_bot_is_not_in_combat; + + // Trash + creators["underbog colossus spawned toxic pool after death"] = + &RaidSSCTriggerContext::underbog_colossus_spawned_toxic_pool_after_death; + + creators["greyheart tidecaller water elemental totem spawned"] = + &RaidSSCTriggerContext::greyheart_tidecaller_water_elemental_totem_spawned; + + // Hydross the Unstable + creators["hydross the unstable bot is frost tank"] = + &RaidSSCTriggerContext::hydross_the_unstable_bot_is_frost_tank; + + creators["hydross the unstable bot is nature tank"] = + &RaidSSCTriggerContext::hydross_the_unstable_bot_is_nature_tank; + + creators["hydross the unstable elementals spawned"] = + &RaidSSCTriggerContext::hydross_the_unstable_elementals_spawned; + + creators["hydross the unstable danger from water tombs"] = + &RaidSSCTriggerContext::hydross_the_unstable_danger_from_water_tombs; + + creators["hydross the unstable tank needs aggro upon phase change"] = + &RaidSSCTriggerContext::hydross_the_unstable_tank_needs_aggro_upon_phase_change; + + creators["hydross the unstable aggro resets upon phase change"] = + &RaidSSCTriggerContext::hydross_the_unstable_aggro_resets_upon_phase_change; + + creators["hydross the unstable need to manage timers"] = + &RaidSSCTriggerContext::hydross_the_unstable_need_to_manage_timers; + + // The Lurker Below + creators["the lurker below spout is active"] = + &RaidSSCTriggerContext::the_lurker_below_spout_is_active; + + creators["the lurker below boss is active for main tank"] = + &RaidSSCTriggerContext::the_lurker_below_boss_is_active_for_main_tank; + + creators["the lurker below boss casts geyser"] = + &RaidSSCTriggerContext::the_lurker_below_boss_casts_geyser; + + creators["the lurker below boss is submerged"] = + &RaidSSCTriggerContext::the_lurker_below_boss_is_submerged; + + creators["the lurker below need to prepare timer for spout"] = + &RaidSSCTriggerContext::the_lurker_below_need_to_prepare_timer_for_spout; + + // Leotheras the Blind + creators["leotheras the blind boss is inactive"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_is_inactive; + + creators["leotheras the blind boss transformed into demon form"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_transformed_into_demon_form; + + creators["leotheras the blind only warlock should tank demon form"] = + &RaidSSCTriggerContext::leotheras_the_blind_only_warlock_should_tank_demon_form; + + creators["leotheras the blind boss engaged by ranged"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_engaged_by_ranged; + + creators["leotheras the blind boss channeling whirlwind"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_channeling_whirlwind; + + creators["leotheras the blind bot has too many chaos blast stacks"] = + &RaidSSCTriggerContext::leotheras_the_blind_bot_has_too_many_chaos_blast_stacks; + + creators["leotheras the blind inner demon has awakened"] = + &RaidSSCTriggerContext::leotheras_the_blind_inner_demon_has_awakened; + + creators["leotheras the blind entered final phase"] = + &RaidSSCTriggerContext::leotheras_the_blind_entered_final_phase; + + creators["leotheras the blind demon form tank needs aggro"] = + &RaidSSCTriggerContext::leotheras_the_blind_demon_form_tank_needs_aggro; + + creators["leotheras the blind boss wipes aggro upon phase change"] = + &RaidSSCTriggerContext::leotheras_the_blind_boss_wipes_aggro_upon_phase_change; + + // Fathom-Lord Karathress + creators["fathom-lord karathress boss engaged by main tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_boss_engaged_by_main_tank; + + creators["fathom-lord karathress caribdis engaged by first assist tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_caribdis_engaged_by_first_assist_tank; + + creators["fathom-lord karathress sharkkis engaged by second assist tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_sharkkis_engaged_by_second_assist_tank; + + creators["fathom-lord karathress tidalvess engaged by third assist tank"] = + &RaidSSCTriggerContext::fathom_lord_karathress_tidalvess_engaged_by_third_assist_tank; + + creators["fathom-lord karathress caribdis tank needs dedicated healer"] = + &RaidSSCTriggerContext::fathom_lord_karathress_caribdis_tank_needs_dedicated_healer; + + creators["fathom-lord karathress pulling bosses"] = + &RaidSSCTriggerContext::fathom_lord_karathress_pulling_bosses; + + creators["fathom-lord karathress determining kill order"] = + &RaidSSCTriggerContext::fathom_lord_karathress_determining_kill_order; + + creators["fathom-lord karathress tanks need to establish aggro"] = + &RaidSSCTriggerContext::fathom_lord_karathress_tanks_need_to_establish_aggro; + + // Morogrim Tidewalker + creators["morogrim tidewalker boss engaged by main tank"] = + &RaidSSCTriggerContext::morogrim_tidewalker_boss_engaged_by_main_tank; + + creators["morogrim tidewalker pulling boss"] = + &RaidSSCTriggerContext::morogrim_tidewalker_pulling_boss; + + creators["morogrim tidewalker water globules are incoming"] = + &RaidSSCTriggerContext::morogrim_tidewalker_water_globules_are_incoming; + + // Lady Vashj + creators["lady vashj boss engaged by main tank"] = + &RaidSSCTriggerContext::lady_vashj_boss_engaged_by_main_tank; + + creators["lady vashj boss engaged by ranged in phase 1"] = + &RaidSSCTriggerContext::lady_vashj_boss_engaged_by_ranged_in_phase_1; + + creators["lady vashj casts shock blast on highest aggro"] = + &RaidSSCTriggerContext::lady_vashj_casts_shock_blast_on_highest_aggro; + + creators["lady vashj bot has static charge"] = + &RaidSSCTriggerContext::lady_vashj_bot_has_static_charge; + + creators["lady vashj pulling boss in phase 1 and phase 3"] = + &RaidSSCTriggerContext::lady_vashj_pulling_boss_in_phase_1_and_phase_3; + + creators["lady vashj adds spawn in phase 2 and phase 3"] = + &RaidSSCTriggerContext::lady_vashj_adds_spawn_in_phase_2_and_phase_3; + + creators["lady vashj coilfang strider is approaching"] = + &RaidSSCTriggerContext::lady_vashj_coilfang_strider_is_approaching; + + creators["lady vashj tainted elemental cheat"] = + &RaidSSCTriggerContext::lady_vashj_tainted_elemental_cheat; + + creators["lady vashj tainted core was looted"] = + &RaidSSCTriggerContext::lady_vashj_tainted_core_was_looted; + + creators["lady vashj tainted core is unusable"] = + &RaidSSCTriggerContext::lady_vashj_tainted_core_is_unusable; + + creators["lady vashj need to reset core passing trackers"] = + &RaidSSCTriggerContext::lady_vashj_need_to_reset_core_passing_trackers; + + creators["lady vashj toxic sporebats are spewing poison clouds"] = + &RaidSSCTriggerContext::lady_vashj_toxic_sporebats_are_spewing_poison_clouds; + + creators["lady vashj bot is entangled in toxic spores or static charge"] = + &RaidSSCTriggerContext::lady_vashj_bot_is_entangled_in_toxic_spores_or_static_charge; + } + +private: + // General + static Trigger* serpent_shrine_cavern_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new SerpentShrineCavernBotIsNotInCombatTrigger(botAI); } + + // Trash + static Trigger* underbog_colossus_spawned_toxic_pool_after_death( + PlayerbotAI* botAI) { return new UnderbogColossusSpawnedToxicPoolAfterDeathTrigger(botAI); } + + static Trigger* greyheart_tidecaller_water_elemental_totem_spawned( + PlayerbotAI* botAI) { return new GreyheartTidecallerWaterElementalTotemSpawnedTrigger(botAI); } + + // Hydross the Unstable + static Trigger* hydross_the_unstable_bot_is_frost_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstableBotIsFrostTankTrigger(botAI); } + + static Trigger* hydross_the_unstable_bot_is_nature_tank( + PlayerbotAI* botAI) { return new HydrossTheUnstableBotIsNatureTankTrigger(botAI); } + + static Trigger* hydross_the_unstable_elementals_spawned( + PlayerbotAI* botAI) { return new HydrossTheUnstableElementalsSpawnedTrigger(botAI); } + + static Trigger* hydross_the_unstable_danger_from_water_tombs( + PlayerbotAI* botAI) { return new HydrossTheUnstableDangerFromWaterTombsTrigger(botAI); } + + static Trigger* hydross_the_unstable_tank_needs_aggro_upon_phase_change( + PlayerbotAI* botAI) { return new HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger(botAI); } + + static Trigger* hydross_the_unstable_aggro_resets_upon_phase_change( + PlayerbotAI* botAI) { return new HydrossTheUnstableAggroResetsUponPhaseChangeTrigger(botAI); } + + static Trigger* hydross_the_unstable_need_to_manage_timers( + PlayerbotAI* botAI) { return new HydrossTheUnstableNeedToManageTimersTrigger(botAI); } + + // The Lurker Below + static Trigger* the_lurker_below_spout_is_active( + PlayerbotAI* botAI) { return new TheLurkerBelowSpoutIsActiveTrigger(botAI); } + + static Trigger* the_lurker_below_boss_is_active_for_main_tank( + PlayerbotAI* botAI) { return new TheLurkerBelowBossIsActiveForMainTankTrigger(botAI); } + + static Trigger* the_lurker_below_boss_casts_geyser( + PlayerbotAI* botAI) { return new TheLurkerBelowBossCastsGeyserTrigger(botAI); } + + static Trigger* the_lurker_below_boss_is_submerged( + PlayerbotAI* botAI) { return new TheLurkerBelowBossIsSubmergedTrigger(botAI); } + + static Trigger* the_lurker_below_need_to_prepare_timer_for_spout( + PlayerbotAI* botAI) { return new TheLurkerBelowNeedToPrepareTimerForSpoutTrigger(botAI); } + + // Leotheras the Blind + static Trigger* leotheras_the_blind_boss_is_inactive( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossIsInactiveTrigger(botAI); } + + static Trigger* leotheras_the_blind_boss_transformed_into_demon_form( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossTransformedIntoDemonFormTrigger(botAI); } + + static Trigger* leotheras_the_blind_only_warlock_should_tank_demon_form( + PlayerbotAI* botAI) { return new LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger(botAI); } + + static Trigger* leotheras_the_blind_boss_engaged_by_ranged( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossEngagedByRangedTrigger(botAI); } + + static Trigger* leotheras_the_blind_boss_channeling_whirlwind( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossChannelingWhirlwindTrigger(botAI); } + + static Trigger* leotheras_the_blind_bot_has_too_many_chaos_blast_stacks( + PlayerbotAI* botAI) { return new LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger(botAI); } + + static Trigger* leotheras_the_blind_inner_demon_has_awakened( + PlayerbotAI* botAI) { return new LeotherasTheBlindInnerDemonHasAwakenedTrigger(botAI); } + + static Trigger* leotheras_the_blind_entered_final_phase( + PlayerbotAI* botAI) { return new LeotherasTheBlindEnteredFinalPhaseTrigger(botAI); } + + static Trigger* leotheras_the_blind_demon_form_tank_needs_aggro( + PlayerbotAI* botAI) { return new LeotherasTheBlindDemonFormTankNeedsAggro(botAI); } + + static Trigger* leotheras_the_blind_boss_wipes_aggro_upon_phase_change( + PlayerbotAI* botAI) { return new LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger(botAI); } + + // Fathom-Lord Karathress + static Trigger* fathom_lord_karathress_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressBossEngagedByMainTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_caribdis_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_sharkkis_engaged_by_second_assist_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_tidalvess_engaged_by_third_assist_tank( + PlayerbotAI* botAI) { return new FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger(botAI); } + + static Trigger* fathom_lord_karathress_caribdis_tank_needs_dedicated_healer( + PlayerbotAI* botAI) { return new FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger(botAI); } + + static Trigger* fathom_lord_karathress_pulling_bosses( + PlayerbotAI* botAI) { return new FathomLordKarathressPullingBossesTrigger(botAI); } + + static Trigger* fathom_lord_karathress_determining_kill_order( + PlayerbotAI* botAI) { return new FathomLordKarathressDeterminingKillOrderTrigger(botAI); } + + static Trigger* fathom_lord_karathress_tanks_need_to_establish_aggro( + PlayerbotAI* botAI) { return new FathomLordKarathressTanksNeedToEstablishAggroTrigger(botAI); } + + // Morogrim Tidewalker + static Trigger* morogrim_tidewalker_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new MorogrimTidewalkerBossEngagedByMainTankTrigger(botAI); } + + static Trigger* morogrim_tidewalker_pulling_boss( + PlayerbotAI* botAI) { return new MorogrimTidewalkerPullingBossTrigger(botAI); } + + static Trigger* morogrim_tidewalker_water_globules_are_incoming( + PlayerbotAI* botAI) { return new MorogrimTidewalkerWaterGlobulesAreIncomingTrigger(botAI); } + + // Lady Vashj + static Trigger* lady_vashj_boss_engaged_by_main_tank( + PlayerbotAI* botAI) { return new LadyVashjBossEngagedByMainTankTrigger(botAI); } + + static Trigger* lady_vashj_boss_engaged_by_ranged_in_phase_1( + PlayerbotAI* botAI) { return new LadyVashjBossEngagedByRangedInPhase1Trigger(botAI); } + + static Trigger* lady_vashj_casts_shock_blast_on_highest_aggro( + PlayerbotAI* botAI) { return new LadyVashjCastsShockBlastOnHighestAggroTrigger(botAI); } + + static Trigger* lady_vashj_bot_has_static_charge( + PlayerbotAI* botAI) { return new LadyVashjBotHasStaticChargeTrigger(botAI); } + + static Trigger* lady_vashj_pulling_boss_in_phase_1_and_phase_3( + PlayerbotAI* botAI) { return new LadyVashjPullingBossInPhase1AndPhase3Trigger(botAI); } + + static Trigger* lady_vashj_adds_spawn_in_phase_2_and_phase_3( + PlayerbotAI* botAI) { return new LadyVashjAddsSpawnInPhase2AndPhase3Trigger(botAI); } + + static Trigger* lady_vashj_coilfang_strider_is_approaching( + PlayerbotAI* botAI) { return new LadyVashjCoilfangStriderIsApproachingTrigger(botAI); } + + static Trigger* lady_vashj_tainted_elemental_cheat( + PlayerbotAI* botAI) { return new LadyVashjTaintedElementalCheatTrigger(botAI); } + + static Trigger* lady_vashj_tainted_core_was_looted( + PlayerbotAI* botAI) { return new LadyVashjTaintedCoreWasLootedTrigger(botAI); } + + static Trigger* lady_vashj_tainted_core_is_unusable( + PlayerbotAI* botAI) { return new LadyVashjTaintedCoreIsUnusableTrigger(botAI); } + + static Trigger* lady_vashj_need_to_reset_core_passing_trackers( + PlayerbotAI* botAI) { return new LadyVashjNeedToResetCorePassingTrackersTrigger(botAI); } + + static Trigger* lady_vashj_toxic_sporebats_are_spewing_poison_clouds( + PlayerbotAI* botAI) { return new LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger(botAI); } + + static Trigger* lady_vashj_bot_is_entangled_in_toxic_spores_or_static_charge( + PlayerbotAI* botAI) { return new LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp new file mode 100644 index 000000000..139667dc6 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp @@ -0,0 +1,206 @@ +#include "RaidSSCStrategy.h" +#include "RaidSSCMultipliers.h" + +void RaidSSCStrategy::InitTriggers(std::vector& triggers) +{ + // General + triggers.push_back(new TriggerNode("serpent shrine cavern bot is not in combat", { + NextAction("serpent shrine cavern erase timers and trackers", ACTION_EMERGENCY + 11) })); + + // Trash Mobs + triggers.push_back(new TriggerNode("underbog colossus spawned toxic pool after death", { + NextAction("underbog colossus escape toxic pool", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("greyheart tidecaller water elemental totem spawned", { + NextAction("greyheart tidecaller mark water elemental totem", ACTION_RAID + 1) })); + + // Hydross the Unstable + triggers.push_back(new TriggerNode("hydross the unstable bot is frost tank", { + NextAction("hydross the unstable position frost tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable bot is nature tank", { + NextAction("hydross the unstable position nature tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable elementals spawned", { + NextAction("hydross the unstable prioritize elemental adds", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable danger from water tombs", { + NextAction("hydross the unstable frost phase spread out", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("hydross the unstable tank needs aggro upon phase change", { + NextAction("hydross the unstable misdirect boss to tank", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("hydross the unstable aggro resets upon phase change", { + NextAction("hydross the unstable stop dps upon phase change", ACTION_EMERGENCY + 9) })); + + triggers.push_back(new TriggerNode("hydross the unstable need to manage timers", { + NextAction("hydross the unstable manage timers", ACTION_EMERGENCY + 10) })); + + // The Lurker Below + triggers.push_back(new TriggerNode("the lurker below spout is active", { + NextAction("the lurker below run around behind boss", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("the lurker below boss is active for main tank", { + NextAction("the lurker below position main tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("the lurker below boss casts geyser", { + NextAction("the lurker below spread ranged in arc", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("the lurker below boss is submerged", { + NextAction("the lurker below tanks pick up adds", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("the lurker below need to prepare timer for spout", { + NextAction("the lurker below manage spout timer", ACTION_EMERGENCY + 10) })); + + // Leotheras the Blind + triggers.push_back(new TriggerNode("leotheras the blind boss is inactive", { + NextAction("leotheras the blind target spellbinders", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss transformed into demon form", { + NextAction("leotheras the blind demon form tank attack boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind only warlock should tank demon form", { + NextAction("leotheras the blind melee tanks don't attack demon form", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss engaged by ranged", { + NextAction("leotheras the blind position ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss channeling whirlwind", { + NextAction("leotheras the blind run away from whirlwind", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind bot has too many chaos blast stacks", { + NextAction("leotheras the blind melee dps run away from boss", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("leotheras the blind inner demon has awakened", { + NextAction("leotheras the blind destroy inner demon", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("leotheras the blind entered final phase", { + NextAction("leotheras the blind final phase assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("leotheras the blind demon form tank needs aggro", { + NextAction("leotheras the blind misdirect boss to demon form tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("leotheras the blind boss wipes aggro upon phase change", { + NextAction("leotheras the blind manage dps wait timers", ACTION_EMERGENCY + 10) })); + + // Fathom-Lord Karathress + triggers.push_back(new TriggerNode("fathom-lord karathress boss engaged by main tank", { + NextAction("fathom-lord karathress main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress caribdis engaged by first assist tank", { + NextAction("fathom-lord karathress first assist tank position caribdis", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress sharkkis engaged by second assist tank", { + NextAction("fathom-lord karathress second assist tank position sharkkis", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress tidalvess engaged by third assist tank", { + NextAction("fathom-lord karathress third assist tank position tidalvess", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress caribdis tank needs dedicated healer", { + NextAction("fathom-lord karathress position caribdis tank healer", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress pulling bosses", { + NextAction("fathom-lord karathress misdirect bosses to tanks", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress determining kill order", { + NextAction("fathom-lord karathress assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("fathom-lord karathress tanks need to establish aggro", { + NextAction("fathom-lord karathress manage dps timer", ACTION_EMERGENCY + 10) })); + + // Morogrim Tidewalker + triggers.push_back(new TriggerNode("morogrim tidewalker boss engaged by main tank", { + NextAction("morogrim tidewalker move boss to tank position", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("morogrim tidewalker water globules are incoming", { + NextAction("morogrim tidewalker phase 2 reposition ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("morogrim tidewalker pulling boss", { + NextAction("morogrim tidewalker misdirect boss to main tank", ACTION_RAID + 1) })); + + // Lady Vashj + triggers.push_back(new TriggerNode("lady vashj boss engaged by main tank", { + NextAction("lady vashj main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("lady vashj boss engaged by ranged in phase 1", { + NextAction("lady vashj phase 1 spread ranged in arc", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("lady vashj casts shock blast on highest aggro", { + NextAction("lady vashj set grounding totem in main tank group", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("lady vashj bot has static charge", { + NextAction("lady vashj static charge move away from group", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("lady vashj pulling boss in phase 1 and phase 3", { + NextAction("lady vashj misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("lady vashj tainted elemental cheat", { + NextAction("lady vashj teleport to tainted elemental", ACTION_EMERGENCY + 10), + NextAction("lady vashj loot tainted core", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("lady vashj tainted core was looted", { + NextAction("lady vashj pass the tainted core", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("lady vashj tainted core is unusable", { + NextAction("lady vashj destroy tainted core", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("lady vashj need to reset core passing trackers", { + NextAction("lady vashj erase core passing trackers", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("lady vashj adds spawn in phase 2 and phase 3", { + NextAction("lady vashj assign phase 2 and phase 3 dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("lady vashj coilfang strider is approaching", { + NextAction("lady vashj misdirect strider to first assist tank", ACTION_EMERGENCY + 2), + NextAction("lady vashj tank attack and move away strider", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("lady vashj toxic sporebats are spewing poison clouds", { + NextAction("lady vashj avoid toxic spores", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("lady vashj bot is entangled in toxic spores or static charge", { + NextAction("lady vashj use free action abilities", ACTION_EMERGENCY + 7) })); +} + +void RaidSSCStrategy::InitMultipliers(std::vector& multipliers) +{ + // Trash Mobs + multipliers.push_back(new UnderbogColossusEscapeToxicPoolMultiplier(botAI)); + + // Hydross the Unstable + multipliers.push_back(new HydrossTheUnstableDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new HydrossTheUnstableWaitForDpsMultiplier(botAI)); + multipliers.push_back(new HydrossTheUnstableControlMisdirectionMultiplier(botAI)); + + // The Lurker Below + multipliers.push_back(new TheLurkerBelowStayAwayFromSpoutMultiplier(botAI)); + multipliers.push_back(new TheLurkerBelowMaintainRangedSpreadMultiplier(botAI)); + multipliers.push_back(new TheLurkerBelowDisableTankAssistMultiplier(botAI)); + + // Leotheras the Blind + multipliers.push_back(new LeotherasTheBlindAvoidWhirlwindMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindFocusOnInnerDemonMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindWaitForDpsMultiplier(botAI)); + multipliers.push_back(new LeotherasTheBlindDelayBloodlustAndHeroismMultiplier(botAI)); + + // Fathom-Lord Karathress + multipliers.push_back(new FathomLordKarathressDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressDisableAoeMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressWaitForDpsMultiplier(botAI)); + multipliers.push_back(new FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier(botAI)); + + // Morogrim Tidewalker + multipliers.push_back(new MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new MorogrimTidewalkerDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new MorogrimTidewalkerMaintainPhase2StackingMultiplier(botAI)); + + // Lady Vashj + multipliers.push_back(new LadyVashjDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new LadyVashjMaintainPhase1RangedSpreadMultiplier(botAI)); + multipliers.push_back(new LadyVashjStaticChargeStayAwayFromGroupMultiplier(botAI)); + multipliers.push_back(new LadyVashjDoNotLootTheTaintedCoreMultiplier(botAI)); + multipliers.push_back(new LadyVashjCorePassersPrioritizePositioningMultiplier(botAI)); + multipliers.push_back(new LadyVashjDisableAutomaticTargetingAndMovementModifier(botAI)); +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h new file mode 100644 index 000000000..3c2c05f58 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDSSCSTRATEGY_H_ +#define _PLAYERBOT_RAIDSSCSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidSSCStrategy : public Strategy +{ +public: + RaidSSCStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "ssc"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp new file mode 100644 index 000000000..e77e63642 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp @@ -0,0 +1,670 @@ +#include "RaidSSCTriggers.h" +#include "RaidSSCHelpers.h" +#include "RaidSSCActions.h" +#include "AiFactory.h" +#include "Corpse.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace SerpentShrineCavernHelpers; + +// General +bool SerpentShrineCavernBotIsNotInCombatTrigger::IsActive() +{ + return !bot->IsInCombat(); +} + +// Trash Mobs + +bool UnderbogColossusSpawnedToxicPoolAfterDeathTrigger::IsActive() +{ + return bot->HasAura(SPELL_TOXIC_POOL); +} + +bool GreyheartTidecallerWaterElementalTotemSpawnedTrigger::IsActive() +{ + return botAI->IsDps(bot) && + GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM); +} + +// Hydross the Unstable + +bool HydrossTheUnstableBotIsFrostTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hydross the unstable") && + botAI->IsMainTank(bot); +} + +bool HydrossTheUnstableBotIsNatureTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hydross the unstable") && + botAI->IsAssistTankOfIndex(bot, 0, true); +} + +bool HydrossTheUnstableElementalsSpawnedTrigger::IsActive() +{ + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); + if (hydross && hydross->GetHealthPct() < 10.0f) + return false; + + if (!AI_VALUE2(Unit*, "find target", "pure spawn of hydross") && + !AI_VALUE2(Unit*, "find target", "tainted spawn of hydross")) + return false; + + return !botAI->IsHeal(bot) && !botAI->IsMainTank(bot) && + !botAI->IsAssistTankOfIndex(bot, 0, true); +} + +bool HydrossTheUnstableDangerFromWaterTombsTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); +} + +bool HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger::IsActive() +{ + return bot->getClass() == CLASS_HUNTER && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); +} + +bool HydrossTheUnstableAggroResetsUponPhaseChangeTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + return false; + + return bot->getClass() != CLASS_HUNTER && + !botAI->IsHeal(bot) && + !botAI->IsMainTank(bot) && + !botAI->IsAssistTankOfIndex(bot, 0, true); +} + +bool HydrossTheUnstableNeedToManageTimersTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hydross the unstable") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// The Lurker Below + +bool TheLurkerBelowSpoutIsActiveTrigger::IsActive() +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + return it != lurkerSpoutTimer.end() && it->second > now; +} + +bool TheLurkerBelowBossIsActiveForMainTankTrigger::IsActive() +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + if (!botAI->IsMainTank(bot)) + return false; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + return lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED && + (it == lurkerSpoutTimer.end() || it->second <= now); +} + +bool TheLurkerBelowBossCastsGeyserTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) + return false; + + const time_t now = std::time(nullptr); + + auto it = lurkerSpoutTimer.find(lurker->GetMap()->GetInstanceId()); + return lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED && + (it == lurkerSpoutTimer.end() || it->second <= now); +} + +// Trigger will be active only if there are at least 3 tanks in the raid +bool TheLurkerBelowBossIsSubmergedTrigger::IsActive() +{ + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) + return false; + + Player* mainTank = nullptr; + Player* firstAssistTank = nullptr; + Player* secondAssistTank = nullptr; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (!mainTank && memberAI->IsMainTank(member)) + mainTank = member; + else if (!firstAssistTank && memberAI->IsAssistTankOfIndex(member, 0, true)) + firstAssistTank = member; + else if (!secondAssistTank && memberAI->IsAssistTankOfIndex(member, 1, true)) + secondAssistTank = member; + } + + if (!mainTank || !firstAssistTank || !secondAssistTank) + return false; + + return bot == mainTank || bot == firstAssistTank || bot == secondAssistTank; +} + +bool TheLurkerBelowNeedToPrepareTimerForSpoutTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "the lurker below") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// Leotheras the Blind + +bool LeotherasTheBlindBossIsInactiveTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "greyheart spellbinder"); +} + +bool LeotherasTheBlindBossTransformedIntoDemonFormTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + if (GetLeotherasDemonFormTank(bot) != bot) + return false; + + return GetActiveLeotherasDemon(botAI); +} + +bool LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger::IsActive() +{ + if (botAI->IsRanged(bot) || !botAI->IsTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (!GetLeotherasDemonFormTank(bot)) + return false; + + return GetPhase2LeotherasDemon(botAI); +} + +bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (!botAI->IsRanged(bot)) + return false; + + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras) + return false; + + return !leotheras->HasAura(SPELL_LEOTHERAS_BANISHED) && + !leotheras->HasAura(SPELL_WHIRLWIND) && + !leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL); +} + +bool LeotherasTheBlindBossChannelingWhirlwindTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (botAI->IsTank(bot) && botAI->IsMelee(bot)) + return false; + + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras || leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + return false; + + return leotheras->HasAura(SPELL_WHIRLWIND) || + leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL); +} + +bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (botAI->IsRanged(bot)) + return false; + + Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); + if (!chaosBlast || chaosBlast->GetStackAmount() < 5) + return false; + + if (!GetLeotherasDemonFormTank(bot) && botAI->IsMainTank(bot)) + return false; + + return GetPhase2LeotherasDemon(botAI); +} + +bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive() +{ + return bot->HasAura(SPELL_INSIDIOUS_WHISPER) && + GetLeotherasDemonFormTank(bot) != bot; +} + +bool LeotherasTheBlindEnteredFinalPhaseTrigger::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (botAI->IsHeal(bot)) + return false; + + if (GetLeotherasDemonFormTank(bot) == bot) + return false; + + return GetPhase3LeotherasDemon(botAI) && + GetLeotherasHuman(botAI); +} + +bool LeotherasTheBlindDemonFormTankNeedsAggro::IsActive() +{ + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (bot->getClass() != CLASS_HUNTER) + return false; + + return AI_VALUE2(Unit*, "find target", "leotheras the blind"); +} + +bool LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "leotheras the blind") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// Fathom-Lord Karathress + +bool FathomLordKarathressBossEngagedByMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && + botAI->IsMainTank(bot); +} + +bool FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-guard caribdis") && + botAI->IsAssistTankOfIndex(bot, 0, false); +} + +bool FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis") && + botAI->IsAssistTankOfIndex(bot, 1, false); +} + +bool FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess") && + botAI->IsAssistTankOfIndex(bot, 2, false); +} + +bool FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger::IsActive() +{ + Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + if (!caribdis) + return false; + + if (!botAI->IsAssistHealOfIndex(bot, 0, true)) + return false; + + Player* firstAssistTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->IsAssistTankOfIndex(member, 0, false)) + { + firstAssistTank = member; + break; + } + } + } + + return firstAssistTank; +} + +bool FathomLordKarathressPullingBossesTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); + return karathress && karathress->GetHealthPct() > 98.0f; +} + +bool FathomLordKarathressDeterminingKillOrderTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return false; + + if (botAI->IsHeal(bot)) + return false; + + if (botAI->IsDps(bot)) + return true; + else if (botAI->IsAssistTankOfIndex(bot, 0, false)) + return !AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); + else if (botAI->IsAssistTankOfIndex(bot, 1, false)) + return !AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); + else if (botAI->IsAssistTankOfIndex(bot, 2, false)) + return !AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); + else + return false; +} + +bool FathomLordKarathressTanksNeedToEstablishAggroTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && + IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); +} + +// Morogrim Tidewalker + +bool MorogrimTidewalkerPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + return tidewalker && tidewalker->GetHealthPct() > 95.0f; +} + +bool MorogrimTidewalkerBossEngagedByMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "morogrim tidewalker") && + botAI->IsMainTank(bot); +} + +bool MorogrimTidewalkerWaterGlobulesAreIncomingTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); + return tidewalker && tidewalker->GetHealthPct() < 25.0f; +} + +// Lady Vashj + +bool LadyVashjBossEngagedByMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "lady vashj") && + !IsLadyVashjInPhase2(botAI) && botAI->IsMainTank(bot); +} + +bool LadyVashjBossEngagedByRangedInPhase1Trigger::IsActive() +{ + return botAI->IsRanged(bot) && IsLadyVashjInPhase1(botAI); +} + +bool LadyVashjCastsShockBlastOnHighestAggroTrigger::IsActive() +{ + if (bot->getClass() != CLASS_SHAMAN) + return false; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || + IsLadyVashjInPhase2(botAI)) + return false; + + if (!IsMainTankInSameSubgroup(bot)) + return false; + + return true; +} + +bool LadyVashjBotHasStaticChargeTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura(SPELL_STATIC_CHARGE)) + return true; + } + } + + return false; +} + +bool LadyVashjPullingBossInPhase1AndPhase3Trigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + return (vashj->GetHealthPct() <= 100.0f && vashj->GetHealthPct() > 90.0f) || + (!vashj->HasUnitState(UNIT_STATE_ROOT) && vashj->GetHealthPct() <= 50.0f && + vashj->GetHealthPct() > 40.0f); +} + +bool LadyVashjAddsSpawnInPhase2AndPhase3Trigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + return AI_VALUE2(Unit*, "find target", "lady vashj") && + !IsLadyVashjInPhase1(botAI); +} + +bool LadyVashjCoilfangStriderIsApproachingTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "coilfang strider"); +} + +bool LadyVashjTaintedElementalCheatTrigger::IsActive() +{ + if (!botAI->HasCheat(BotCheatMask::raid)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return false; + + bool taintedPresent = false; + Unit* taintedUnit = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (taintedUnit) + taintedPresent = true; + else + { + GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses"); + for (auto const& guid : corpses) + { + LootObject loot(bot, guid); + WorldObject* object = loot.GetWorldObject(bot); + if (!object) + continue; + + if (Creature* creature = object->ToCreature()) + { + if (creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive()) + { + taintedPresent = true; + break; + } + } + } + } + + if (!taintedPresent) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + return (GetDesignatedCoreLooter(group, botAI) == bot && + !bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)); +} + +bool LadyVashjTaintedCoreWasLootedTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + + auto hasCore = [](Player* player) -> bool + { + return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); + }; + + if (bot == designatedLooter) + { + if (!hasCore(bot)) + return false; + } + else if (bot == firstCorePasser) + { + if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || + hasCore(fourthCorePasser)) + return false; + } + else if (bot == secondCorePasser) + { + if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) + return false; + } + else if (bot == thirdCorePasser) + { + if (hasCore(fourthCorePasser)) + return false; + } + else if (bot != fourthCorePasser) + return false; + + if (AnyRecentCoreInInventory(group, botAI)) + return true; + + // First and second passers move to positions as soon as the elemental appears + if (AI_VALUE2(Unit*, "find target", "tainted elemental") && + (bot == firstCorePasser || bot == secondCorePasser)) + return true; + + return false; +} + +bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + if (!IsLadyVashjInPhase2(botAI)) + return bot->HasItemCount(ITEM_TAINTED_CORE, 1, false); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* coreHandlers[] = + { + GetDesignatedCoreLooter(group, botAI), + GetFirstTaintedCorePasser(group, botAI), + GetSecondTaintedCorePasser(group, botAI), + GetThirdTaintedCorePasser(group, botAI), + GetFourthTaintedCorePasser(group, botAI) + }; + + if (bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + { + for (Player* coreHandler : coreHandlers) + { + if (coreHandler && bot == coreHandler) + return false; + } + return true; + } + + return false; +} + +bool LadyVashjNeedToResetCorePassingTrackersTrigger::IsActive() +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj || IsLadyVashjInPhase2(botAI)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr) || + GetDesignatedCoreLooter(group, botAI) == bot || + GetFirstTaintedCorePasser(group, botAI) == bot || + GetSecondTaintedCorePasser(group, botAI) == bot || + GetThirdTaintedCorePasser(group, botAI) == bot || + GetFourthTaintedCorePasser(group, botAI) == bot; +} + +bool LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger::IsActive() +{ + return IsLadyVashjInPhase3(botAI); +} + +bool LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->HasAura(SPELL_ENTANGLE)) + continue; + + if (botAI->IsMelee(member)) + return true; + } + } + + return false; +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h new file mode 100644 index 000000000..e106b58f3 --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h @@ -0,0 +1,414 @@ +#ifndef _PLAYERBOT_RAIDSSCTRIGGERS_H +#define _PLAYERBOT_RAIDSSCTRIGGERS_H + +#include "Trigger.h" + +// General + +class SerpentShrineCavernBotIsNotInCombatTrigger : public Trigger +{ +public: + SerpentShrineCavernBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "serpent shrine cavern bot is not in combat") {} + bool IsActive() override; +}; + +// Trash + +class UnderbogColossusSpawnedToxicPoolAfterDeathTrigger : public Trigger +{ +public: + UnderbogColossusSpawnedToxicPoolAfterDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "underbog colossus spawned toxic pool after death") {} + bool IsActive() override; +}; + +class GreyheartTidecallerWaterElementalTotemSpawnedTrigger : public Trigger +{ +public: + GreyheartTidecallerWaterElementalTotemSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "greyheart tidecaller water elemental totem spawned") {} + bool IsActive() override; +}; + +// Hydross the Unstable + +class HydrossTheUnstableBotIsFrostTankTrigger : public Trigger +{ +public: + HydrossTheUnstableBotIsFrostTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable bot is frost tank") {} + bool IsActive() override; +}; + +class HydrossTheUnstableBotIsNatureTankTrigger : public Trigger +{ +public: + HydrossTheUnstableBotIsNatureTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable bot is nature tank") {} + bool IsActive() override; +}; + +class HydrossTheUnstableElementalsSpawnedTrigger : public Trigger +{ +public: + HydrossTheUnstableElementalsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable elementals spawned") {} + bool IsActive() override; +}; + +class HydrossTheUnstableDangerFromWaterTombsTrigger : public Trigger +{ +public: + HydrossTheUnstableDangerFromWaterTombsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable danger from water tombs") {} + bool IsActive() override; +}; + +class HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger : public Trigger +{ +public: + HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable tank needs aggro upon phase change") {} + bool IsActive() override; +}; + +class HydrossTheUnstableAggroResetsUponPhaseChangeTrigger : public Trigger +{ +public: + HydrossTheUnstableAggroResetsUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable aggro resets upon phase change") {} + bool IsActive() override; +}; + +class HydrossTheUnstableNeedToManageTimersTrigger : public Trigger +{ +public: + HydrossTheUnstableNeedToManageTimersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hydross the unstable need to manage timers") {} + bool IsActive() override; +}; + +// The Lurker Below + +class TheLurkerBelowSpoutIsActiveTrigger : public Trigger +{ +public: + TheLurkerBelowSpoutIsActiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below spout is active") {} + bool IsActive() override; +}; + +class TheLurkerBelowBossIsActiveForMainTankTrigger : public Trigger +{ +public: + TheLurkerBelowBossIsActiveForMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below boss is active for main tank") {} + bool IsActive() override; +}; + +class TheLurkerBelowBossCastsGeyserTrigger : public Trigger +{ +public: + TheLurkerBelowBossCastsGeyserTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below boss casts geyser") {} + bool IsActive() override; +}; + +class TheLurkerBelowBossIsSubmergedTrigger : public Trigger +{ +public: + TheLurkerBelowBossIsSubmergedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below boss is submerged") {} + bool IsActive() override; +}; + +class TheLurkerBelowNeedToPrepareTimerForSpoutTrigger : public Trigger +{ +public: + TheLurkerBelowNeedToPrepareTimerForSpoutTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "the lurker below need to prepare timer for spout") {} + bool IsActive() override; +}; + +// Leotheras the Blind + +class LeotherasTheBlindBossIsInactiveTrigger : public Trigger +{ +public: + LeotherasTheBlindBossIsInactiveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss is inactive") {} + bool IsActive() override; +}; + +class LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger : public Trigger +{ +public: + LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind only warlock should tank demon form") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossTransformedIntoDemonFormTrigger : public Trigger +{ +public: + LeotherasTheBlindBossTransformedIntoDemonFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss transformed into demon form") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossEngagedByRangedTrigger : public Trigger +{ +public: + LeotherasTheBlindBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss engaged by ranged") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossChannelingWhirlwindTrigger : public Trigger +{ +public: + LeotherasTheBlindBossChannelingWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss channeling whirlwind") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger : public Trigger +{ +public: + LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind bot has too many chaos blast stacks") {} + bool IsActive() override; +}; + +class LeotherasTheBlindInnerDemonHasAwakenedTrigger : public Trigger +{ +public: + LeotherasTheBlindInnerDemonHasAwakenedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind inner demon has awakened") {} + bool IsActive() override; +}; + +class LeotherasTheBlindEnteredFinalPhaseTrigger : public Trigger +{ +public: + LeotherasTheBlindEnteredFinalPhaseTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind entered final phase") {} + bool IsActive() override; +}; + +class LeotherasTheBlindDemonFormTankNeedsAggro : public Trigger +{ +public: + LeotherasTheBlindDemonFormTankNeedsAggro( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind demon form tank needs aggro") {} + bool IsActive() override; +}; + +class LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger : public Trigger +{ +public: + LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "leotheras the blind boss wipes aggro upon phase change") {} + bool IsActive() override; +}; + +// Fathom-Lord Karathress + +class FathomLordKarathressBossEngagedByMainTankTrigger : public Trigger +{ +public: + FathomLordKarathressBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress boss engaged by main tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress caribdis engaged by first assist tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress sharkkis engaged by second assist tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger : public Trigger +{ +public: + FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress tidalvess engaged by third assist tank") {} + bool IsActive() override; +}; + +class FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger : public Trigger +{ +public: + FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress caribdis tank needs dedicated healer") {} + bool IsActive() override; +}; + +class FathomLordKarathressPullingBossesTrigger : public Trigger +{ +public: + FathomLordKarathressPullingBossesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress pulling bosses") {} + bool IsActive() override; +}; + +class FathomLordKarathressDeterminingKillOrderTrigger : public Trigger +{ +public: + FathomLordKarathressDeterminingKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress determining kill order") {} + bool IsActive() override; +}; + +class FathomLordKarathressTanksNeedToEstablishAggroTrigger : public Trigger +{ +public: + FathomLordKarathressTanksNeedToEstablishAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "fathom-lord karathress tanks need to establish aggro") {} + bool IsActive() override; +}; + +// Morogrim Tidewalker + +class MorogrimTidewalkerPullingBossTrigger : public Trigger +{ +public: + MorogrimTidewalkerPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "morogrim tidewalker pulling boss") {} + bool IsActive() override; +}; + +class MorogrimTidewalkerBossEngagedByMainTankTrigger : public Trigger +{ +public: + MorogrimTidewalkerBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "morogrim tidewalker boss engaged by main tank") {} + bool IsActive() override; +}; + +class MorogrimTidewalkerWaterGlobulesAreIncomingTrigger : public Trigger +{ +public: + MorogrimTidewalkerWaterGlobulesAreIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "morogrim tidewalker water globules are incoming") {} + bool IsActive() override; +}; + +// Lady Vashj + +class LadyVashjBossEngagedByMainTankTrigger : public Trigger +{ +public: + LadyVashjBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj boss engaged by main tank") {} + bool IsActive() override; +}; + +class LadyVashjBossEngagedByRangedInPhase1Trigger : public Trigger +{ +public: + LadyVashjBossEngagedByRangedInPhase1Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj boss engaged by ranged in phase 1") {} + bool IsActive() override; +}; + +class LadyVashjCastsShockBlastOnHighestAggroTrigger : public Trigger +{ +public: + LadyVashjCastsShockBlastOnHighestAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj casts shock blast on highest aggro") {} + bool IsActive() override; +}; + +class LadyVashjBotHasStaticChargeTrigger : public Trigger +{ +public: + LadyVashjBotHasStaticChargeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj bot has static charge") {} + bool IsActive() override; +}; + +class LadyVashjPullingBossInPhase1AndPhase3Trigger : public Trigger +{ +public: + LadyVashjPullingBossInPhase1AndPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj pulling boss in phase 1 and phase 3") {} + bool IsActive() override; +}; + +class LadyVashjAddsSpawnInPhase2AndPhase3Trigger : public Trigger +{ +public: + LadyVashjAddsSpawnInPhase2AndPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj adds spawn in phase 2 and phase 3") {} + bool IsActive() override; +}; + +class LadyVashjCoilfangStriderIsApproachingTrigger : public Trigger +{ +public: + LadyVashjCoilfangStriderIsApproachingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj coilfang strider is approaching") {} + bool IsActive() override; +}; + +class LadyVashjTaintedElementalCheatTrigger : public Trigger +{ +public: + LadyVashjTaintedElementalCheatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj tainted elemental cheat") {} + bool IsActive() override; +}; + +class LadyVashjTaintedCoreWasLootedTrigger : public Trigger +{ +public: + LadyVashjTaintedCoreWasLootedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj tainted core was looted") {} + bool IsActive() override; +}; + +class LadyVashjTaintedCoreIsUnusableTrigger : public Trigger +{ +public: + LadyVashjTaintedCoreIsUnusableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj tainted core is unusable") {} + bool IsActive() override; +}; + +class LadyVashjNeedToResetCorePassingTrackersTrigger : public Trigger +{ +public: + LadyVashjNeedToResetCorePassingTrackersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj need to reset core passing trackers") {} + bool IsActive() override; +}; + +class LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger : public Trigger +{ +public: + LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj toxic sporebats are spewing poison clouds") {} + bool IsActive() override; +}; + +class LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger : public Trigger +{ +public: + LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "lady vashj bot is entangled in toxic spores or static charge") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp new file mode 100644 index 000000000..7bda085be --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp @@ -0,0 +1,583 @@ +#include "RaidSSCHelpers.h" +#include "AiFactory.h" +#include "Creature.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace SerpentShrineCavernHelpers +{ + // Hydross the Unstable + + const Position HYDROSS_FROST_TANK_POSITION = { -236.669f, -358.352f, -0.828f }; + const Position HYDROSS_NATURE_TANK_POSITION = { -225.471f, -327.790f, -3.682f }; + + std::unordered_map hydrossFrostDpsWaitTimer; + std::unordered_map hydrossNatureDpsWaitTimer; + std::unordered_map hydrossChangeToFrostPhaseTimer; + std::unordered_map hydrossChangeToNaturePhaseTimer; + + bool HasMarkOfHydrossAt100Percent(Player* bot) + { + return bot->HasAura(SPELL_MARK_OF_HYDROSS_100) || + bot->HasAura(SPELL_MARK_OF_HYDROSS_250) || + bot->HasAura(SPELL_MARK_OF_HYDROSS_500); + } + + bool HasNoMarkOfHydross(Player* bot) + { + return !bot->HasAura(SPELL_MARK_OF_HYDROSS_10) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_25) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_50) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_100) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_250) && + !bot->HasAura(SPELL_MARK_OF_HYDROSS_500); + } + + bool HasMarkOfCorruptionAt100Percent(Player* bot) + { + return bot->HasAura(SPELL_MARK_OF_CORRUPTION_100) || + bot->HasAura(SPELL_MARK_OF_CORRUPTION_250) || + bot->HasAura(SPELL_MARK_OF_CORRUPTION_500); + } + + bool HasNoMarkOfCorruption(Player* bot) + { + return !bot->HasAura(SPELL_MARK_OF_CORRUPTION_10) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_25) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_50) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_100) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_250) && + !bot->HasAura(SPELL_MARK_OF_CORRUPTION_500); + } + + // The Lurker Below + + const Position LURKER_MAIN_TANK_POSITION = { 23.706f, -406.038f, -19.686f }; + + std::unordered_map lurkerSpoutTimer; + std::unordered_map lurkerRangedPositions; + + bool IsLurkerCastingSpout(Unit* lurker) + { + if (!lurker || !lurker->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* currentSpell = lurker->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!currentSpell) + return false; + + uint32 spellId = currentSpell->m_spellInfo->Id; + bool isSpout = spellId == SPELL_SPOUT_VISUAL; + + return isSpout; + } + + // Leotheras the Blind + + std::unordered_map leotherasHumanFormDpsWaitTimer; + std::unordered_map leotherasDemonFormDpsWaitTimer; + std::unordered_map leotherasFinalPhaseDpsWaitTimer; + + Unit* GetLeotherasHuman(PlayerbotAI* botAI) + { + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && + unit->IsInCombat() && !unit->HasAura(SPELL_METAMORPHOSIS)) + return unit; + } + return nullptr; + } + + Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI) + { + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && + unit->HasAura(SPELL_METAMORPHOSIS)) + return unit; + } + return nullptr; + } + + Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI) + { + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SHADOW_OF_LEOTHERAS) + return unit; + } + return nullptr; + } + + Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI) + { + Unit* phase2 = GetPhase2LeotherasDemon(botAI); + Unit* phase3 = GetPhase3LeotherasDemon(botAI); + return phase2 ? phase2 : phase3; + } + + Player* GetLeotherasDemonFormTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) First loop: Return the first assistant Warlock (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_WARLOCK) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + } + + // (2) Fall back to first found bot Warlock + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_WARLOCK) + continue; + + return member; + } + + // (3) Return nullptr if none found + return nullptr; + } + + // Fathom-Lord Karathress + + const Position KARATHRESS_TANK_POSITION = { 474.403f, -531.118f, -7.548f }; + const Position TIDALVESS_TANK_POSITION = { 511.282f, -501.162f, -13.158f }; + const Position SHARKKIS_TANK_POSITION = { 508.057f, -541.109f, -10.133f }; + const Position CARIBDIS_TANK_POSITION = { 464.462f, -475.820f, -13.158f }; + const Position CARIBDIS_HEALER_POSITION = { 466.203f, -503.201f, -13.158f }; + const Position CARIBDIS_RANGED_DPS_POSITION = { 463.197f, -501.190f, -13.158f }; + + std::unordered_map karathressDpsWaitTimer; + + // Morogrim Tidewalker + + const Position TIDEWALKER_PHASE_1_TANK_POSITION = { 410.925f, -741.916f, -7.146f }; + const Position TIDEWALKER_PHASE_TRANSITION_WAYPOINT = { 407.035f, -759.479f, -7.168f }; + const Position TIDEWALKER_PHASE_2_TANK_POSITION = { 446.571f, -767.155f, -7.144f }; + const Position TIDEWALKER_PHASE_2_RANGED_POSITION = { 432.595f, -766.288f, -7.145f }; + + std::unordered_map tidewalkerTankStep; + std::unordered_map tidewalkerRangedStep; + + // Lady Vashj + + const Position VASHJ_PLATFORM_CENTER_POSITION = { 29.634f, -923.541f, 42.985f }; + + std::unordered_map vashjRangedPositions; + std::unordered_map hasReachedVashjRangedPosition; + std::unordered_map nearestTriggerGuid; + std::unordered_map intendedLineup; + std::unordered_map lastImbueAttempt; + std::unordered_map lastCoreInInventoryTime; + + bool IsMainTankInSameSubgroup(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group || !group->isRaidGroup()) + return false; + + uint8 botSubGroup = group->GetMemberGroup(bot->GetGUID()); + if (botSubGroup >= MAX_RAID_SUBGROUPS) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (group->GetMemberGroup(member->GetGUID()) != botSubGroup) + continue; + + if (PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member)) + { + if (memberAI->IsMainTank(member)) + return true; + } + } + + return false; + } + + bool IsLadyVashjInPhase1(PlayerbotAI* botAI) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + Creature* vashjCreature = vashj->ToCreature(); + return vashjCreature && vashjCreature->GetHealthPct() > 70.0f && + vashjCreature->GetReactState() != REACT_PASSIVE; + } + + bool IsLadyVashjInPhase2(PlayerbotAI* botAI) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + Creature* vashjCreature = vashj->ToCreature(); + return vashjCreature && vashjCreature->GetReactState() == REACT_PASSIVE; + } + + bool IsLadyVashjInPhase3(PlayerbotAI* botAI) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + Creature* vashjCreature = vashj->ToCreature(); + return vashjCreature && vashjCreature->GetHealthPct() <= 50.0f && + vashjCreature->GetReactState() != REACT_PASSIVE; + } + + bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI) + { + if (!unit || !unit->IsAlive()) + return false; + + uint32 entry = unit->GetEntry(); + + if (IsLadyVashjInPhase2(botAI)) + { + return entry == NPC_TAINTED_ELEMENTAL || entry == NPC_ENCHANTED_ELEMENTAL || + entry == NPC_COILFANG_ELITE || entry == NPC_COILFANG_STRIDER; + } + else if (IsLadyVashjInPhase3(botAI)) + { + return entry == NPC_TAINTED_ELEMENTAL || entry == NPC_ENCHANTED_ELEMENTAL || + entry == NPC_COILFANG_ELITE || entry == NPC_COILFANG_STRIDER || + entry == NPC_TOXIC_SPOREBAT || entry == NPC_LADY_VASHJ; + } + + return false; + } + + bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + if (group) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return true; + } + } + + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); + const time_t now = std::time(nullptr); + + auto it = lastCoreInInventoryTime.find(instanceId); + if (it != lastCoreInInventoryTime.end()) + { + if ((now - it->second) <= static_cast(graceSeconds)) + return true; + } + + return false; + } + + Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* leader = nullptr; + ObjectGuid leaderGuid = group->GetLeaderGUID(); + if (!leaderGuid.IsEmpty()) + leader = ObjectAccessor::FindPlayer(leaderGuid); + + if (!botAI->HasCheat(BotCheatMask::raid)) + return leader; + + Player* fallback = leader; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == leader) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsMelee(member) && memberAI->IsDps(member)) + return member; + + if (!fallback && memberAI->IsRangedDps(member)) + fallback = member; + } + + return fallback ? fallback : leader; + } + + Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistHealOfIndex(member, 0, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter) + continue; + return member; + } + + return nullptr; + } + + Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter || + member == firstCorePasser) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistHealOfIndex(member, 1, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter || + member == firstCorePasser) + continue; + return member; + } + + return nullptr; + } + + Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistHealOfIndex(member, 2, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser) + continue; + return member; + } + + return nullptr; + } + + Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI) + { + if (!group) + return nullptr; + + Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser || + member == thirdCorePasser) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + + if (memberAI->IsAssistRangedDpsOfIndex(member, 0, true)) + return member; + } + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + botAI->IsTank(member) || member == designatedLooter || + member == firstCorePasser || member == secondCorePasser || + member == thirdCorePasser) + continue; + return member; + } + + return nullptr; + } + + const std::vector SHIELD_GENERATOR_DB_GUIDS = + { + 47482, // NW + 47483, // NE + 47484, // SE + 47485 // SW + }; + + // Get the positions of all active Shield Generators by their database GUIDs + std::vector GetAllGeneratorInfosByDbGuids( + Map* map, const std::vector& generatorDbGuids) + { + std::vector generators; + if (!map) + return generators; + + for (uint32 dbGuid : generatorDbGuids) + { + auto bounds = map->GetGameObjectBySpawnIdStore().equal_range(dbGuid); + if (bounds.first == bounds.second) + continue; + + GameObject* go = bounds.first->second; + if (!go) + continue; + + if (go->GetGoState() != GO_STATE_READY) + continue; + + GeneratorInfo info; + info.guid = go->GetGUID(); + info.x = go->GetPositionX(); + info.y = go->GetPositionY(); + info.z = go->GetPositionZ(); + generators.push_back(info); + } + + return generators; + } + + // Returns the nearest active Shield Generator to the bot + // Active generators are powered by NPC_WORLD_INVISIBLE_TRIGGER creatures, + // which depawn after use + Unit* GetNearestActiveShieldGeneratorTriggerByEntry(Unit* reference) + { + if (!reference) + return nullptr; + + std::list triggers; + constexpr float searchRange = 150.0f; + reference->GetCreatureListWithEntryInGrid( + triggers, NPC_WORLD_INVISIBLE_TRIGGER, searchRange); + + Creature* nearest = nullptr; + float minDist = std::numeric_limits::max(); + + for (Creature* creature : triggers) + { + if (!creature->IsAlive()) + continue; + + float dist = reference->GetDistance(creature); + if (dist < minDist) + { + minDist = dist; + nearest = creature; + } + } + + return nearest; + } + + const GeneratorInfo* GetNearestGeneratorToBot( + Player* bot, const std::vector& generators) + { + if (!bot || generators.empty()) + return nullptr; + + const GeneratorInfo* nearest = nullptr; + float minDist = std::numeric_limits::max(); + + for (auto const& gen : generators) + { + float dist = bot->GetExactDist(gen.x, gen.y, gen.z); + if (dist < minDist) + { + minDist = dist; + nearest = &gen; + } + } + + return nearest; + } +} diff --git a/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h new file mode 100644 index 000000000..a725e28fc --- /dev/null +++ b/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h @@ -0,0 +1,189 @@ +#ifndef _PLAYERBOT_RAIDSSCHELPERS_H_ +#define _PLAYERBOT_RAIDSSCHELPERS_H_ + +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace SerpentShrineCavernHelpers +{ + enum SerpentShrineCavernSpells + { + // Trash Mobs + SPELL_TOXIC_POOL = 38718, + + // Hydross the Unstable + SPELL_MARK_OF_HYDROSS_10 = 38215, + SPELL_MARK_OF_HYDROSS_25 = 38216, + SPELL_MARK_OF_HYDROSS_50 = 38217, + SPELL_MARK_OF_HYDROSS_100 = 38218, + SPELL_MARK_OF_HYDROSS_250 = 38231, + SPELL_MARK_OF_HYDROSS_500 = 40584, + SPELL_MARK_OF_CORRUPTION_10 = 38219, + SPELL_MARK_OF_CORRUPTION_25 = 38220, + SPELL_MARK_OF_CORRUPTION_50 = 38221, + SPELL_MARK_OF_CORRUPTION_100 = 38222, + SPELL_MARK_OF_CORRUPTION_250 = 38230, + SPELL_MARK_OF_CORRUPTION_500 = 40583, + SPELL_CORRUPTION = 37961, + + // The Lurker Below + SPELL_SPOUT_VISUAL = 37431, + + // Leotheras the Blind + SPELL_LEOTHERAS_BANISHED = 37546, + SPELL_WHIRLWIND = 37640, + SPELL_WHIRLWIND_CHANNEL = 37641, + SPELL_METAMORPHOSIS = 37673, + SPELL_CHAOS_BLAST = 37675, + SPELL_INSIDIOUS_WHISPER = 37676, + + // Lady Vashj + SPELL_FEAR_WARD = 6346, + SPELL_POISON_BOLT = 38253, + SPELL_STATIC_CHARGE = 38280, + SPELL_ENTANGLE = 38316, + + // Druid + SPELL_CAT_FORM = 768, + SPELL_BEAR_FORM = 5487, + SPELL_DIRE_BEAR_FORM = 9634, + SPELL_TREE_OF_LIFE = 33891, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Mage + SPELL_SLOW = 31589, + + // Shaman + SPELL_GROUNDING_TOTEM_EFFECT = 8178, + + // Warlock + SPELL_CURSE_OF_EXHAUSTION = 18223, + + // Item + SPELL_HEAVY_NETHERWEAVE_NET = 31368, + }; + + enum SerpentShrineCavernNPCs + { + // Trash Mobs + NPC_WATER_ELEMENTAL_TOTEM = 22236, + + // Hydross the Unstable + NPC_PURE_SPAWN_OF_HYDROSS = 22035, + NPC_TAINTED_SPAWN_OF_HYDROSS = 22036, + + // The Lurker Below + NPC_COILFANG_GUARDIAN = 21873, + + // Leotheras the Blind + NPC_LEOTHERAS_THE_BLIND = 21215, + NPC_GREYHEART_SPELLBINDER = 21806, + NPC_INNER_DEMON = 21857, + NPC_SHADOW_OF_LEOTHERAS = 21875, + + // Fathom-Lord Karathress + NPC_SPITFIRE_TOTEM = 22091, + + // Lady Vashj + NPC_WORLD_INVISIBLE_TRIGGER = 12999, + NPC_LADY_VASHJ = 21212, + NPC_ENCHANTED_ELEMENTAL = 21958, + NPC_TAINTED_ELEMENTAL = 22009, + NPC_COILFANG_ELITE = 22055, + NPC_COILFANG_STRIDER = 22056, + NPC_TOXIC_SPOREBAT = 22140, + NPC_SPORE_DROP_TRIGGER = 22207, + }; + + enum SerpentShrineCavernItems + { + // Lady Vashj + ITEM_TAINTED_CORE = 31088, + + // Tailoring + ITEM_HEAVY_NETHERWEAVE_NET = 24269, + }; + + constexpr uint32 SSC_MAP_ID = 548; + + // Hydross the Unstable + extern const Position HYDROSS_FROST_TANK_POSITION; + extern const Position HYDROSS_NATURE_TANK_POSITION; + extern std::unordered_map hydrossFrostDpsWaitTimer; + extern std::unordered_map hydrossNatureDpsWaitTimer; + extern std::unordered_map hydrossChangeToFrostPhaseTimer; + extern std::unordered_map hydrossChangeToNaturePhaseTimer; + bool HasMarkOfHydrossAt100Percent(Player* bot); + bool HasNoMarkOfHydross(Player* bot); + bool HasMarkOfCorruptionAt100Percent(Player* bot); + bool HasNoMarkOfCorruption(Player* bot); + + // The Lurker Below + extern const Position LURKER_MAIN_TANK_POSITION; + extern std::unordered_map lurkerSpoutTimer; + extern std::unordered_map lurkerRangedPositions; + bool IsLurkerCastingSpout(Unit* lurker); + + // Leotheras the Blind + extern std::unordered_map leotherasHumanFormDpsWaitTimer; + extern std::unordered_map leotherasDemonFormDpsWaitTimer; + extern std::unordered_map leotherasFinalPhaseDpsWaitTimer; + Unit* GetLeotherasHuman(PlayerbotAI* botAI); + Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI); + Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI); + Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI); + Player* GetLeotherasDemonFormTank(Player* bot); + + // Fathom-Lord Karathress + extern const Position KARATHRESS_TANK_POSITION; + extern const Position TIDALVESS_TANK_POSITION; + extern const Position SHARKKIS_TANK_POSITION; + extern const Position CARIBDIS_TANK_POSITION; + extern const Position CARIBDIS_HEALER_POSITION; + extern const Position CARIBDIS_RANGED_DPS_POSITION; + extern std::unordered_map karathressDpsWaitTimer; + + // Morogrim Tidewalker + extern const Position TIDEWALKER_PHASE_1_TANK_POSITION; + extern const Position TIDEWALKER_PHASE_TRANSITION_WAYPOINT; + extern const Position TIDEWALKER_PHASE_2_TANK_POSITION; + extern const Position TIDEWALKER_PHASE_2_RANGED_POSITION; + extern std::unordered_map tidewalkerTankStep; + extern std::unordered_map tidewalkerRangedStep; + + // Lady Vashj + constexpr float VASHJ_PLATFORM_Z = 42.985f; + extern const Position VASHJ_PLATFORM_CENTER_POSITION; + extern std::unordered_map vashjRangedPositions; + extern std::unordered_map hasReachedVashjRangedPosition; + extern std::unordered_map nearestTriggerGuid; + extern std::unordered_map intendedLineup; + extern std::unordered_map lastImbueAttempt; + extern std::unordered_map lastCoreInInventoryTime; + bool IsMainTankInSameSubgroup(Player* bot); + bool IsLadyVashjInPhase1(PlayerbotAI* botAI); + bool IsLadyVashjInPhase2(PlayerbotAI* botAI); + bool IsLadyVashjInPhase3(PlayerbotAI* botAI); + bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI); + bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds = 3); + Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI); + Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI); + struct GeneratorInfo { ObjectGuid guid; float x, y, z; }; + extern const std::vector SHIELD_GENERATOR_DB_GUIDS; + std::vector GetAllGeneratorInfosByDbGuids( + Map* map, const std::vector& generatorDbGuids); + Unit* GetNearestActiveShieldGeneratorTriggerByEntry(Unit* reference); + const GeneratorInfo* GetNearestGeneratorToBot( + Player* bot, const std::vector& generators); +} + +#endif diff --git a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp index b20425d22..f7eca38db 100644 --- a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp +++ b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp @@ -11,7 +11,6 @@ #include "GameObject.h" #include "Group.h" #include "LastMovementValue.h" -#include "ObjectDefines.h" #include "ObjectGuid.h" #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" @@ -19,11 +18,9 @@ #include "Position.h" #include "RaidUlduarBossHelper.h" #include "RaidUlduarScripts.h" -#include "RaidUlduarStrategy.h" #include "RtiValue.h" #include "ScriptedCreature.h" #include "ServerFacade.h" -#include "SharedDefines.h" #include "Unit.h" #include "Vehicle.h" #include diff --git a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp b/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp deleted file mode 100644 index 0a51ca407..000000000 --- a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "RaidUlduarMultipliers.h" - -#include "ChooseTargetActions.h" -#include "DKActions.h" -#include "DruidActions.h" -#include "DruidBearActions.h" -#include "FollowActions.h" -#include "GenericActions.h" -#include "GenericSpellActions.h" -#include "HunterActions.h" -#include "MageActions.h" -#include "MovementActions.h" -#include "PaladinActions.h" -#include "PriestActions.h" -#include "RaidUlduarActions.h" -#include "ReachTargetActions.h" -#include "RogueActions.h" -#include "ScriptedCreature.h" -#include "ShamanActions.h" -#include "UseMeetingStoneAction.h" -#include "WarriorActions.h" - -float FlameLeviathanMultiplier::GetValue(Action* action) -{ - // if (dynamic_cast(action)) - // return 0.0f; - return 1.0f; -} diff --git a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h b/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h deleted file mode 100644 index 6c9a468fc..000000000 --- a/src/Ai/Raid/Ulduar/Multiplier/RaidUlduarMultipliers.h +++ /dev/null @@ -1,17 +0,0 @@ - -#ifndef _PLAYERRBOT_RAIDULDUARMULTIPLIERS_H_ -#define _PLAYERRBOT_RAIDULDUARMULTIPLIERS_H_ - -#include "Multiplier.h" -#include "Ai/Raid/Ulduar/RaidUlduarBossHelper.h" - -class FlameLeviathanMultiplier : public Multiplier -{ -public: - FlameLeviathanMultiplier(PlayerbotAI* ai) : Multiplier(ai, "flame leviathan") {} - -public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h b/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h deleted file mode 100644 index 592fbc80e..000000000 --- a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.h +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H -#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H - -#include -#include -#include -#include -#include -#include - -#include "AiObject.h" -#include "AiObjectContext.h" -#include "EventMap.h" -#include "Log.h" -#include "ObjectGuid.h" -#include "Player.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "ScriptedCreature.h" -#include "SharedDefines.h" - -const uint32 ULDUAR_MAP_ID = 603; - -class RazorscaleBossHelper : public AiObject -{ -public: - // Enums and constants specific to Razorscale - enum RazorscaleUnits : uint32 - { - UNIT_RAZORSCALE = 33186, - UNIT_DARK_RUNE_SENTINEL = 33846, - UNIT_DARK_RUNE_WATCHER = 33453, - UNIT_DARK_RUNE_GUARDIAN = 33388, - UNIT_DEVOURING_FLAME = 34188, - }; - - enum RazorscaleGameObjects : uint32 - { - GO_RAZORSCALE_HARPOON_1 = 194519, - GO_RAZORSCALE_HARPOON_2 = 194541, - GO_RAZORSCALE_HARPOON_3 = 194542, - GO_RAZORSCALE_HARPOON_4 = 194543, - }; - - enum RazorscaleSpells : uint32 - { - SPELL_CHAIN_1 = 49679, - SPELL_CHAIN_2 = 49682, - SPELL_CHAIN_3 = 49683, - SPELL_CHAIN_4 = 49684, - SPELL_SENTINEL_WHIRLWIND = 63806, - SPELL_STUN_AURA = 62794, - SPELL_FUSEARMOR = 64771 - }; - - static constexpr uint32 FUSEARMOR_THRESHOLD = 2; - - // Constants for arena parameters - static constexpr float RAZORSCALE_FLYING_Z_THRESHOLD = 440.0f; - static constexpr float RAZORSCALE_ARENA_CENTER_X = 587.54f; - static constexpr float RAZORSCALE_ARENA_CENTER_Y = -175.04f; - static constexpr float RAZORSCALE_ARENA_RADIUS = 30.0f; - - // Harpoon cooldown (seconds) - static constexpr time_t HARPOON_COOLDOWN_DURATION = 5; - - // Structure for harpoon data - struct HarpoonData - { - uint32 gameObjectEntry; - uint32 chainSpellId; - }; - - explicit RazorscaleBossHelper(PlayerbotAI* botAI) - : AiObject(botAI), _boss(nullptr) {} - - bool UpdateBossAI(); - Unit* GetBoss() const; - - bool IsGroundPhase() const; - bool IsFlyingPhase() const; - - bool IsHarpoonFired(uint32 chainSpellId) const; - static bool IsHarpoonReady(GameObject* harpoonGO); - static void SetHarpoonOnCooldown(GameObject* harpoonGO); - GameObject* FindNearestHarpoon(float x, float y, float z) const; - - static const std::vector& GetHarpoonData(); - - void AssignRolesBasedOnHealth(); - bool AreRolesAssigned() const; - bool CanSwapRoles() const; - -private: - Unit* _boss; - - // A map to track the last role swap *per bot* by their GUID - static std::unordered_map _lastRoleSwapTime; - - // The cooldown that applies to every bot - static const std::time_t _roleSwapCooldown = 10; - - static std::unordered_map _harpoonCooldowns; -}; - -// template -// class GenericBossHelper : public AiObject -// { -// public: -// GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} -// virtual bool UpdateBossAI() -// { -// if (!bot->IsInCombat()) -// { -// _unit = nullptr; -// } -// if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) -// { -// _unit = nullptr; -// } -// if (!_unit) -// { -// _unit = AI_VALUE2(Unit*, "find target", _name); -// if (!_unit) -// { -// return false; -// } -// _target = _unit->ToCreature(); -// if (!_target) -// { -// return false; -// } -// _ai = dynamic_cast(_target->GetAI()); -// if (!_ai) -// { -// return false; -// } -// _event_map = &_ai->events; -// if (!_event_map) -// { -// return false; -// } -// } -// if (!_event_map) -// { -// return false; -// } -// _timer = _event_map->GetTimer(); -// return true; -// } -// virtual void Reset() -// { -// _unit = nullptr; -// _target = nullptr; -// _ai = nullptr; -// _event_map = nullptr; -// _timer = 0; -// } - -// protected: -// std::string _name; -// Unit* _unit = nullptr; -// Creature* _target = nullptr; -// BossAiType* _ai = nullptr; -// EventMap* _event_map = nullptr; -// uint32 _timer = 0; -// }; - -#endif diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp index 3b9a426cc..0a1b76a40 100644 --- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp +++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp @@ -1,7 +1,5 @@ #include "RaidUlduarStrategy.h" -#include "RaidUlduarMultipliers.h" - void RaidUlduarStrategy::InitTriggers(std::vector& triggers) { // @@ -316,8 +314,3 @@ void RaidUlduarStrategy::InitTriggers(std::vector& triggers) "yogg-saron phase 3 positioning trigger", { NextAction("yogg-saron phase 3 positioning action", ACTION_RAID) })); } - -void RaidUlduarStrategy::InitMultipliers(std::vector& multipliers) -{ - multipliers.push_back(new FlameLeviathanMultiplier(botAI)); -} diff --git a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h index 81bb93c3a..bb2feefe4 100644 --- a/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h +++ b/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h @@ -3,16 +3,14 @@ #define _PLAYERBOT_RAIDULDUARSTRATEGY_H #include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidUlduarStrategy : public Strategy { public: RaidUlduarStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "uld"; } + virtual std::string const getName() override { return "ulduar"; } virtual void InitTriggers(std::vector& triggers) override; - virtual void InitMultipliers(std::vector& multipliers) override; }; #endif diff --git a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp index a4bc2cf9a..f14a51311 100644 --- a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp +++ b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp @@ -1634,7 +1634,7 @@ bool VezaxShadowCrashTrigger::IsActive() return false; } - return botAI->HasAura(SPELL_SHADOW_CRASH, bot); + return botAI->HasAura(SPELL_VEZAX_SHADOW_CRASH, bot); } bool VezaxMarkOfTheFacelessTrigger::IsActive() diff --git a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h index 129c3d4db..7f8cb51a8 100644 --- a/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h +++ b/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h @@ -3,187 +3,9 @@ #include "EventMap.h" #include "GenericTriggers.h" -#include "PlayerbotAIConfig.h" #include "RaidUlduarBossHelper.h" #include "Trigger.h" -enum UlduarIDs -{ - // Iron Assembly - SPELL_LIGHTNING_TENDRILS_10_MAN = 61887, - SPELL_LIGHTNING_TENDRILS_25_MAN = 63486, - SPELL_OVERLOAD_10_MAN = 61869, - SPELL_OVERLOAD_25_MAN = 63481, - SPELL_OVERLOAD_10_MAN_2 = 63485, - SPELL_OVERLOAD_25_MAN_2 = 61886, - SPELL_RUNE_OF_POWER = 64320, - - // Kologarn - NPC_RIGHT_ARM = 32934, - NPC_RUBBLE = 33768, - SPELL_CRUNCH_ARMOR = 64002, - - SPELL_FOCUSED_EYEBEAM_10_2 = 63346, - SPELL_FOCUSED_EYEBEAM_10 = 63347, - SPELL_FOCUSED_EYEBEAM_25_2 = 63976, - SPELL_FOCUSED_EYEBEAM_25 = 63977, - - // Hodir - NPC_SNOWPACKED_ICICLE = 33174, - NPC_TOASTY_FIRE = 33342, - SPELL_FLASH_FREEZE = 61968, - SPELL_BITING_COLD_PLAYER_AURA = 62039, - - // Freya - NPC_SNAPLASHER = 32916, - NPC_STORM_LASHER = 32919, - NPC_DETONATING_LASHER = 32918, - NPC_ANCIENT_WATER_SPIRIT = 33202, - NPC_ANCIENT_CONSERVATOR = 33203, - NPC_HEALTHY_SPORE = 33215, - NPC_EONARS_GIFT = 33228, - GOBJECT_NATURE_BOMB = 194902, - - // Thorim - NPC_DARK_RUNE_ACOLYTE_I = 32886, - NPC_CAPTURED_MERCENARY_SOLDIER_ALLY = 32885, - NPC_CAPTURED_MERCENARY_SOLDIER_HORDE = 32883, - NPC_CAPTURED_MERCENARY_CAPTAIN_ALLY = 32908, - NPC_CAPTURED_MERCENARY_CAPTAIN_HORDE = 32907, - NPC_JORMUNGAR_BEHEMOT = 32882, - NPC_DARK_RUNE_WARBRINGER = 32877, - NPC_DARK_RUNE_EVOKER = 32878, - NPC_DARK_RUNE_CHAMPION = 32876, - NPC_DARK_RUNE_COMMONER = 32904, - NPC_IRON_RING_GUARD = 32874, - NPC_RUNIC_COLOSSUS = 32872, - NPC_ANCIENT_RUNE_GIANT = 32873, - NPC_DARK_RUNE_ACOLYTE_G = 33110, - NPC_IRON_HONOR_GUARD = 32875, - SPELL_UNBALANCING_STRIKE = 62130, - - // Mimiron - NPC_LEVIATHAN_MKII = 33432, - NPC_VX001 = 33651, - NPC_AERIAL_COMMAND_UNIT = 33670, - NPC_BOMB_BOT = 33836, - NPC_ROCKET_STRIKE_N = 34047, - NPC_ASSAULT_BOT = 34057, - NPC_PROXIMITY_MINE = 34362, - SPELL_P3WX2_LASER_BARRAGE_1 = 63293, - SPELL_P3WX2_LASER_BARRAGE_2 = 63297, - SPELL_SPINNING_UP = 63414, - SPELL_SHOCK_BLAST = 63631, - SPELL_P3WX2_LASER_BARRAGE_3 = 64042, - SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, - SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, - - // General Vezax - SPELL_MARK_OF_THE_FACELESS = 63276, - SPELL_SHADOW_CRASH = 63277, - - // Yogg-Saron - ACTION_ILLUSION_DRAGONS = 1, - ACTION_ILLUSION_ICECROWN = 2, - ACTION_ILLUSION_STORMWIND = 3, - NPC_GUARDIAN_OF_YS = 33136, - NPC_YOGG_SARON = 33288, - NPC_OMINOUS_CLOUD = 33292, - NPC_RUBY_CONSORT = 33716, - NPC_AZURE_CONSORT = 33717, - NPC_BRONZE_CONSORT = 33718, - NPC_EMERALD_CONSORT = 33719, - NPC_OBSIDIAN_CONSORT = 33720, - NPC_ALEXTRASZA = 33536, - NPC_MALYGOS_ILLUSION = 33535, - NPC_NELTHARION = 33523, - NPC_YSERA = 33495, - GO_DRAGON_SOUL = 194462, - NPC_SARA_PHASE_1 = 33134, - NPC_LICH_KING_ILLUSION = 33441, - NPC_IMMOLATED_CHAMPION = 33442, - NPC_SUIT_OF_ARMOR = 33433, - NPC_GARONA = 33436, - NPC_KING_LLANE = 33437, - NPC_DEATHSWORN_ZEALOT = 33567, - NPC_INFLUENCE_TENTACLE = 33943, - NPC_DEATH_ORB = 33882, - NPC_BRAIN = 33890, - NPC_CRUSHER_TENTACLE = 33966, - NPC_CONSTRICTOR_TENTACLE = 33983, - NPC_CORRUPTOR_TENTACLE = 33985, - NPC_IMMORTAL_GUARDIAN = 33988, - NPC_LAUGHING_SKULL = 33990, - NPC_SANITY_WELL = 33991, - NPC_DESCEND_INTO_MADNESS = 34072, - NPC_MARKED_IMMORTAL_GUARDIAN = 36064, - SPELL_SANITY = 63050, - SPELL_BRAIN_LINK = 63802, - SPELL_MALADY_OF_THE_MIND = 63830, - SPELL_SHADOW_BARRIER = 63894, - SPELL_TELEPORT_TO_CHAMBER = 63997, - SPELL_TELEPORT_TO_ICECROWN = 63998, - SPELL_TELEPORT_TO_STORMWIND = 63989, - SPELL_TELEPORT_BACK = 63992, - SPELL_CANCEL_ILLUSION_AURA = 63993, - SPELL_INDUCE_MADNESS = 64059, - SPELL_LUNATIC_GAZE_YS = 64163, - GO_FLEE_TO_THE_SURFACE_PORTAL = 194625, - - // Buffs - SPELL_FROST_TRAP = 13809 -}; - -const float ULDUAR_KOLOGARN_AXIS_Z_PATHING_ISSUE_DETECT = 420.0f; -const float ULDUAR_KOLOGARN_EYEBEAM_RADIUS = 3.0f; -const float ULDUAR_THORIM_AXIS_Z_FLOOR_THRESHOLD = 429.6094f; -const float ULDUAR_THORIM_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; -const float ULDUAR_AURIAYA_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; -const float ULDUAR_YOGG_SARON_BOSS_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 300.0f; -const float ULDUAR_YOGG_SARON_BRAIN_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 200.0f; -const float ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS = 150.0f; -const float ULDUAR_YOGG_SARON_BRAIN_ROOM_RADIUS = 50.0f; - -const Position ULDUAR_THORIM_NEAR_ARENA_CENTER = Position(2134.9854f, -263.11853f, 419.8465f); -const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION = Position(2172.4355f, -258.27957f, 418.47162f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1 = Position(2237.6187f, -265.08844f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2 = Position(2237.2498f, -275.81122f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1 = Position(2236.895f, -294.62448f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1 = Position(2242.1162f, -310.15308f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2 = Position(2242.018f, -318.66003f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3 = Position(2242.1904f, -329.0533f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1 = Position(2219.5417f, -264.77167f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2 = Position(2217.446f, -275.85248f, 412.17548f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f, -295.01193f, 412.13434f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); -const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); -const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); -const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); -const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); -const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); -const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT = Position(2156.798f, -267.57434f, 419.52722f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT = Position(2753.708f, 2583.9617f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT = Position(2746.9792f, 2573.6716f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT = Position(2727.7224f, 2569.527f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569.4106f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); -const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); -const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); -const Position ULDUAR_YOGG_SARON_MIDDLE = Position(1980.28f, -25.5868f, 329.397f); -const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE = Position(1927.1511f, 68.507256f, 242.37657f); -const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE = Position(1925.6553f, -121.59296f, 239.98965f); -const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE = Position(2104.5667f, -25.509348f, 242.64679f); -const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE = Position(1980.1971f, -27.854689f, 236.06789f); -const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE = Position(1954.06f, 21.66f, 239.71f); -const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE = Position(1950.11f, -79.284f, 239.98982f); -const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE = Position(2048.63f, -25.5f, 239.72f); -const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT = Position(1998.5377f, -22.90317f, 324.8895f); -const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT = Position(2018.7628f, -18.896868f, 327.07245f); - // // Flame Levi // diff --git a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp similarity index 63% rename from src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp rename to src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp index 72333a079..fd6711cf0 100644 --- a/src/Ai/Raid/Ulduar/RaidUlduarBossHelper.cpp +++ b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp @@ -1,4 +1,3 @@ -#include "ChatHelper.h" #include "RaidUlduarBossHelper.h" #include "ObjectAccessor.h" #include "GameObject.h" @@ -9,6 +8,44 @@ #include "Playerbots.h" #include "World.h" +const Position ULDUAR_THORIM_NEAR_ARENA_CENTER = Position(2134.9854f, -263.11853f, 419.8465f); +const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION = Position(2172.4355f, -258.27957f, 418.47162f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1 = Position(2237.6187f, -265.08844f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2 = Position(2237.2498f, -275.81122f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1 = Position(2236.895f, -294.62448f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1 = Position(2242.1162f, -310.15308f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2 = Position(2242.018f, -318.66003f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3 = Position(2242.1904f, -329.0533f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1 = Position(2219.5417f, -264.77167f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2 = Position(2217.446f, -275.85248f, 412.17548f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1 = Position(2217.8877f, -295.01193f, 412.13434f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1 = Position(2212.193f, -307.44992f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2 = Position(2212.1353f, -318.20795f, 412.1348f); +const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3 = Position(2212.1956f, -328.0144f, 412.1348f); +const Position ULDUAR_THORIM_JUMP_END_POINT = Position(2137.8818f, -278.18942f, 419.66653f); +const Position ULDUAR_THORIM_PHASE2_TANK_SPOT = Position(2134.8572f, -287.0291f, 419.4935f); +const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT = Position(2112.8752f, -267.69305f, 419.52814f); +const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT = Position(2134.1296f, -257.3316f, 419.8462f); +const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT = Position(2156.798f, -267.57434f, 419.52722f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT = Position(2753.708f, 2583.9617f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT = Position(2746.9792f, 2573.6716f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT = Position(2727.7224f, 2569.527f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT = Position(2739.4746f, 2569.4106f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT = Position(2754.1294f, 2553.9954f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT = Position(2746.8513f, 2565.4263f, 364.31357f); +const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT = Position(2744.5754f, 2570.8657f, 364.3138f); +const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT = Position(1913.6501f, 122.93989f, 342.38083f); +const Position ULDUAR_YOGG_SARON_MIDDLE = Position(1980.28f, -25.5868f, 329.397f); +const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE = Position(1927.1511f, 68.507256f, 242.37657f); +const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE = Position(1925.6553f, -121.59296f, 239.98965f); +const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE = Position(2104.5667f, -25.509348f, 242.64679f); +const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE = Position(1980.1971f, -27.854689f, 236.06789f); +const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE = Position(1954.06f, 21.66f, 239.71f); +const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE = Position(1950.11f, -79.284f, 239.98982f); +const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE = Position(2048.63f, -25.5f, 239.72f); +const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT = Position(1998.5377f, -22.90317f, 324.8895f); +const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT = Position(2018.7628f, -18.896868f, 327.07245f); + // Prevent harpoon spam std::unordered_map RazorscaleBossHelper::_harpoonCooldowns; // Prevent role assignment spam diff --git a/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h new file mode 100644 index 000000000..b3d49ff58 --- /dev/null +++ b/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h @@ -0,0 +1,341 @@ +#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H +#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H + +#include +#include +#include + +#include "AiObject.h" +#include "AiObjectContext.h" +#include "EventMap.h" +#include "ObjectGuid.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "ScriptedCreature.h" + +constexpr uint32 ULDUAR_MAP_ID = 603; + +enum UlduarIDs +{ + // Iron Assembly + SPELL_LIGHTNING_TENDRILS_10_MAN = 61887, + SPELL_LIGHTNING_TENDRILS_25_MAN = 63486, + SPELL_OVERLOAD_10_MAN = 61869, + SPELL_OVERLOAD_25_MAN = 63481, + SPELL_OVERLOAD_10_MAN_2 = 63485, + SPELL_OVERLOAD_25_MAN_2 = 61886, + SPELL_RUNE_OF_POWER = 64320, + + // Kologarn + NPC_RIGHT_ARM = 32934, + NPC_RUBBLE = 33768, + SPELL_CRUNCH_ARMOR = 64002, + + SPELL_FOCUSED_EYEBEAM_10_2 = 63346, + SPELL_FOCUSED_EYEBEAM_10 = 63347, + SPELL_FOCUSED_EYEBEAM_25_2 = 63976, + SPELL_FOCUSED_EYEBEAM_25 = 63977, + + // Hodir + NPC_SNOWPACKED_ICICLE = 33174, + NPC_TOASTY_FIRE = 33342, + SPELL_FLASH_FREEZE = 61968, + SPELL_BITING_COLD_PLAYER_AURA = 62039, + + // Freya + NPC_SNAPLASHER = 32916, + NPC_STORM_LASHER = 32919, + NPC_DETONATING_LASHER = 32918, + NPC_ANCIENT_WATER_SPIRIT = 33202, + NPC_ANCIENT_CONSERVATOR = 33203, + NPC_HEALTHY_SPORE = 33215, + NPC_EONARS_GIFT = 33228, + GOBJECT_NATURE_BOMB = 194902, + + // Thorim + NPC_DARK_RUNE_ACOLYTE_I = 32886, + NPC_CAPTURED_MERCENARY_SOLDIER_ALLY = 32885, + NPC_CAPTURED_MERCENARY_SOLDIER_HORDE = 32883, + NPC_CAPTURED_MERCENARY_CAPTAIN_ALLY = 32908, + NPC_CAPTURED_MERCENARY_CAPTAIN_HORDE = 32907, + NPC_JORMUNGAR_BEHEMOT = 32882, + NPC_DARK_RUNE_WARBRINGER = 32877, + NPC_DARK_RUNE_EVOKER = 32878, + NPC_DARK_RUNE_CHAMPION = 32876, + NPC_DARK_RUNE_COMMONER = 32904, + NPC_IRON_RING_GUARD = 32874, + NPC_RUNIC_COLOSSUS = 32872, + NPC_ANCIENT_RUNE_GIANT = 32873, + NPC_DARK_RUNE_ACOLYTE_G = 33110, + NPC_IRON_HONOR_GUARD = 32875, + SPELL_UNBALANCING_STRIKE = 62130, + + // Mimiron + NPC_LEVIATHAN_MKII = 33432, + NPC_VX001 = 33651, + NPC_AERIAL_COMMAND_UNIT = 33670, + NPC_BOMB_BOT = 33836, + NPC_ROCKET_STRIKE_N = 34047, + NPC_ASSAULT_BOT = 34057, + NPC_PROXIMITY_MINE = 34362, + SPELL_P3WX2_LASER_BARRAGE_1 = 63293, + SPELL_P3WX2_LASER_BARRAGE_2 = 63297, + SPELL_SPINNING_UP = 63414, + SPELL_SHOCK_BLAST = 63631, + SPELL_P3WX2_LASER_BARRAGE_3 = 64042, + SPELL_P3WX2_LASER_BARRAGE_AURA_1 = 63274, + SPELL_P3WX2_LASER_BARRAGE_AURA_2 = 63300, + + // General Vezax + SPELL_MARK_OF_THE_FACELESS = 63276, + SPELL_VEZAX_SHADOW_CRASH = 63277, + + // Yogg-Saron + ACTION_ILLUSION_DRAGONS = 1, + ACTION_ILLUSION_ICECROWN = 2, + ACTION_ILLUSION_STORMWIND = 3, + NPC_GUARDIAN_OF_YS = 33136, + NPC_YOGG_SARON = 33288, + NPC_OMINOUS_CLOUD = 33292, + NPC_RUBY_CONSORT = 33716, + NPC_AZURE_CONSORT = 33717, + NPC_BRONZE_CONSORT = 33718, + NPC_EMERALD_CONSORT = 33719, + NPC_OBSIDIAN_CONSORT = 33720, + NPC_ALEXTRASZA = 33536, + NPC_MALYGOS_ILLUSION = 33535, + NPC_NELTHARION = 33523, + NPC_YSERA = 33495, + GO_DRAGON_SOUL = 194462, + NPC_SARA_PHASE_1 = 33134, + NPC_LICH_KING_ILLUSION = 33441, + NPC_IMMOLATED_CHAMPION = 33442, + NPC_SUIT_OF_ARMOR = 33433, + NPC_GARONA = 33436, + NPC_KING_LLANE = 33437, + NPC_DEATHSWORN_ZEALOT = 33567, + NPC_INFLUENCE_TENTACLE = 33943, + NPC_DEATH_ORB = 33882, + NPC_BRAIN = 33890, + NPC_CRUSHER_TENTACLE = 33966, + NPC_CONSTRICTOR_TENTACLE = 33983, + NPC_CORRUPTOR_TENTACLE = 33985, + NPC_IMMORTAL_GUARDIAN = 33988, + NPC_LAUGHING_SKULL = 33990, + NPC_SANITY_WELL = 33991, + NPC_DESCEND_INTO_MADNESS = 34072, + NPC_MARKED_IMMORTAL_GUARDIAN = 36064, + SPELL_SANITY = 63050, + SPELL_BRAIN_LINK = 63802, + SPELL_MALADY_OF_THE_MIND = 63830, + SPELL_SHADOW_BARRIER = 63894, + SPELL_TELEPORT_TO_CHAMBER = 63997, + SPELL_TELEPORT_TO_ICECROWN = 63998, + SPELL_TELEPORT_TO_STORMWIND = 63989, + SPELL_TELEPORT_BACK = 63992, + SPELL_CANCEL_ILLUSION_AURA = 63993, + SPELL_INDUCE_MADNESS = 64059, + SPELL_LUNATIC_GAZE_YS = 64163, + GO_FLEE_TO_THE_SURFACE_PORTAL = 194625, + + // Buffs + SPELL_FROST_TRAP = 13809 +}; + +constexpr float ULDUAR_KOLOGARN_AXIS_Z_PATHING_ISSUE_DETECT = 420.0f; +constexpr float ULDUAR_KOLOGARN_EYEBEAM_RADIUS = 3.0f; +constexpr float ULDUAR_THORIM_AXIS_Z_FLOOR_THRESHOLD = 429.6094f; +constexpr float ULDUAR_THORIM_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; +constexpr float ULDUAR_AURIAYA_AXIS_Z_PATHING_ISSUE_DETECT = 410.0f; +constexpr float ULDUAR_YOGG_SARON_BOSS_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 300.0f; +constexpr float ULDUAR_YOGG_SARON_BRAIN_ROOM_AXIS_Z_PATHING_ISSUE_DETECT = 200.0f; +constexpr float ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS = 150.0f; +constexpr float ULDUAR_YOGG_SARON_BRAIN_ROOM_RADIUS = 50.0f; + +extern const Position ULDUAR_THORIM_NEAR_ARENA_CENTER; +extern const Position ULDUAR_THORIM_NEAR_ENTRANCE_POSITION; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_6_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_5_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_LEFT_SIDE_10_YARDS_3; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_6_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_5_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_1; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_2; +extern const Position ULDUAR_THORIM_GAUNTLET_RIGHT_SIDE_10_YARDS_3; +extern const Position ULDUAR_THORIM_JUMP_END_POINT; +extern const Position ULDUAR_THORIM_PHASE2_TANK_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE1_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE2_SPOT; +extern const Position ULDUAR_THORIM_PHASE2_RANGE3_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE1RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE1MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE2RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE2MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE3RANGE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE2_SIDE3MELEE_SPOT; +extern const Position ULDUAR_MIMIRON_PHASE4_TANK_SPOT; +extern const Position ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT; +extern const Position ULDUAR_YOGG_SARON_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE; +extern const Position ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE; +extern const Position ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT; +extern const Position ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT; + +class RazorscaleBossHelper : public AiObject +{ +public: + // Enums and constants specific to Razorscale + enum RazorscaleUnits : uint32 + { + UNIT_RAZORSCALE = 33186, + UNIT_DARK_RUNE_SENTINEL = 33846, + UNIT_DARK_RUNE_WATCHER = 33453, + UNIT_DARK_RUNE_GUARDIAN = 33388, + UNIT_DEVOURING_FLAME = 34188, + }; + + enum RazorscaleGameObjects : uint32 + { + GO_RAZORSCALE_HARPOON_1 = 194519, + GO_RAZORSCALE_HARPOON_2 = 194541, + GO_RAZORSCALE_HARPOON_3 = 194542, + GO_RAZORSCALE_HARPOON_4 = 194543, + }; + + enum RazorscaleSpells : uint32 + { + SPELL_CHAIN_1 = 49679, + SPELL_CHAIN_2 = 49682, + SPELL_CHAIN_3 = 49683, + SPELL_CHAIN_4 = 49684, + SPELL_SENTINEL_WHIRLWIND = 63806, + SPELL_STUN_AURA = 62794, + SPELL_FUSEARMOR = 64771 + }; + + static constexpr uint32 FUSEARMOR_THRESHOLD = 2; + + // Constants for arena parameters + static constexpr float RAZORSCALE_FLYING_Z_THRESHOLD = 440.0f; + static constexpr float RAZORSCALE_ARENA_CENTER_X = 587.54f; + static constexpr float RAZORSCALE_ARENA_CENTER_Y = -175.04f; + static constexpr float RAZORSCALE_ARENA_RADIUS = 30.0f; + + // Harpoon cooldown (seconds) + static constexpr time_t HARPOON_COOLDOWN_DURATION = 5; + + // Structure for harpoon data + struct HarpoonData + { + uint32 gameObjectEntry; + uint32 chainSpellId; + }; + + explicit RazorscaleBossHelper(PlayerbotAI* botAI) + : AiObject(botAI), _boss(nullptr) {} + + bool UpdateBossAI(); + Unit* GetBoss() const; + + bool IsGroundPhase() const; + bool IsFlyingPhase() const; + + bool IsHarpoonFired(uint32 chainSpellId) const; + static bool IsHarpoonReady(GameObject* harpoonGO); + static void SetHarpoonOnCooldown(GameObject* harpoonGO); + GameObject* FindNearestHarpoon(float x, float y, float z) const; + + static const std::vector& GetHarpoonData(); + + void AssignRolesBasedOnHealth(); + bool AreRolesAssigned() const; + bool CanSwapRoles() const; + +private: + Unit* _boss; + + // A map to track the last role swap *per bot* by their GUID + static std::unordered_map _lastRoleSwapTime; + + // The cooldown that applies to every bot + static const std::time_t _roleSwapCooldown = 10; + + static std::unordered_map _harpoonCooldowns; +}; + +// template +// class GenericBossHelper : public AiObject +// { +// public: +// GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} +// virtual bool UpdateBossAI() +// { +// if (!bot->IsInCombat()) +// { +// _unit = nullptr; +// } +// if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) +// { +// _unit = nullptr; +// } +// if (!_unit) +// { +// _unit = AI_VALUE2(Unit*, "find target", _name); +// if (!_unit) +// { +// return false; +// } +// _target = _unit->ToCreature(); +// if (!_target) +// { +// return false; +// } +// _ai = dynamic_cast(_target->GetAI()); +// if (!_ai) +// { +// return false; +// } +// _event_map = &_ai->events; +// if (!_event_map) +// { +// return false; +// } +// } +// if (!_event_map) +// { +// return false; +// } +// _timer = _event_map->GetTimer(); +// return true; +// } +// virtual void Reset() +// { +// _unit = nullptr; +// _target = nullptr; +// _ai = nullptr; +// _event_map = nullptr; +// _timer = 0; +// } + +// protected: +// std::string _name; +// Unit* _unit = nullptr; +// Creature* _target = nullptr; +// BossAiType* _ai = nullptr; +// EventMap* _event_map = nullptr; +// uint32 _timer = 0; +// }; + +#endif diff --git a/src/Ai/Raid/Ulduar/RaidUlduarScripts.h b/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h similarity index 100% rename from src/Ai/Raid/Ulduar/RaidUlduarScripts.h rename to src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h index b304826b8..263d03d9b 100644 --- a/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h +++ b/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_RAIDVOAACTIONCONTEXT_H #include "Action.h" +#include "BossAuraActions.h" #include "NamedObjectContext.h" #include "RaidVoAActions.h" #include "PlayerbotAI.h" diff --git a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h index 9fe078f80..6566793fd 100644 --- a/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h +++ b/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H #include "AiObjectContext.h" +#include "BossAuraTriggers.h" #include "NamedObjectContext.h" #include "RaidVoATriggers.h" diff --git a/src/Bot/Engine/AiObjectContext.cpp b/src/Bot/Engine/AiObjectContext.cpp index b6d5f7de0..3a2037a08 100644 --- a/src/Bot/Engine/AiObjectContext.cpp +++ b/src/Bot/Engine/AiObjectContext.cpp @@ -15,8 +15,6 @@ #include "PaladinAiObjectContext.h" #include "Playerbots.h" #include "PriestAiObjectContext.h" -#include "RaidUlduarActionContext.h" -#include "RaidUlduarTriggerContext.h" #include "RogueAiObjectContext.h" #include "ShamanAiObjectContext.h" #include "SharedValueContext.h" @@ -43,12 +41,16 @@ #include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" #include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" #include "Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" #include "Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" +#include "Ai/Raid/Ulduar/RaidUlduarTriggerContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h" #include "Ai/Raid/Icecrown/RaidIccActionContext.h" @@ -115,6 +117,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList PlayerbotAI::GetPlayersInGroup() +std::vector PlayerbotAI::GetRealPlayersInGroup() { std::vector members; @@ -2607,6 +2611,30 @@ std::vector PlayerbotAI::GetPlayersInGroup() return members; } +std::vector PlayerbotAI::GetAllPlayersInGroup() +{ + std::vector members; + + Group* group = bot->GetGroup(); + + if (!group) + return members; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + + if (!member) + { + continue; + } + + members.push_back(ref->GetSource()); + } + + return members; +} + bool PlayerbotAI::SayToGuild(const std::string& msg) { if (msg.empty()) @@ -2715,9 +2743,9 @@ bool PlayerbotAI::SayToParty(const std::string& msg) ChatHandler::BuildChatPacket(data, CHAT_MSG_PARTY, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); - for (auto reciever : GetPlayersInGroup()) + for (auto receiver : GetRealPlayersInGroup()) { - ServerFacade::instance().SendPacket(reciever, &data); + ServerFacade::instance().SendPacket(receiver, &data); } return true; @@ -2732,9 +2760,9 @@ bool PlayerbotAI::SayToRaid(const std::string& msg) ChatHandler::BuildChatPacket(data, CHAT_MSG_RAID, msg.c_str(), LANG_UNIVERSAL, CHAT_TAG_NONE, bot->GetGUID(), bot->GetName()); - for (auto reciever : GetPlayersInGroup()) + for (auto receiver : GetRealPlayersInGroup()) { - ServerFacade::instance().SendPacket(reciever, &data); + ServerFacade::instance().SendPacket(receiver, &data); } return true; diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index c2d4aeb75..3364b31ac 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -446,7 +446,8 @@ public: GameObject* GetGameObject(ObjectGuid guid); // static GameObject* GetGameObject(GameObjectData const* gameObjectData); WorldObject* GetWorldObject(ObjectGuid guid); - std::vector GetPlayersInGroup(); + std::vector GetAllPlayersInGroup(); + std::vector GetRealPlayersInGroup(); const AreaTableEntry* GetCurrentArea(); const AreaTableEntry* GetCurrentZone(); static std::string GetLocalizedAreaName(const AreaTableEntry* entry); diff --git a/src/Mgr/Item/RandomItemMgr.cpp b/src/Mgr/Item/RandomItemMgr.cpp index 5c0e8c94a..1053e542e 100644 --- a/src/Mgr/Item/RandomItemMgr.cpp +++ b/src/Mgr/Item/RandomItemMgr.cpp @@ -2256,10 +2256,13 @@ void RandomItemMgr::BuildEquipCacheNew() { continue; } - if (itemId == 22784) - { // Sunwell Orb + + // Unobtainable or unusable items + if (itemId == 12468 || // Chilton Wand + itemId == 22784 || // Sunwell Orb + itemId == 46978) // Totem of the Earthen Ring continue; - } + equipCacheNew[proto->RequiredLevel][proto->InventoryType].push_back(itemId); } } diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 84eef9b99..748f929aa 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -881,107 +881,6 @@ std::vector WorldPosition::getGameObjectsNear(float radiu return worker.GetResult(); } -Creature* GuidPosition::GetCreature() -{ - if (!*this) - return nullptr; - - if (loadedFromDB) - { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; - - return nullptr; - } - - return getMap()->GetCreature(*this); -} - -Unit* GuidPosition::GetUnit() -{ - if (!*this) - return nullptr; - - if (loadedFromDB) - { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; - - return nullptr; - } - - if (IsPlayer()) - return ObjectAccessor::FindPlayer(*this); - - if (IsPet()) - return getMap()->GetPet(*this); - - return GetCreature(); -} - -GameObject* GuidPosition::GetGameObject() -{ - if (!*this) - return nullptr; - - if (loadedFromDB) - { - auto gameobjectBounds = getMap()->GetGameObjectBySpawnIdStore().equal_range(GetCounter()); - if (gameobjectBounds.first != gameobjectBounds.second) - return gameobjectBounds.second->second; - - return nullptr; - } - - return getMap()->GetGameObject(*this); -} - -Player* GuidPosition::GetPlayer() -{ - if (!*this) - return nullptr; - - if (IsPlayer()) - return ObjectAccessor::FindPlayer(*this); - - return nullptr; -} - -bool GuidPosition::isDead() -{ - if (!getMap()) - return false; - - if (!getMap()->IsGridLoaded(getX(), getY())) - return false; - - if (IsUnit() && GetUnit() && GetUnit()->IsInWorld() && GetUnit()->IsAlive()) - return false; - - if (IsGameObject() && GetGameObject() && GetGameObject()->IsInWorld()) - return false; - - return true; -} - -GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} - -GuidPosition::GuidPosition(CreatureData const& creData) - : ObjectGuid(HighGuid::Unit, creData.id1, creData.spawnId), - WorldPosition(creData.mapid, creData.posX, creData.posY, creData.posZ, creData.orientation) -{ - loadedFromDB = true; -} - -GuidPosition::GuidPosition(GameObjectData const& goData) - : ObjectGuid(HighGuid::GameObject, goData.id), - WorldPosition(goData.mapid, goData.posX, goData.posY, goData.posZ, goData.orientation) -{ - loadedFromDB = true; -} - CreatureTemplate const* GuidPosition::GetCreatureTemplate() { return IsCreature() ? sObjectMgr->GetCreatureTemplate(GetEntry()) : nullptr; @@ -1000,7 +899,7 @@ WorldObject* GuidPosition::GetWorldObject() switch (GetHigh()) { case HighGuid::Player: - return ObjectAccessor::FindPlayer(*this); + return GetPlayer(); case HighGuid::Transport: case HighGuid::Mo_Transport: case HighGuid::GameObject: @@ -1021,8 +920,93 @@ WorldObject* GuidPosition::GetWorldObject() return nullptr; } +GameObject* GuidPosition::GetGameObject() +{ + if (!*this) + return nullptr; + + if (loadedFromDB) + return ObjectAccessor::GetSpawnedGameObjectByDBGUID(GetMapId(), GetCounter()); + + return getMap()->GetGameObject(*this); // fallback +} + +Unit* GuidPosition::GetUnit() +{ + if (!*this) + return nullptr; + + if (IsPlayer()) + return GetPlayer(); + + if (IsPet()) + return getMap()->GetPet(*this); + + return GetCreature(); +} + +Creature* GuidPosition::GetCreature() +{ + if (!*this) + return nullptr; + + if (loadedFromDB) + return ObjectAccessor::GetSpawnedCreatureByDBGUID(GetMapId(), GetCounter()); + + return getMap()->GetCreature(*this); // fallback +} + +Player* GuidPosition::GetPlayer() +{ + if (!*this) + return nullptr; + + if (IsPlayer()) + return ObjectAccessor::FindPlayer(*this); + + return nullptr; +} + bool GuidPosition::HasNpcFlag(NPCFlags flag) { return IsCreature() && GetCreatureTemplate()->npcflag & flag; } +bool GuidPosition::IsCreatureOrGOAccessible() +{ + Map* map = getMap(); + if (!map || !map->IsGridLoaded(GetPositionX(), GetPositionY())) + return false; + + if (IsCreature()) + { + Creature* creature = GetCreature(); + if (creature && creature->IsInWorld() && creature->IsAlive()) + return true; + } + else if (IsGameObject()) + { + GameObject* go = GetGameObject(); + if (go && go->IsInWorld()) + return true; + } + + return false; +} + +GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} + +GuidPosition::GuidPosition(CreatureData const& creData) + : ObjectGuid(HighGuid::Unit, creData.id1, creData.spawnId), + WorldPosition(creData.mapid, creData.posX, creData.posY, creData.posZ, creData.orientation) +{ + loadedFromDB = true; +} + +GuidPosition::GuidPosition(GameObjectData const& goData) + : ObjectGuid(HighGuid::GameObject, goData.id), + WorldPosition(goData.mapid, goData.posX, goData.posY, goData.posZ, goData.orientation) +{ + loadedFromDB = true; +} + std::vector TravelDestination::getPoints(bool ignoreFull) { if (ignoreFull) diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 3223444bf..251313736 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -416,14 +416,13 @@ public: GameObjectTemplate const* GetGameObjectTemplate(); WorldObject* GetWorldObject(); - Creature* GetCreature(); - Unit* GetUnit(); GameObject* GetGameObject(); + Unit* GetUnit(); + Creature* GetCreature(); Player* GetPlayer(); bool HasNpcFlag(NPCFlags flag); - - bool isDead(); // For loaded grids check if the unit/object is unloaded/dead. + bool IsCreatureOrGOAccessible(); // For loaded grids check if the creature/gameobject is in world + alive operator bool() const { return !IsEmpty(); } bool operator==(ObjectGuid const& guid) const { return GetRawValue() == guid.GetRawValue(); } diff --git a/src/Script/WorldThr/PlayerbotOperations.h b/src/Script/WorldThr/PlayerbotOperations.h index a80321d53..ee6443cd8 100644 --- a/src/Script/WorldThr/PlayerbotOperations.h +++ b/src/Script/WorldThr/PlayerbotOperations.h @@ -14,9 +14,11 @@ #include "PlayerbotOperation.h" #include "Player.h" #include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" #include "PlayerbotMgr.h" #include "PlayerbotRepository.h" #include "RandomPlayerbotMgr.h" +#include "UseMeetingStoneAction.h" #include "WorldSession.h" #include "WorldSessionMgr.h" @@ -74,6 +76,15 @@ public: if (group->AddMember(target)) { LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName()); + if (sPlayerbotAIConfig.summonWhenGroup && target->GetDistance(bot) > sPlayerbotAIConfig.sightDistance) + { + PlayerbotAI* targetAI = sPlayerbotsMgr.GetPlayerbotAI(target); + if (targetAI) + { + SummonAction summonAction(targetAI, "group summon"); + summonAction.Teleport(bot, target, true); + } + } return true; } else