diff --git a/.github/workflows/check_pr_source.yml b/.github/workflows/check_pr_source.yml index 877ab85c739..5b354f6f74a 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 7b1688fb6db..83b78c39276 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 2927716f125..ac6ee60d844 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 30a558c6398..ab4d83daa64 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 49678c97aa9..121d97751ff 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 90737731672..a2f06885c17 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 32508ef2e52..a8b262692b0 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 518d39c8ee4..4f84f942bd9 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 @@ class EquipAction : public InventoryAction bool Execute(Event event) override; void EquipItems(ItemIds ids); + ItemIds SelectInventoryItemsToEquip(); private: void EquipItem(FindItemVisitor* visitor); @@ -27,10 +29,10 @@ class EquipAction : public InventoryAction 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 @@ class EquipUpgradesAction : public EquipAction 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 e600f1f2041..9a21c8139fd 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 ff1282284ea..fd2a2c5f2da 100644 --- a/src/Ai/Base/Actions/UseMeetingStoneAction.h +++ b/src/Ai/Base/Actions/UseMeetingStoneAction.h @@ -17,9 +17,9 @@ class SummonAction : public MovementAction 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 5e215e1237e..8e4ed9ccf84 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -120,7 +120,7 @@ class ChatActionContext : public NamedObjectContext 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 @@ class ChatActionContext : public NamedObjectContext 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 cb37d271790..2debc44cbd8 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 6f48fa97395..c6183e30fd8 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 d50ea1b0608..b759bab6af2 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 @@ class ItemUsageValue : public CalculatedValue, public Qualified 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 @@ class ItemUsageValue : public CalculatedValue, public Qualified 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 1724778509a..f556f443d36 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 ab93bcea471..b67aee9bb43 100644 --- a/src/Ai/Base/ValueContext.h +++ b/src/Ai/Base/ValueContext.h @@ -216,6 +216,7 @@ class ValueContext : public NamedObjectContext 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 @@ class ValueContext : public NamedObjectContext 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 1e730952447..62afafd1a92 100644 --- a/src/Ai/Base/WorldPacketTriggerContext.h +++ b/src/Ai/Base/WorldPacketTriggerContext.h @@ -46,6 +46,7 @@ class WorldPacketTriggerContext : public NamedObjectContext 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 @@ class WorldPacketTriggerContext : public NamedObjectContext 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 787b518d696..09f34637031 100644 --- a/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/src/Ai/Class/Warlock/Action/WarlockActions.h @@ -41,6 +41,18 @@ class CastSoulLinkAction : public CastBuffSpellAction 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 a78ff0a988c..d22abe3caba 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 53e77669b26..1df531e7fd7 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 8512549012d..d66fe537e1f 100644 --- a/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h @@ -32,6 +32,20 @@ class SoulLinkTrigger : public BuffTrigger 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 a70327b6c68..27b9166c658 100644 --- a/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp +++ b/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp @@ -143,6 +143,8 @@ class WarlockTriggerFactoryInternal : public NamedObjectContext 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 @@ class WarlockTriggerFactoryInternal : public NamedObjectContext 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 @@ class WarlockAiObjectContextInternal : public NamedObjectContext 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 @@ class WarlockAiObjectContextInternal : public NamedObjectContext 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 414acc4bc90..bbb94d110a3 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 1a98135cac3..2af82ebe99f 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 6faf7ed3ecb..525e0ee6dff 100644 --- a/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h +++ b/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h @@ -85,10 +85,10 @@ class HighKingMaulgarMisdirectOlmAndBlindeyeAction : public AttackAction 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 5ca2de9327d..6604fc5250b 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 809fadf034e..3850f58c6d7 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h @@ -22,7 +22,7 @@ class RaidGruulsLairActionContext : public NamedObjectContext 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 @@ class RaidGruulsLairActionContext : public NamedObjectContext 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 fa8e76f58bc..d12b0ce4672 100644 --- a/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h +++ b/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h @@ -22,8 +22,8 @@ class RaidGruulsLairTriggerContext : public NamedObjectContext 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 @@ class RaidGruulsLairTriggerContext : public NamedObjectContext 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 9ec264ea00a..249c8e8a804 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 35d9f9a1dac..4bc5efe99cb 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 f3f32853613..583b8b75f7b 100644 --- a/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h +++ b/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h @@ -73,17 +73,17 @@ class HighKingMaulgarPullingOlmAndBlindeyeTrigger : public Trigger 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 0c8a23a19c8..27c703aecc9 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) + bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); - if (!target || !group) - return; + if (!group) + return false; - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) + // (1) First loop: Return the first assistant Mage (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - 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(); + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) + continue; - if (currentRti != rtiName || currentTarget != target) - { - botAI->GetAiObjectContext()->GetValue("rti")->Set(rtiName); - botAI->GetAiObjectContext()->GetValue("rti target")->Set(target); + if (group->IsAssistant(member->GetGUID())) + return member == bot; } - } - - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) - { - Group* group = bot->GetGroup(); - if (!group) - return false; + // (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)) + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_MAGE) continue; - if (member->getClass() == CLASS_MAGE) + 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; + // (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() || member->getClass() != CLASS_DRUID) + continue; + + 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() || !GET_PLAYERBOT_AI(member)) + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID || + !GET_PLAYERBOT_AI(member) || AiFactory::GetPlayerSpecTab(member) != DRUID_TAB_BALANCE) continue; - if (member->getClass() == CLASS_DRUID) + 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 c7becc83625..4615a9b7acd 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 adf0eac488d..4d40a9eedb5 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 117a17f38a2..06a3335c392 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 2fb7d5af0f0..3c43aa898ca 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 821cc670199..ea989dcf31f 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 394693b2e4a..ad7e8c3d5fa 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 69fc8624429..dab7efab97e 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 6c4ed84c22f..d47d06459ab 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 @@ class MagtheridonUseManticronCubeAction : public MovementAction 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 9580fd923a9..55aaf90e94f 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 35442df6e0e..43aa3361f3f 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 dc88d2a1927..a56d4b85bc3 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; - } - - 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); - } + Creature* channeler = it->second; + if (!channeler->IsAlive()) + return nullptr; - 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 f9f514de3ed..1335aff4655 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 79a4a95a856..aaccb80d337 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 b74958919cd..a62d851dc0d 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 00000000000..bcb48294f89 --- /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 00000000000..15c60353e50 --- /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 4f7a63c7adb..4d87c5ac11b 100644 --- a/src/Ai/Raid/RaidStrategyContext.h +++ b/src/Ai/Raid/RaidStrategyContext.h @@ -6,8 +6,10 @@ #include "RaidMcStrategy.h" #include "RaidBwlStrategy.h" #include "RaidKarazhanStrategy.h" -#include "RaidMagtheridonStrategy.h" #include "RaidGruulsLairStrategy.h" +#include "RaidMagtheridonStrategy.h" +#include "RaidSSCStrategy.h" +#include "RaidTempestKeepStrategy.h" #include "RaidOsStrategy.h" #include "RaidEoEStrategy.h" #include "RaidVoAStrategy.h" @@ -24,12 +26,14 @@ class RaidStrategyContext : public NamedObjectContext creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; - creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["magtheridon"] = &RaidStrategyContext::magtheridon; + creators["ssc"] = &RaidStrategyContext::ssc; + creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; 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; } @@ -39,13 +43,15 @@ class RaidStrategyContext : public NamedObjectContext static Strategy* moltencore(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); } static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } - static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } + static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } + static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } + static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(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 00000000000..7aa3eda35bd --- /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 00000000000..cbd23740210 --- /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 00000000000..c99cafa3c7f --- /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 00000000000..6630dc2060c --- /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 00000000000..e6dce16949d --- /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 00000000000..13135bf3e51 --- /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 00000000000..139667dc668 --- /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 00000000000..3c2c05f586e --- /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 00000000000..e77e63642ed --- /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 00000000000..e106b58f3ee --- /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 00000000000..7bda085be12 --- /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 00000000000..a725e28fcd4 --- /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/TempestKeep/Action/RaidTempestKeepActions.cpp b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp new file mode 100644 index 00000000000..6067cb9a1f3 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.cpp @@ -0,0 +1,2142 @@ +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "AiFactory.h" +#include "EquipAction.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// General + +bool TempestKeepEraseTimersAndTrackersAction::Execute(Event /*event*/) +{ + const ObjectGuid guid = bot->GetGUID(); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "void reaver")) + { + if (initialVoidReaverPositions.erase(guid) > 0) + erased = true; + if (hasReachedInitialVoidReaverPosition.erase(guid) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + { + if (advisorDpsWaitTimer.erase(instanceId) > 0) + erased = true; + } + + return erased; +} + +// Trash + +bool CrimsonHandCenturionCastPolymorphAction::Execute(Event /*event*/) +{ + Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + if (!centurion) + return false; + + if (centurion->GetHealth() == centurion->GetMaxHealth()) + { + if (!botAI->HasAura("polymorph", centurion)) + { + if (botAI->CanCastSpell("polymorph", centurion)) + return botAI->CastSpell("polymorph", centurion); + } + } + else if (centurion->HasAura(SPELL_ARCANE_FLURRY)) + { + botAI->Reset(); + return botAI->CastSpell("polymorph", centurion); + } + + return false; +} + +// Al'ar + +bool AlarMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", alar)) + return botAI->CastSpell("steady shot", alar); + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + MarkTargetWithStar(bot, alar); + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (botAI->IsMainTank(bot)) + return PositionMainTank(bot, alar, locationIndex); + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + return PositionAssistTank(bot, alar, locationIndex); + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionMainTank( + Player* mainTank, Unit* alar, int8 locationIndex) +{ + if (!mainTank) + return false; + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_3_IDX) + ? PLATFORM_POSITIONS[0] : PLATFORM_POSITIONS[2]; + + if (mainTank->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if (mainTank->GetTarget() != alar->GetGUID()) + { + return Attack(alar); + } + else if (alar->GetVictim() != mainTank) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + } + } + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionAssistTank( + Player* assistTank, Unit* alar, int8 locationIndex) +{ + if (!assistTank) + return false; + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_1_IDX) + ? PLATFORM_POSITIONS[1] : PLATFORM_POSITIONS[3]; + + if (assistTank->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if (assistTank->GetTarget() != alar->GetGUID()) + { + return Attack(alar); + } + else if (alar->GetVictim() != assistTank) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + } + } + + return false; +} + +bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = PLATFORM_POSITIONS[locationIndex]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position groundPositions[] = + {ALAR_GROUND_0, ALAR_GROUND_1, ALAR_GROUND_2, ALAR_GROUND_3}; + const Position& groundTarget = groundPositions[locationIndex]; + + if (botAI->IsRanged(bot)) + { + if (bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > 8.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + 8.0f, MovementPriority::MOVEMENT_COMBAT); + } + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of alar"); + if (!ember && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > 20.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + 20.0f, MovementPriority::MOVEMENT_COMBAT); + } + } + } + + return false; +} + +bool AlarAssistTanksPickUpEmbersAction::Execute(Event /*event*/) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return HandlePhase1Embers(alar); + else + return HandlePhase2Embers(alar); + + return false; +} + +// Embers will be tanked by only the second assist tank in Phase 1 +bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + Unit* ember = AI_VALUE2(Unit*, "find target", "ember of alar"); + if (ember) + { + MarkTargetWithSquare(bot, ember); + SetRtiTarget(botAI, "square", ember); + + if (bot->GetTarget() != ember->GetGUID()) + return Attack(ember); + + if (ember->GetVictim() == bot) + { + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + const Position& center = ALAR_POINT_MIDDLE; + float dx = center.GetPositionX() - groundTarget.GetPositionX(); + float dy = center.GetPositionY() - groundTarget.GetPositionY(); + float distToCenter = groundTarget.GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + + float moveDist = 25.0f; + float targetX = groundTarget.GetPositionX() + (dx / distToCenter) * moveDist; + float targetY = groundTarget.GetPositionY() + (dy / distToCenter) * moveDist; + float targetZ = groundTarget.GetPositionZ(); + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, targetZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + constexpr float safeDistance = 16.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (!bot->IsWithinMeleeRange(ember)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ember->GetPositionX(), ember->GetPositionY(), + ember->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// One Ember will be tanked by the second assist tank in Phase 2, and the other by +// the main tank or first assist tank (whichever is not tanking Al'ar) +bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers(Unit* alar) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + if (botAI->IsAssistTankOfIndex(bot, 1, true) && firstEmber) + { + MarkTargetWithSquare(bot, firstEmber); + SetRtiTarget(botAI, "square", firstEmber); + + if (firstEmber->GetVictim() != bot) + { + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, firstEmber)) + return botAI->CastSpell(spellName, firstEmber); + } + } + else if (bot->IsWithinMeleeRange(firstEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (GetSecondEmberTank(botAI, alar) == bot && secondEmber) + { + MarkTargetWithCircle(bot, secondEmber); + SetRtiTarget(botAI, "circle", secondEmber); + + if (secondEmber->GetVictim() != bot) + { + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, secondEmber)) + return botAI->CastSpell(spellName, secondEmber); + } + } + else if (bot->IsWithinMeleeRange(secondEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + + return false; +} + +bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event /*event*/) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + constexpr float safeDistance = 15.0f; + if (firstEmber) + { + if (bot->GetDistance2d(firstEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(firstEmber, safeDistance - bot->GetDistance2d(firstEmber)); + } + SetRtiTarget(botAI, "square", firstEmber); + + if (bot->GetTarget() != firstEmber->GetGUID()) + return Attack(firstEmber); + } + else if (secondEmber) + { + if (bot->GetDistance2d(secondEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(secondEmber, safeDistance - bot->GetDistance2d(secondEmber)); + } + SetRtiTarget(botAI, "circle", secondEmber); + + if (bot->GetTarget() != secondEmber->GetGUID()) + return Attack(secondEmber); + } + else if (Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar")) + { + SetRtiTarget(botAI, "star", alar); + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + return false; +} + +// Jump from platform during Flame Quills and wait at assigned position after landing +bool AlarJumpFromPlatformAction::Execute(Event /*event*/) +{ + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsMainTank(bot)) + { + if (bot->GetExactDist2d(ALAR_SW_RAMP_BASE.GetPositionX(), ALAR_SW_RAMP_BASE.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SW_RAMP_BASE.GetPositionX(), ALAR_SW_RAMP_BASE.GetPositionY(), + ALAR_SW_RAMP_BASE.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (bot->GetExactDist2d(ALAR_SE_RAMP_BASE.GetPositionX(), ALAR_SE_RAMP_BASE.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SE_RAMP_BASE.GetPositionX(), ALAR_SE_RAMP_BASE.GetPositionY(), + ALAR_SE_RAMP_BASE.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + if (bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY()) > 25.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY(), + ALAR_POINT_MIDDLE.GetPositionZ(), 25.0f, MovementPriority::MOVEMENT_FORCED); + } + } + else if (botAI->IsMelee(bot)) + { + if (bot->GetExactDist2d(ALAR_ROOM_S_CENTER.GetPositionX(), ALAR_ROOM_S_CENTER.GetPositionY()) > 5.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_ROOM_S_CENTER.GetPositionX(), ALAR_ROOM_S_CENTER.GetPositionY(), + ALAR_ROOM_S_CENTER.GetPositionZ(), 5.0f, MovementPriority::MOVEMENT_FORCED); + } + } + else if (botAI->IsRanged(bot)) + { + if (bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY()) > 10.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), ALAR_POINT_MIDDLE.GetPositionY(), + ALAR_POINT_MIDDLE.GetPositionZ(), 10.0f, MovementPriority::MOVEMENT_FORCED); + } + } + + return false; +} + +bool AlarMoveAwayFromRebirthAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(alar, safeDistance - currentDistance); + } + } + + return false; +} + +// Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied +bool AlarSwapTanksOnBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (alar->GetHealth() == alar->GetMaxHealth()) + { + SetRtiTarget(botAI, "star", alar); + + if (bot->GetTarget() != alar->GetGUID()) + return Attack(alar); + } + + Player* secondEmberTank = GetSecondEmberTank(botAI, alar); + if (secondEmberTank && secondEmberTank != bot) + { + SetRtiTarget(botAI, "star", alar); + + if (bot->GetTarget() != alar->GetGUID()) + { + return Attack(alar); + } + else if (alar->GetVictim() != bot) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, alar)) + return botAI->CastSpell(spellName, alar); + } + } + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (AvoidFlamePatch()) + return true; + + if (HandleDiveBomb(alar)) + return true; + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() +{ + std::vector flamePatches = GetAllHazardTriggers(botAI, bot, NPC_FLAME_PATCH, 40.0f); + constexpr float hazardRadius = 8.0f; + for (Unit* flamePatch : flamePatches) + { + if (bot->GetExactDist2d(flamePatch) < hazardRadius) + { + Position safestPos = FindSafestNearbyPosition(bot, flamePatches, 30.0f, hazardRadius); + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) +{ + if ((alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) || + !alar->IsVisible()) + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(alar, safeDistance - currentDistance); + } + } + else + { + Position dest; + if (GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) + { + constexpr float safeDistance = 10.0f; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + { + constexpr uint32 minInterval = 0; + return FleePosition(Position(nearestPlayer->GetPosition()), + safeDistance, minInterval); + } + } + } + + return false; +} + +// For Phase 2, ensure that bots don't get too far away and become inactive +bool AlarReturnToRoomCenterAction::Execute(Event /*event*/) +{ + const Position& center = ALAR_ROOM_CENTER; + if (bot->GetVictim() == nullptr && + bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 45.0f) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), 40.0f, MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool AlarManagePhaseTrackerAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + const uint32 instanceId = alar->GetMap()->GetInstanceId(); + + if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) + { + isAlarInPhase2.erase(instanceId); + lastRebirthState.erase(instanceId); + } + + bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); + bool lastRebirth = lastRebirthState[instanceId]; + + if (lastRebirth && !rebirthActive) + isAlarInPhase2[instanceId] = true; + + lastRebirthState[instanceId] = rebirthActive; + + return false; +} + +// Void Reaver + +bool VoidReaverTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + const Position& position = VOID_REAVER_TANK_POSITION; + + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + + if (bot->IsWithinMeleeRange(voidReaver) && distToPosition > 2.0f) + { + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, true); + } + + return false; +} + +bool VoidReaverRangedUseAggroDumpAbilityAction::Execute(Event /*event*/) +{ + botAI->Reset(); + static const std::array spells = + { + "divine protection", + "fade", + "feign death", + "ice block", + "soulshatter", + "vanish", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, bot)) + return botAI->CastSpell(spell, bot); + } + + return false; +} + +// As far as I can tell, it is not possible for bots to detect Arcane Orbs +// Therefore, this spreads out the ranged bots so as few of them as possible get hit +bool VoidReaverSpreadRangedAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + if (initialVoidReaverPositions.empty()) + { + std::vector healers; + std::vector rangedDps; + 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; + if (botAI->IsHeal(member)) + healers.push_back(member); + else + rangedDps.push_back(member); + } + } + + const Position& position = VOID_REAVER_TANK_POSITION; + constexpr float radius = 30.0f; + constexpr float offsetArc = 1.0f; + constexpr uint8 botsPerRing = 8; + + std::vector rangedBots = healers; + rangedBots.insert(rangedBots.end(), rangedDps.begin(), rangedDps.end()); + + for (size_t i = 0; i < rangedBots.size(); ++i) + { + Player* ranged = rangedBots[i]; + uint8 ringIndex = i / botsPerRing; + uint8 posInRing = i % botsPerRing; + float ringRadius = radius + (ringIndex * offsetArc); + float angle = 2 * M_PI * posInRing / botsPerRing; + + float targetX = position.GetPositionX() + ringRadius * std::cos(angle); + float targetY = position.GetPositionY() + ringRadius * std::sin(angle); + + Position pos(targetX, targetY, ranged->GetPositionZ()); + initialVoidReaverPositions[ranged->GetGUID()] = pos; + hasReachedInitialVoidReaverPosition[ranged->GetGUID()] = false; + } + } + + Position targetPosition = initialVoidReaverPositions[bot->GetGUID()]; + float destX = targetPosition.GetPositionX(); + float destY = targetPosition.GetPositionY(); + + if (bot->GetExactDist2d(destX, destY) > 1.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, destX, destY, targetPosition.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +Position VoidReaverSpreadRangedAction::GetRangedBotPosition(const Position& center, + float radius, uint8 botsPerRing, float offsetArc, uint8 botIndex, float botZ) +{ + float angleOffset = (offsetArc / radius); + uint8 ringIndex = botIndex / botsPerRing; + uint8 posInRing = botIndex % botsPerRing; + float baseAngle = 2 * M_PI * posInRing / botsPerRing; + float angle = baseAngle + (ringIndex == 1 ? angleOffset : 0); + + if (ringIndex > 1) + { + angle = 2 * M_PI * (botIndex % botsPerRing) / botsPerRing; + ringIndex = 0; + } + + float targetX = center.GetPositionX() + radius * cos(angle); + float targetY = center.GetPositionY() + radius * sin(angle); + + return Position(targetX, targetY, botZ); +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event /*event*/) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + float currentDistance = bot->GetExactDist2d(astromancer); + constexpr float minDistance = 20.0f; + if (currentDistance < minDistance) + return MoveAway(astromancer, minDistance - currentDistance); + + return false; +} + +bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 15.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + { + botAI->Reset(); + return MoveFromGroup(safeDistance); + } + + return false; +} + +bool HighAstromancerSolarianStackForAoeAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* stackTarget = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsRanged(member)) + { + stackTarget = member; + break; + } + } + + if (stackTarget && bot != stackTarget && bot->GetExactDist2d(stackTarget) >= 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Split melee into two groups, one on each Solarium Priest +bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event /*event*/) +{ + auto priestsPair = GetSolariumPriests(botAI); + if (!priestsPair.first || !priestsPair.second) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto meleeMembers = GetMeleeBots(group); + if (meleeMembers.empty()) + return false; + + Unit* targetPriest = AssignSolariumPriestsToBots(priestsPair, meleeMembers); + if (!targetPriest) + return false; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return false; + + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + + if (targetPriest == priestsPair.first) + { + MarkTargetWithSquare(bot, targetPriest); + SetRtiTarget(botAI, "square", targetPriest); + } + else + { + MarkTargetWithStar(bot, targetPriest); + SetRtiTarget(botAI, "star", targetPriest); + } + + if (bot->GetTarget() != targetPriest->GetGUID()) + return Attack(targetPriest); + + return false; +} + +std::pair HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests(PlayerbotAI* botAI) +{ + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST) + { + if (!lowest || unit->GetGUID().GetRawValue() < lowest->GetGUID().GetRawValue()) + lowest = unit; + + if (!highest || unit->GetGUID().GetRawValue() > highest->GetGUID().GetRawValue()) + highest = unit; + } + } + + return {lowest, highest}; +} + +std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMeleeBots(Group* group) +{ + std::vector meleeMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMelee(member) && GET_PLAYERBOT_AI(member)) + meleeMembers.push_back(member); + } + + std::sort(meleeMembers.begin(), meleeMembers.end(), + [](Player* left, Player* right) { return left->GetGUID() < right->GetGUID(); }); + + return meleeMembers; +} + +Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsToBots( + const std::pair& priestsPair, const std::vector& meleeMembers) +{ + if (!priestsPair.first || !priestsPair.second || meleeMembers.empty()) + return nullptr; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return nullptr; + + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + + if (totalMelee == 1) + return priestsPair.first; + + size_t split = totalMelee / 2; + if (botIndex < split) + return priestsPair.first; + else + return priestsPair.second; +} + +bool HighAstromancerSolarianTankVoidwalkerAction::Execute(Event /*event*/) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + if (bot->GetTarget() != astromancer->GetGUID()) + return Attack(astromancer); + + if (astromancer->GetVictim() != bot) + { + const char* taunts[] = { "taunt", "growl", "hand of reckoning", "dark command" }; + for (const char* spellName : taunts) + { + if (botAI->CanCastSpell(spellName, astromancer)) + return botAI->CastSpell(spellName, astromancer); + } + } + + return false; +} + +bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event /*event*/) +{ + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderKiteThaladredAction::Execute(Event /*event*/) +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred) + return false; + + float currentDistance = bot->GetExactDist2d(thaladred); + constexpr float safeDistance = 23.0f; + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(thaladred, safeDistance - currentDistance); + } + + return false; +} + +// Misdirect order: (1) Capernian, (2) Telonicus, (3) Capernian (again for good measure) +bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::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; + } + + int8 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* advisorTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(bot); + } + else if (hunterIndex == 1) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && + GET_PLAYERBOT_AI(member)->IsAssistTankOfIndex(member, 0, true)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(bot); + } + + if (!advisorTarget || + advisorTarget->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + advisorTarget->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) || + advisorTarget->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + if (!tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", advisorTarget)) + return botAI->CastSpell("steady shot", advisorTarget); + + return false; +} + +bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event /*event*/) +{ + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (!sanguinar) + return false; + + MarkTargetWithStar(bot, sanguinar); + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + if (sanguinar->GetVictim() == bot && bot->IsWithinMeleeRange(sanguinar)) + { + const Position& position = SANGUINAR_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(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + MarkTargetWithCircle(bot, capernian); + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID()) + { + if (botAI->CanCastSpell("searing pain", capernian)) + botAI->CastSpell("searing pain", capernian); + } + + if (capernian->GetVictim() == bot) + { + float currentDist = bot->GetDistance2d(capernian); + constexpr float minDistance = 20.0f; + if (currentDist < minDistance) + return MoveAway(capernian, minDistance - currentDist); + } + + if (botAI->CanCastSpell("searing pain", capernian)) + return botAI->CastSpell("searing pain", capernian); + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (botAI->IsRanged(bot)) + { + if (RangedBotsDisperse(kaelAI)) + return true; + } + + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + if (StayBackFromCapernian()) + return true; + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse(boss_kaelthas* kaelAI) +{ + if (kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + { + if (AI_VALUE2(Unit*, "find target", "thaladred the darkener")) + return false; + } + + constexpr float safeDistance = 6.0f; + if (Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + { + constexpr uint32 minInterval = 1000; + return FleePosition(Position(nearestPlayer->GetPosition()), + safeDistance, minInterval); + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::StayBackFromCapernian() +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + // Main tank purposely stays in range to bait Conflagration in Phase 1 + if (botAI->IsMainTank(bot)) + { + constexpr float desiredDist = 15.0f; + if (fabs(bot->GetExactDist2d(capernian) - desiredDist)) + { + float dx = bot->GetPositionX() - capernian->GetPositionX(); + float dy = bot->GetPositionY() - capernian->GetPositionY(); + float distanceToCapernian = bot->GetExactDist2d(capernian); + if (distanceToCapernian == 0.0f) + return false; + + float nx = dx / distanceToCapernian; + float ny = dy / distanceToCapernian; + + float targetX = capernian->GetPositionX() + nx * desiredDist; + float targetY = capernian->GetPositionY() + ny * desiredDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, capernian->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return true; + } + + float safeDistance = 0.0f; + if (botAI->IsMelee(bot)) + safeDistance = 45.0f; + else if (botAI->IsRangedDps(bot)) + safeDistance = 25.0f; + else if (botAI->IsHeal(bot)) + safeDistance = 40.0f; + + float currentDistance = bot->GetExactDist2d(capernian); + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(capernian, safeDistance - currentDistance); + } + + if (botAI->IsMelee(bot)) + { + botAI->Reset(); + return true; + } + + return false; +} + +bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event /*event*/) +{ + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (!telonicus) + return false; + + MarkTargetWithTriangle(bot, telonicus); + SetRtiTarget(botAI, "triangle", telonicus); + + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + if (telonicus->GetVictim() == bot && bot->IsWithinMeleeRange(telonicus)) + { + const Position& position = TELONICUS_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(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event /*event*/) +{ + const Position* movePosition = nullptr; + if (botAI->IsAssistHealOfIndex(bot, 0, true)) + movePosition = &ADVISOR_HEAL_POSITION; + else if (botAI->IsMainTank(bot)) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &SANGUINAR_WAITING_POSITION; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &TELONICUS_WAITING_POSITION; + } + else if (GetCapernianTank(bot) == bot) + { + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (capernian && capernian->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &CAPERNIAN_WAITING_POSITION; + } + + if (movePosition) + { + if (bot->GetExactDist2d(movePosition->GetPositionX(), movePosition->GetPositionY()) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, movePosition->GetPositionX(), movePosition->GetPositionY(), + movePosition->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool KaelthasSunstriderReequipGearAction::Execute(Event /*event*/) +{ + return botAI->DoSpecificAction("equip upgrade", Event(), true); +} + +bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Thaladred, except Capernian tank + Player* capernianTank = GetCapernianTank(bot); + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if ((!capernianTank || bot != capernianTank) && + thaladred && !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + MarkTargetWithSquare(bot, thaladred); + SetRtiTarget(botAI, "square", thaladred); + + if (bot->GetTarget() != thaladred->GetGUID()) + return Attack(thaladred); + + return false; + } + + // Target priority 2: Capernian for ranged only (excluding longbow tank) + Player* debuffHunter = GetDebuffHunter(bot); + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (botAI->IsRangedDps(bot) && (!debuffHunter || bot != debuffHunter) && + capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "circle", capernian); + + if (bot->GetTarget() != capernian->GetGUID()) + return Attack(capernian); + + return false; + } + + // Target priority 3: Sanguinar (debuff hunter and melee move here after Thaladred) + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "star", sanguinar); + + if (bot->GetTarget() != sanguinar->GetGUID()) + return Attack(sanguinar); + + return false; + } + + // Target priority 4: Telonicus + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "triangle", telonicus); + + if (bot->GetTarget() != telonicus->GetGUID()) + return Attack(telonicus); + + // Melee DPS need to stay at max-ish melee range behind Telonicus (god damn bombs) + if (botAI->IsMelee(bot) && botAI->IsDps(bot) && telonicus->GetVictim() != bot) + { + float maxMeleeRange = bot->GetMeleeRange(telonicus); + constexpr float tolerance = 0.25f; + + float desiredDist = std::max(2.0f, maxMeleeRange - tolerance); + float currentDist = bot->GetExactDist2d(telonicus); + + if (fabs(currentDist - desiredDist) > tolerance) + { + float behindAngle = Position::NormalizeOrientation(telonicus->GetOrientation() + M_PI); + float targetX = telonicus->GetPositionX() + desiredDist * std::cos(behindAngle); + float targetY = telonicus->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > tolerance) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, telonicus->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + } + + return false; +} + +bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + const char* advisorNames[] = + { + "grand astromancer capernian", + "master engineer telonicus", + "lord sanguinar" + }; + + for (const char* name : advisorNames) + { + Unit* advisor = AI_VALUE2(Unit*, "find target", name); + if (!advisor) + continue; + + if (advisor->GetHealth() == advisor->GetMaxHealth() && + !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE)) + { + const time_t now = std::time(nullptr); + advisorDpsWaitTimer.insert_or_assign(kaelthas->GetMap()->GetInstanceId(), now); + return true; + } + } + + return false; +} + +bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTank(bot)) + SetRtiTarget(botAI, "moon", nullptr); + + // Priority 0: Everybody other than the main tank needs to stay away from the axe + // But this applies to assist tanks only after they get aggro on the mace, dagger, or sword + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + Unit* mace = AI_VALUE2(Unit*, "find target", "cosmic infuser"); + Unit* dagger = AI_VALUE2(Unit*, "find target", "infinity blades"); + Unit* sword = AI_VALUE2(Unit*, "find target", "warp slicer"); + if (axe) + { + if (botAI->IsDps(bot) || botAI->IsHeal(bot) || + (botAI->IsAssistTank(bot) && (mace && mace->GetVictim() == bot || + dagger && dagger->GetVictim() == bot || sword && sword->GetVictim() == bot))) + { + const float safeDistance = botAI->IsAssistTank(bot) ? 17.0f : 13.0f; + float currentDistance = bot->GetExactDist2d(axe); + if (currentDistance < safeDistance) + return MoveAway(axe, safeDistance - currentDistance); + } + } + + if (botAI->IsDps(bot)) + { + // Priority 1: Staff of Disintegration (Skull) + if (Unit* staff = AI_VALUE2(Unit*, "find target", "staff of disintegration")) + { + MarkTargetWithSkull(bot, staff); + SetRtiTarget(botAI, "skull", staff); + + if (bot->GetTarget() != staff->GetGUID()) + return Attack(staff); + } + // Priority 2: Cosmic Infuser (Skull) + else if (mace) + { + MarkTargetWithSkull(bot, mace); + SetRtiTarget(botAI, "skull", mace); + + if (bot->GetTarget() != mace->GetGUID()) + return Attack(mace); + } + // Priority 3: Warp Slicer (Skull) + else if (sword) + { + MarkTargetWithSkull(bot, sword); + SetRtiTarget(botAI, "skull", sword); + + if (bot->GetTarget() != sword->GetGUID()) + return Attack(sword); + } + // Priority 4: Infinity Blades (Skull) + else if (dagger) + { + MarkTargetWithSkull(bot, dagger); + SetRtiTarget(botAI, "skull", dagger); + + if (bot->GetTarget() != dagger->GetGUID()) + return Attack(dagger); + } + // Priority 5: Devastation - ranged only (Diamond--marked in other method by main tank) + else if (axe && botAI->IsRangedDps(bot)) + { + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + } + // Priority 6: Netherstrand Longbow (Cross) + else if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + { + MarkTargetWithCross(bot, longbow); + SetRtiTarget(botAI, "cross", longbow); + + if (bot->GetTarget() != longbow->GetGUID()) + return Attack(longbow); + } + // Priority 7: Phaseshift Bulwark (Skull) + else if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) + { + MarkTargetWithSkull(bot, shield); + SetRtiTarget(botAI, "skull", shield); + + if (bot->GetTarget() != shield->GetGUID()) + return Attack(shield); + } + } + + return false; +} + +bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event /*event*/) +{ + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (!axe) + return false; + + MarkTargetWithDiamond(bot, axe); + SetRtiTarget(botAI, "diamond", axe); + + if (bot->GetTarget() != axe->GetGUID()) + return Attack(axe); + + if (axe->GetVictim() == bot) + { + constexpr float safeDistance = 13.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event /*event*/) +{ + struct WeaponInfo + { + uint32 npcEntry; + uint32 itemId; + }; + + const WeaponInfo weapons[] = + { + { NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW }, + { NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER }, + { NPC_DEVASTATION, ITEM_DEVASTATION }, + { NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE }, + { NPC_WARP_SLICER, ITEM_WARP_SLICER }, + { NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION }, + { NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK } + }; + + for (auto const& weapon : weapons) + { + if (ShouldBotLootWeapon(weapon.npcEntry)) + { + if (bot->HasItemCount(weapon.itemId, 1, false)) + { + EquipAction* equipAction = + dynamic_cast(botAI->GetAiObjectContext()->GetAction("equip")); + if (equipAction) + { + ItemIds ids; + ids.insert(weapon.itemId); + equipAction->EquipItems(ids); + } + continue; + } + + return LootWeapon(weapon.npcEntry, weapon.itemId); + } + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 weaponEntry) +{ + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + switch (weaponEntry) + { + case NPC_NETHERSTRAND_LONGBOW: + return bot->getClass() == CLASS_HUNTER; + + case NPC_COSMIC_INFUSER: + return botAI->IsHeal(bot); + + // Fury Warriors could use the axe, but their DPS is terrible at appropriate gear levels + // So IMO they're better off looting only the dagger to MH it and break MCs + case NPC_DEVASTATION: + return (bot->getClass() == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) || + (bot->getClass() == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) || + (botAI->IsDps(bot) && bot->getClass() == CLASS_DEATH_KNIGHT); + + case NPC_INFINITY_BLADES: + return bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_HUNTER || + (bot->getClass() == CLASS_SHAMAN && tab == SHAMAN_TAB_ENHANCEMENT) || + (bot->getClass() == CLASS_WARRIOR && tab != WARRIOR_TAB_ARMS); + + case NPC_WARP_SLICER: + return bot->getClass() == CLASS_ROGUE && tab != ROGUE_TAB_ASSASSINATION || + (botAI->IsTank(bot) && + (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_PALADIN)); + + case NPC_STAFF_OF_DISINTEGRATION: + return (botAI->IsRangedDps(bot) && bot->getClass() != CLASS_HUNTER) || + (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL); + + case NPC_PHASESHIFT_BULWARK: + return botAI->IsTank(bot) && + (bot->getClass() == CLASS_PALADIN || + bot->getClass() == CLASS_WARRIOR || + bot->getClass() == CLASS_DEATH_KNIGHT); + + default: + return false; + } +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( + uint32 weaponEntry, uint32 itemId) +{ + 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) + continue; + + if (creature->GetEntry() != weaponEntry || creature->IsAlive()) + continue; + + context->GetValue("loot target")->Set(loot); + + float distToObject = bot->GetDistance(object); + + if (distToObject > maxLootRange) + return MoveTo(object, 2.0f, MovementPriority::MOVEMENT_FORCED); + + OpenLootAction open(botAI); + bool opened = open.Execute(Event()); + + if (!opened) + return opened; + + const ObjectGuid botGuid = bot->GetGUID(); + const ObjectGuid corpseGuid = guid; + constexpr uint8 weaponIndex = 0; + + botAI->AddTimedEvent([this, botGuid, corpseGuid, weaponIndex, itemId]() + { + Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); + if (!receiver || !receiver->IsInWorld()) + return; + + if (receiver->HasItemCount(itemId, 1, false)) + return; + + receiver->SetLootGUID(corpseGuid); + + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << weaponIndex; + receiver->GetSession()->QueuePacket(packet); + }, 600); + return true; + } + + return false; +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event /*event*/) +{ + return UsePhaseshiftBulwark() || + UseStaffOfDisintegration() || + UseNetherstrandLongbow(); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() +{ + Item* offHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!offHand || offHand->GetEntry() != ITEM_PHASESHIFT_BULWARK) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas || !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + return false; + + if (bot->HasAura(SPELL_ARCANE_BARRIER)) + return false; + + if (bot->CanUseItem(offHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(offHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseStaffOfDisintegration() +{ + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!mainHand || mainHand->GetEntry() != ITEM_STAFF_OF_DISINTEGRATION) + return false; + + if (bot->HasAura(SPELL_MENTAL_PROTECTION_FIELD)) + return false; + + return UseEquippedItemWithPacket(mainHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseNetherstrandLongbow() +{ + Item* ranged = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!ranged || ranged->GetEntry() != ITEM_NETHERSTRAND_LONGBOW) + return false; + + if (bot->HasItemCount(ITEM_NETHER_SPIKES, 401, false)) + return false; + + return UseEquippedItemWithPacket(ranged); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseEquippedItemWithPacket(Item* item) +{ + if (!item) + return false; + + if (bot->CanUseItem(item) != EQUIP_ERR_OK) + return false; + + if (bot->IsNonMeleeSpellCast(true)) + return false; + + uint8 bagIndex = item->GetBagSlot(); + uint8 slot = item->GetSlot(); + uint8 cast_count = 1; + ObjectGuid item_guid = item->GetGUID(); + uint32 glyphIndex = 0; + uint8 castFlags = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (item->GetTemplate()->Spells[i].SpellId > 0 && + item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + spellId = item->GetTemplate()->Spells[i].SpellId; + break; + } + } + + if (!spellId) + return false; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; + + uint32 targetFlag = TARGET_FLAG_UNIT; + packet << targetFlag << bot->GetPackGUID(); + + bot->GetSession()->HandleUseItemOpcode(packet); + return true; +} + +bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + MarkTargetWithStar(bot, kaelthas); + SetRtiTarget(botAI, "star", kaelthas); + + if (bot->GetTarget() != kaelthas->GetGUID()) + return Attack(kaelthas); + + if (kaelthas->GetVictim() == bot && bot->IsWithinMeleeRange(kaelthas)) + { + const Position& position = KAELTHAS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.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(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event /*event*/) +{ + std::vector flameStrikes = + GetAllHazardTriggers(botAI, bot, NPC_FLAME_STRIKE_TRIGGER, 40.0f); + if (flameStrikes.empty()) + return false; + + constexpr float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameStrike : flameStrikes) + { + if (bot->GetExactDist2d(flameStrike) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + Position safestPos = FindSafestNearbyPosition(bot, flameStrikes, 30.0f, hazardRadius); + + botAI->Reset(); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTankOfIndex(bot, 0, true) || + botAI->IsAssistTankOfIndex(bot, 1, true)) + { + std::vector phoenixes; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_PHOENIX && unit->IsAlive()) + phoenixes.push_back(unit); + } + + if (phoenixes.empty()) + return false; + + std::sort(phoenixes.begin(), phoenixes.end(), + [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + Unit* targetPhoenix = nullptr; + if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + targetPhoenix = phoenixes[0]; + MarkTargetWithSquare(bot, targetPhoenix); + SetRtiTarget(botAI, "square", targetPhoenix); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && phoenixes.size() >= 2) + { + targetPhoenix = phoenixes[1]; + MarkTargetWithCircle(bot, targetPhoenix); + SetRtiTarget(botAI, "circle", targetPhoenix); + } + + if (!targetPhoenix) + return false; + + if (bot->GetTarget() != targetPhoenix->GetGUID()) + return Attack(targetPhoenix); + + if (targetPhoenix->GetVictim() == bot) + { + constexpr float safeDistance = 12.0f; + if (GetNearestNonTankPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + + return false; + } + else if (!botAI->IsTank(bot)) + { + if (Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix")) + { + float currentDistance = bot->GetExactDist2d(phoenix); + constexpr float safeDistance = 12.0f; + if (currentDistance < safeDistance) + return MoveAway(phoenix, safeDistance - currentDistance); + } + + if (botAI->IsDps(bot)) + { + if (Unit* phoenixEgg = GetFirstAliveUnitByEntry(botAI, NPC_PHOENIX_EGG)) + { + MarkTargetWithDiamond(bot, phoenixEgg); + SetRtiTarget(botAI, "diamond", phoenixEgg); + + if (bot->GetTarget() != phoenixEgg->GetGUID()) + return Attack (phoenixEgg); + } + } + } + + return false; +} + +bool KaelthasSunstriderBreakMindControlAction::Execute(Event /*event*/) +{ + Player* mcTarget = nullptr; + float closestDist = std::numeric_limits::max(); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) + { + closestDist = dist; + mcTarget = member; + } + } + } + + if (!mcTarget) + return false; + + if (!bot->IsWithinMeleeRange(mcTarget)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), + mcTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->getClass() == CLASS_ROGUE && + AiFactory::GetPlayerSpecTab(bot) != ROGUE_TAB_COMBAT) + { + if (botAI->CanCastSpell("sinister strike", mcTarget)) + return botAI->CastSpell("sinister strike", mcTarget); + } + else + { + static const std::array spells = + { + "hamstring", + "wing clip", + "shiv", + "stormstrike" + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, mcTarget)) + return botAI->CastSpell(spell, mcTarget); + } + } + + return false; +} + +// Shock Barrier needs to be #1 focus, even if there is a Phoenix Egg up +bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + static const std::array spells = + { + "bash", + "counterspell", + "kick", + "mind freeze", + "pummel", + "shield bash", + "silencing shot", + "wind shear", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, kaelthas)) + return botAI->CastSpell(spell, kaelthas); + } + } + else if (bot->GetTarget() != kaelthas->GetGUID()) + { + SetRtiTarget(botAI, "star", kaelthas); + return Attack(kaelthas); + } + + return false; +} + +// Bots generally immediately fall to the ground after Gravity Lapse, so this action +// name is kind of a misnomer (though bots are still in a flying state) +bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr float minSpreadDistance = 16.0f; + + std::vector nearbyPlayers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (bot->IsWithinDist3d(member, minSpreadDistance * 1.0f)) + nearbyPlayers.push_back(member); + } + + if (nearbyPlayers.empty()) + return false; + + Player* closestPlayer = nullptr; + float closestDist = std::numeric_limits::max(); + for (Player* player : nearbyPlayers) + { + float distToPlayer = bot->GetExactDist(player); + if (distToPlayer < closestDist) + { + closestDist = distToPlayer; + closestPlayer = player; + } + } + + if (closestPlayer && closestDist < minSpreadDistance) + { + float angle = bot->GetAngle(closestPlayer) + M_PI; + float distance = minSpreadDistance - closestDist; + + float x = bot->GetPositionX() + std::cos(angle) * distance; + float y = bot->GetPositionY() + std::sin(angle) * distance; + + return MoveTo(TEMPEST_KEEP_MAP_ID, x, y, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} diff --git a/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h new file mode 100644 index 00000000000..bb30023d58c --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Action/RaidTempestKeepActions.h @@ -0,0 +1,411 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONS_H + +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +using namespace TempestKeepHelpers; + +// General + +class TempestKeepEraseTimersAndTrackersAction : public Action +{ +public: + TempestKeepEraseTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "tempest keep erase timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Trash + +class CrimsonHandCenturionCastPolymorphAction : public Action +{ +public: + CrimsonHandCenturionCastPolymorphAction( + PlayerbotAI* botAI, std::string const name = "crimson hand centurion cast polymorph") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Al'ar + +class AlarMisdirectBossToMainTankAction : public AttackAction +{ +public: + AlarMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "al'ar misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarBossTanksMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarBossTanksMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar boss tanks move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool PositionMainTank(Player* mainTank, Unit* alar, int8 locationIndex); + bool PositionAssistTank(Player* assistTank, Unit* alar, int8 locationIndex); +}; + +class AlarMeleeDpsMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarMeleeDpsMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar melee dps move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarRangedAndEmberTankMoveUnderPlatformsAction : public AttackAction +{ +public: + AlarRangedAndEmberTankMoveUnderPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged and ember tank move under platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAssistTanksPickUpEmbersAction : public AttackAction +{ +public: + AlarAssistTanksPickUpEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar assist tanks pick up embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandlePhase1Embers(Unit* alar); + bool HandlePhase2Embers(Unit* alar); +}; + +class AlarRangedDpsPrioritizeEmbersAction : public AttackAction +{ +public: + AlarRangedDpsPrioritizeEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged dps prioritize embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarJumpFromPlatformAction : public MovementAction +{ +public: + AlarJumpFromPlatformAction( + PlayerbotAI* botAI, std::string const name = "al'ar jump from platform") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarMoveAwayFromRebirthAction : public MovementAction +{ +public: + AlarMoveAwayFromRebirthAction( + PlayerbotAI* botAI, std::string const name = "al'ar move away from rebirth") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarSwapTanksOnBossAction : public AttackAction +{ +public: + AlarSwapTanksOnBossAction( + PlayerbotAI* botAI, std::string const name = "al'ar swap tanks on boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAvoidFlamePatchesAndDiveBombsAction : public MovementAction +{ +public: + AlarAvoidFlamePatchesAndDiveBombsAction( + PlayerbotAI* botAI, std::string const name = "al'ar avoid flame patches and dive bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AvoidFlamePatch(); + bool HandleDiveBomb(Unit* alar); +}; + +class AlarReturnToRoomCenterAction : public MovementAction +{ +public: + AlarReturnToRoomCenterAction( + PlayerbotAI* botAI, std::string const name = "al'ar return to room center") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarManagePhaseTrackerAction : public Action +{ +public: + AlarManagePhaseTrackerAction( + PlayerbotAI* botAI, std::string const name = "al'ar manage phase tracker") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Void Reaver + +class VoidReaverTanksPositionBossAction : public AttackAction +{ +public: + VoidReaverTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "void reaver tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverRangedUseAggroDumpAbilityAction : public Action +{ +public: + VoidReaverRangedUseAggroDumpAbilityAction( + PlayerbotAI* botAI, std::string const name = "void reaver ranged use aggro dump ability") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverSpreadRangedAction : public MovementAction +{ +public: + VoidReaverSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "void reaver spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + Position GetRangedBotPosition( + const Position& center, float radius, uint8 botsPerRing, float offsetArc, uint8 botIndex, float botZ); +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianRangedLeaveSpaceForMeleeAction : public MovementAction +{ +public: + HighAstromancerSolarianRangedLeaveSpaceForMeleeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian ranged leave space for melee") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianMoveAwayFromGroupAction : public MovementAction +{ +public: + HighAstromancerSolarianMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianStackForAoeAction : public MovementAction +{ +public: + HighAstromancerSolarianStackForAoeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian stack for aoe") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianTargetSolariumPriestsAction : public AttackAction +{ +public: + HighAstromancerSolarianTargetSolariumPriestsAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian target solarium priests") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + std::pair GetSolariumPriests(PlayerbotAI* botAI); + std::vector GetMeleeBots(Group* group); + Unit* AssignSolariumPriestsToBots(const std::pair& priestsPair, const std::vector& meleeMembers); +}; + +class HighAstromancerSolarianTankVoidwalkerAction : public AttackAction +{ +public: + HighAstromancerSolarianTankVoidwalkerAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian tank voidwalker") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianCastFearWardOnMainTankAction : public Action +{ +public: + HighAstromancerSolarianCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderKiteThaladredAction : public MovementAction +{ +public: + KaelthasSunstriderKiteThaladredAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider kite thaladred") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMisdirectAdvisorsToTanksAction : public AttackAction +{ +public: + KaelthasSunstriderMisdirectAdvisorsToTanksAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider misdirect advisors to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionSanguinarAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionSanguinarAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position sanguinar") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderCastFearWardOnSanguinarTankAction : public Action +{ +public: + KaelthasSunstriderCastFearWardOnSanguinarTankAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider cast fear ward on sanguinar tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderWarlockTankPositionCapernianAction : public AttackAction +{ +public: + KaelthasSunstriderWarlockTankPositionCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider warlock tank position capernian") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread and move away from capernian") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool RangedBotsDisperse(boss_kaelthas* kaelAI); + bool StayBackFromCapernian(); +}; + +class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAction +{ +public: + KaelthasSunstriderFirstAssistTankPositionTelonicusAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider first assist tank position telonicus") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandleAdvisorRolesInPhase3Action : public MovementAction +{ +public: + KaelthasSunstriderHandleAdvisorRolesInPhase3Action( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle advisor roles in phase 3") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignAdvisorDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignAdvisorDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign advisor dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderManageAdvisorDpsTimerAction : public Action +{ +public: + KaelthasSunstriderManageAdvisorDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage advisor dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign legendary weapon dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMoveDevastationAwayAction : public AttackAction +{ +public: + KaelthasSunstriderMoveDevastationAwayAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider move devastation away") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction +{ +public: + KaelthasSunstriderLootLegendaryWeaponsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider loot legendary weapons") {} + bool Execute(Event event) override; + +private: + bool ShouldBotLootWeapon(uint32 weaponEntry); + bool LootWeapon(uint32 weaponEntry, uint32 itemId); +}; + +class KaelthasSunstriderUseLegendaryWeaponsAction : public Action +{ +public: + KaelthasSunstriderUseLegendaryWeaponsAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider use legendary weapons") {} + bool Execute(Event event) override; + +private: + bool UsePhaseshiftBulwark(); + bool UseStaffOfDisintegration(); + bool UseNetherstrandLongbow(); + bool UseEquippedItemWithPacket(Item* item); +}; + +class KaelthasSunstriderReequipGearAction : public Action +{ +public: + KaelthasSunstriderReequipGearAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider reequip gear") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionBossAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAvoidFlameStrikeAction : public MovementAction +{ +public: + KaelthasSunstriderAvoidFlameStrikeAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider avoid flame strike") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandlePhoenixesAndEggsAction : public AttackAction +{ +public: + KaelthasSunstriderHandlePhoenixesAndEggsAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle phoenixes and eggs") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakMindControlAction : public AttackAction +{ +public: + KaelthasSunstriderBreakMindControlAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakThroughShockBarrierAction : public AttackAction +{ +public: + KaelthasSunstriderBreakThroughShockBarrierAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break through shock barrier") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadOutInMidairAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadOutInMidairAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread out in midair") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp new file mode 100644 index 00000000000..b634cbfc9b9 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.cpp @@ -0,0 +1,424 @@ +#include "RaidTempestKeepMultipliers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "EquipAction.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +// Al'ar + +float AlarMoveBetweenPlatformsMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsDps(bot)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float AlarDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (bot->GetVictim() != nullptr) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float AlarStayAwayFromRebirthMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + { + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_MELT_ARMOR)) + return 1.0f; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (alar && bot->GetTarget() == alar->GetGUID()) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Void Reaver + +float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "void reaver")) + return 1.0f; + + if (botAI->IsTank(bot)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsRanged(bot)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// High Astromancer Solarian + +float HighAstromancerSolarianMaintainPositionMultiplier::GetValue(Action* action) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer || astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) + return 1.0f; + + if (botAI->IsRanged(bot)) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + + return 1.0f; +} + +float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "solarium priest")) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Kael'thas Sunstrider + +float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 10; + + auto it = advisorDpsWaitTimer.find(kaelthas->GetMap()->GetInstanceId()); + if (it == advisorDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + auto isAdvisorActive = [](Unit* advisor) + { + return advisor && !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + }; + + if ((isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(bot) != bot)) + { + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + } + } + + return 1.0f; +} + +float KaelthasSunstriderKiteThaladredMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + return 1.0f; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (thaladred->GetVictim() == bot) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (kaelAI && kaelAI->GetPhase() != PHASE_FINAL) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderManageTankTargetingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + // Try to keep main tank from grabbing aggro on any weapon other than the axe + if (kaelAI->GetPhase() == PHASE_WEAPONS) + { + if (botAI->IsMainTank(bot)) + { + 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)) + return 0.0f; + } + } + else + { + if (dynamic_cast(action)) + return 0.0f; + } + + if (bot->GetVictim() == nullptr) + return 1.0f; + else if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bloodlust/Heroism and other major cooldowns should be used at the start of Phase 3 +float KaelthasSunstriderDelayCooldownsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_ALL_ADVISORS && kaelAI->GetPhase() != PHASE_FINAL) + { + if (bot->getClass() == CLASS_SHAMAN) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsDps(bot)) + { + 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 KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier::GetValue(Action* action) +{ + if (!botAI->IsDps(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas || !kaelthas->HasUnitState(UNIT_STATE_CASTING)) + return 1.0f; + + if (kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier::GetValue(Action* action) +{ + if (bot->HasAura(SPELL_GRAVITY_LAPSE)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} diff --git a/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h new file mode 100644 index 00000000000..ab0467db592 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Multiplier/RaidTempestKeepMultipliers.h @@ -0,0 +1,142 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPMULTIPLIERS_H + +#include "Multiplier.h" + +// Al'ar + +class AlarMoveBetweenPlatformsMultiplier : public Multiplier +{ +public: + AlarMoveBetweenPlatformsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar move between platforms multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableDisperseMultiplier : public Multiplier +{ +public: + AlarDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableTankAssistMultiplier : public Multiplier +{ +public: + AlarDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarStayAwayFromRebirthMultiplier : public Multiplier +{ +public: + AlarStayAwayFromRebirthMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar stay away from rebirth multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarPhase2NoTankingIfArmorMeltedMultiplier : public Multiplier +{ +public: + AlarPhase2NoTankingIfArmorMeltedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar phase 2 no tanking if armor melted multiplier") {} + virtual float GetValue(Action* action); +}; + +// Void Reaver + +class VoidReaverMaintainPositionsMultiplier : public Multiplier +{ +public: + VoidReaverMaintainPositionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "void reaver maintain positions multiplier") {} + virtual float GetValue(Action* action); +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianDisableTankAssistMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighAstromancerSolarianMaintainPositionMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian maintain position multiplier") {} + virtual float GetValue(Action* action); +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderKiteThaladredMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKiteThaladredMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider kite thaladred multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderWaitForDpsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderControlMisdirectionMultiplier : public Multiplier +{ +public: + KaelthasSunstriderControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderManageTankTargetingMultiplier : public Multiplier +{ +public: + KaelthasSunstriderManageTankTargetingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider manage tank targeting multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableDisperseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDelayCooldownsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier : public Multiplier +{ +public: + KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider all dps on boss during pyroblast multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider stay spread during gravity lapse multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h new file mode 100644 index 00000000000..f20bd9f65c6 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h @@ -0,0 +1,291 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPACTIONCONTEXT_H + +#include "RaidTempestKeepActions.h" +#include "NamedObjectContext.h" + +class RaidTempestKeepActionContext : public NamedObjectContext +{ +public: + RaidTempestKeepActionContext() + { + // General + creators["tempest keep erase timers and trackers"] = + &RaidTempestKeepActionContext::tempest_keep_erase_timers_and_trackers; + + // Trash + creators["crimson hand centurion cast polymorph"] = + &RaidTempestKeepActionContext::crimson_hand_centurion_cast_polymorph; + + // Al'ar + creators["al'ar misdirect boss to main tank"] = + &RaidTempestKeepActionContext::alar_misdirect_boss_to_main_tank; + + creators["al'ar boss tanks move between platforms"] = + &RaidTempestKeepActionContext::alar_boss_tanks_move_between_platforms; + + creators["al'ar melee dps move between platforms"] = + &RaidTempestKeepActionContext::alar_melee_dps_move_between_platforms; + + creators["al'ar ranged and ember tank move under platforms"] = + &RaidTempestKeepActionContext::alar_ranged_and_ember_tank_move_under_platforms; + + creators["al'ar assist tanks pick up embers"] = + &RaidTempestKeepActionContext::alar_assist_tanks_pick_up_embers; + + creators["al'ar ranged dps prioritize embers"] = + &RaidTempestKeepActionContext::alar_ranged_dps_prioritize_embers; + + creators["al'ar jump from platform"] = + &RaidTempestKeepActionContext::alar_jump_from_platform; + + creators["al'ar move away from rebirth"] = + &RaidTempestKeepActionContext::alar_move_away_from_rebirth; + + creators["al'ar swap tanks on boss"] = + &RaidTempestKeepActionContext::alar_swap_tanks_on_boss; + + creators["al'ar avoid flame patches and dive bombs"] = + &RaidTempestKeepActionContext::alar_avoid_flame_patches_and_dive_bombs; + + creators["al'ar return to room center"] = + &RaidTempestKeepActionContext::alar_return_to_room_center; + + creators["al'ar manage phase tracker"] = + &RaidTempestKeepActionContext::alar_manage_phase_tracker; + + // Void Reaver + creators["void reaver tanks position boss"] = + &RaidTempestKeepActionContext::void_reaver_tanks_position_boss; + + creators["void reaver ranged use aggro dump ability"] = + &RaidTempestKeepActionContext::void_reaver_ranged_use_aggro_dump_ability; + + creators["void reaver spread ranged"] = + &RaidTempestKeepActionContext::void_reaver_spread_ranged; + + // High Astromancer Solarian + creators["high astromancer solarian ranged leave space for melee"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_ranged_leave_space_for_melee; + + creators["high astromancer solarian move away from group"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_move_away_from_group; + + creators["high astromancer solarian stack for aoe"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_stack_for_aoe; + + creators["high astromancer solarian target solarium priests"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_target_solarium_priests; + + creators["high astromancer solarian tank voidwalker"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_tank_voidwalker; + + creators["high astromancer solarian cast fear ward on main tank"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_cast_fear_ward_on_main_tank; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider kite thaladred"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_kite_thaladred; + + creators["kael'thas sunstrider misdirect advisors to tanks"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_misdirect_advisors_to_tanks; + + creators["kael'thas sunstrider main tank position sanguinar"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_sanguinar; + + creators["kael'thas sunstrider cast fear ward on sanguinar tank"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank; + + creators["kael'thas sunstrider warlock tank position capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_warlock_tank_position_capernian; + + creators["kael'thas sunstrider spread and move away from capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_and_move_away_from_capernian; + + creators["kael'thas sunstrider first assist tank position telonicus"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; + + creators["kael'thas sunstrider handle advisor roles in phase 3"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_advisor_roles_in_phase_3; + + creators["kael'thas sunstrider assign advisor dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; + + creators["kael'thas sunstrider manage advisor dps timer"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_advisor_dps_timer; + + creators["kael'thas sunstrider assign legendary weapon dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_legendary_weapon_dps_priority; + + creators["kael'thas sunstrider main tank move devastation away"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_move_devastation_away; + + creators["kael'thas sunstrider loot legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; + + creators["kael'thas sunstrider use legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_use_legendary_weapons; + + creators["kael'thas sunstrider reequip gear"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_reequip_gear; + + creators["kael'thas sunstrider main tank position boss"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_boss; + + creators["kael'thas sunstrider avoid flame strike"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_avoid_flame_strike; + + creators["kael'thas sunstrider handle phoenixes and eggs"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_phoenixes_and_eggs; + + creators["kael'thas sunstrider break mind control"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control; + + creators["kael'thas sunstrider break through shock barrier"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_through_shock_barrier; + + creators["kael'thas sunstrider spread out in midair"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_out_in_midair; + } + +private: + // General + static Action* tempest_keep_erase_timers_and_trackers( + PlayerbotAI* botAI) { return new TempestKeepEraseTimersAndTrackersAction(botAI); } + + // Trash + static Action* crimson_hand_centurion_cast_polymorph( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastPolymorphAction(botAI); } + + // Al'ar + static Action* alar_misdirect_boss_to_main_tank( + PlayerbotAI* botAI) { return new AlarMisdirectBossToMainTankAction(botAI); } + + static Action* alar_boss_tanks_move_between_platforms( + PlayerbotAI* botAI) { return new AlarBossTanksMoveBetweenPlatformsAction(botAI); } + + static Action* alar_melee_dps_move_between_platforms( + PlayerbotAI* botAI) { return new AlarMeleeDpsMoveBetweenPlatformsAction(botAI); } + + static Action* alar_ranged_and_ember_tank_move_under_platforms( + PlayerbotAI* botAI) { return new AlarRangedAndEmberTankMoveUnderPlatformsAction(botAI); } + + static Action* alar_assist_tanks_pick_up_embers( + PlayerbotAI* botAI) { return new AlarAssistTanksPickUpEmbersAction(botAI); } + + static Action* alar_ranged_dps_prioritize_embers( + PlayerbotAI* botAI) { return new AlarRangedDpsPrioritizeEmbersAction(botAI); } + + static Action* alar_jump_from_platform( + PlayerbotAI* botAI) { return new AlarJumpFromPlatformAction(botAI); } + + static Action* alar_move_away_from_rebirth( + PlayerbotAI* botAI) { return new AlarMoveAwayFromRebirthAction(botAI); } + + static Action* alar_swap_tanks_on_boss( + PlayerbotAI* botAI) { return new AlarSwapTanksOnBossAction(botAI); } + + static Action* alar_avoid_flame_patches_and_dive_bombs( + PlayerbotAI* botAI) { return new AlarAvoidFlamePatchesAndDiveBombsAction(botAI); } + + static Action* alar_return_to_room_center( + PlayerbotAI* botAI) { return new AlarReturnToRoomCenterAction(botAI); } + + static Action* alar_manage_phase_tracker( + PlayerbotAI* botAI) { return new AlarManagePhaseTrackerAction(botAI); } + + // Void Reaver + static Action* void_reaver_tanks_position_boss( + PlayerbotAI* botAI) { return new VoidReaverTanksPositionBossAction(botAI); } + + static Action* void_reaver_ranged_use_aggro_dump_ability( + PlayerbotAI* botAI) { return new VoidReaverRangedUseAggroDumpAbilityAction(botAI); } + + static Action* void_reaver_spread_ranged( + PlayerbotAI* botAI) { return new VoidReaverSpreadRangedAction(botAI); } + + // High Astromancer Solarian + static Action* high_astromancer_solarian_ranged_leave_space_for_melee( + PlayerbotAI* botAI) { return new HighAstromancerSolarianRangedLeaveSpaceForMeleeAction(botAI); } + + static Action* high_astromancer_solarian_move_away_from_group( + PlayerbotAI* botAI) { return new HighAstromancerSolarianMoveAwayFromGroupAction(botAI); } + + static Action* high_astromancer_solarian_stack_for_aoe( + PlayerbotAI* botAI) { return new HighAstromancerSolarianStackForAoeAction(botAI); } + + static Action* high_astromancer_solarian_target_solarium_priests( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTargetSolariumPriestsAction(botAI); } + + static Action* high_astromancer_solarian_tank_voidwalker( + PlayerbotAI* botAI) { return new HighAstromancerSolarianTankVoidwalkerAction(botAI); } + + static Action* high_astromancer_solarian_cast_fear_ward_on_main_tank( + PlayerbotAI* botAI) { return new HighAstromancerSolarianCastFearWardOnMainTankAction(botAI); } + + // Kael'thas Sunstrider + static Action* kaelthas_sunstrider_kite_thaladred( + PlayerbotAI* botAI) { return new KaelthasSunstriderKiteThaladredAction(botAI); } + + static Action* kaelthas_sunstrider_misdirect_advisors_to_tanks( + PlayerbotAI* botAI) { return new KaelthasSunstriderMisdirectAdvisorsToTanksAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_sanguinar( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionSanguinarAction(botAI); } + + static Action* kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderCastFearWardOnSanguinarTankAction(botAI); } + + static Action* kaelthas_sunstrider_warlock_tank_position_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderWarlockTankPositionCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_spread_and_move_away_from_capernian( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction(botAI); } + + static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus( + PlayerbotAI* botAI) { return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); } + + static Action* kaelthas_sunstrider_handle_advisor_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandleAdvisorRolesInPhase3Action(botAI); } + + static Action* kaelthas_sunstrider_assign_advisor_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_manage_advisor_dps_timer( + PlayerbotAI* botAI) { return new KaelthasSunstriderManageAdvisorDpsTimerAction(botAI); } + + static Action* kaelthas_sunstrider_assign_legendary_weapon_dps_priority( + PlayerbotAI* botAI) { return new KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_move_devastation_away( + PlayerbotAI* botAI) { return new KaelthasSunstriderMoveDevastationAwayAction(botAI); } + + static Action* kaelthas_sunstrider_loot_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderLootLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_use_legendary_weapons( + PlayerbotAI* botAI) { return new KaelthasSunstriderUseLegendaryWeaponsAction(botAI); } + + static Action* kaelthas_sunstrider_reequip_gear( + PlayerbotAI* botAI) { return new KaelthasSunstriderReequipGearAction(botAI); } + + static Action* kaelthas_sunstrider_main_tank_position_boss( + PlayerbotAI* botAI) { return new KaelthasSunstriderMainTankPositionBossAction(botAI); } + + static Action* kaelthas_sunstrider_avoid_flame_strike( + PlayerbotAI* botAI) { return new KaelthasSunstriderAvoidFlameStrikeAction(botAI); } + + static Action* kaelthas_sunstrider_handle_phoenixes_and_eggs( + PlayerbotAI* botAI) { return new KaelthasSunstriderHandlePhoenixesAndEggsAction(botAI); } + + static Action* kaelthas_sunstrider_break_mind_control( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakMindControlAction(botAI); } + + static Action* kaelthas_sunstrider_break_through_shock_barrier( + PlayerbotAI* botAI) { return new KaelthasSunstriderBreakThroughShockBarrierAction(botAI); } + + static Action* kaelthas_sunstrider_spread_out_in_midair( + PlayerbotAI* botAI) { return new KaelthasSunstriderSpreadOutInMidairAction(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h new file mode 100644 index 00000000000..7fc85fe3b08 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.h @@ -0,0 +1,267 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H + +#include "RaidTempestKeepTriggers.h" +#include "AiObjectContext.h" + +class RaidTempestKeepTriggerContext : public NamedObjectContext +{ +public: + RaidTempestKeepTriggerContext() + { + // General + creators["tempest keep bot is not in combat"] = + &RaidTempestKeepTriggerContext::tempest_keep_bot_is_not_in_combat; + + // Trash + creators["crimson hand centurion casts arcane volley"] = + &RaidTempestKeepTriggerContext::crimson_hand_centurion_casts_arcane_volley; + + // Al'ar + creators["al'ar pulling boss"] = + &RaidTempestKeepTriggerContext::alar_pulling_boss; + + creators["al'ar boss is flying between platforms"] = + &RaidTempestKeepTriggerContext::alar_boss_is_flying_between_platforms; + + creators["al'ar embers of al'ar explode upon death"] = + &RaidTempestKeepTriggerContext::alar_embers_of_alar_explode_upon_death; + + creators["al'ar killing embers of al'ar damages boss"] = + &RaidTempestKeepTriggerContext::alar_killing_embers_of_alar_damages_boss; + + creators["al'ar incoming flame quills"] = + &RaidTempestKeepTriggerContext::alar_incoming_flame_quills; + + creators["al'ar rising from the ashes"] = + &RaidTempestKeepTriggerContext::alar_rising_from_the_ashes; + + creators["al'ar everything is on fire in phase 2"] = + &RaidTempestKeepTriggerContext::alar_everything_is_on_fire_in_phase_2; + + creators["al'ar phase 2 encounter is at room center"] = + &RaidTempestKeepTriggerContext::alar_phase_2_encounter_is_at_room_center; + + creators["al'ar strategy changes between phases"] = + &RaidTempestKeepTriggerContext::alar_strategy_changes_between_phases; + + // Void Reaver + creators["void reaver boss casts pounding"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_casts_pounding; + + creators["void reaver knock away reduces tank aggro"] = + &RaidTempestKeepTriggerContext::void_reaver_knock_away_reduces_tank_aggro; + + creators["void reaver boss launches arcane orbs"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_launches_arcane_orbs; + + // High Astromancer Solarian + creators["high astromancer solarian boss casts wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_wrath_of_the_astromancer; + + creators["high astromancer solarian bot has wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_bot_has_wrath_of_the_astromancer; + + creators["high astromancer solarian boss has vanished"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_has_vanished; + + creators["high astromancer solarian solarium priests spawned"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_solarium_priests_spawned; + + creators["high astromancer solarian boss transformed into voidwalker"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_transformed_into_voidwalker; + + creators["high astromancer solarian boss casts psychic scream"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider thaladred is fixated on bot"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_thaladred_is_fixated_on_bot; + + creators["kael'thas sunstrider pulling tankable advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_pulling_tankable_advisors; + + creators["kael'thas sunstrider sanguinar engaged by main tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_engaged_by_main_tank; + + creators["kael'thas sunstrider sanguinar casts bellowing roar"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_casts_bellowing_roar; + + creators["kael'thas sunstrider capernian should be tanked by a warlock"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock; + + creators["kael'thas sunstrider capernian casts arcane burst and conflagration"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration; + + creators["kael'thas sunstrider telonicus engaged by first assist tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; + + creators["kael'thas sunstrider bots have specific roles in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_bots_have_specific_roles_in_phase_3; + + creators["kael'thas sunstrider determining advisor kill order"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; + + creators["kael'thas sunstrider waiting for tanks to get aggro on advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors; + + creators["kael'thas sunstrider legendary weapons are alive"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_alive; + + creators["kael'thas sunstrider legendary axe casts whirlwind"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_axe_casts_whirlwind; + + creators["kael'thas sunstrider legendary weapons are dead and lootable"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; + + creators["kael'thas sunstrider legendary weapons are equipped"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_equipped; + + creators["kael'thas sunstrider legendary weapons were lost"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_were_lost; + + creators["kael'thas sunstrider boss has entered the fight"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_has_entered_the_fight; + + creators["kael'thas sunstrider raid member is mind controlled"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_raid_member_is_mind_controlled; + + creators["kael'thas sunstrider phoenixes and eggs are spawning"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_phoenixes_and_eggs_are_spawning; + + creators["kael'thas sunstrider boss is casting pyroblast"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_casting_pyroblast; + + creators["kael'thas sunstrider boss is manipulating gravity"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_manipulating_gravity; + } + +private: + // General + static Trigger* tempest_keep_bot_is_not_in_combat( + PlayerbotAI* botAI) { return new TempestKeepBotIsNotInCombatTrigger(botAI); } + + // Trash + static Trigger* crimson_hand_centurion_casts_arcane_volley( + PlayerbotAI* botAI) { return new CrimsonHandCenturionCastsArcaneVolleyTrigger(botAI); } + + // Al'ar + static Trigger* alar_pulling_boss( + PlayerbotAI* botAI) { return new AlarPullingBossTrigger(botAI); } + + static Trigger* alar_boss_is_flying_between_platforms( + PlayerbotAI* botAI) { return new AlarBossIsFlyingBetweenPlatformsTrigger(botAI); } + + static Trigger* alar_embers_of_alar_explode_upon_death( + PlayerbotAI* botAI) { return new AlarEmbersOfAlarExplodeUponDeathTrigger(botAI); } + + static Trigger* alar_killing_embers_of_alar_damages_boss( + PlayerbotAI* botAI) { return new AlarKillingEmbersOfAlarDamagesBossTrigger(botAI); } + + static Trigger* alar_incoming_flame_quills( + PlayerbotAI* botAI) { return new AlarIncomingFlameQuillsTrigger(botAI); } + + static Trigger* alar_rising_from_the_ashes( + PlayerbotAI* botAI) { return new AlarRisingFromTheAshesTrigger(botAI); } + + static Trigger* alar_everything_is_on_fire_in_phase_2( + PlayerbotAI* botAI) { return new AlarEverythingIsOnFireInPhase2Trigger(botAI); } + + static Trigger* alar_phase_2_encounter_is_at_room_center( + PlayerbotAI* botAI) { return new AlarPhase2EncounterIsAtRoomCenterTrigger(botAI); } + + static Trigger* alar_strategy_changes_between_phases( + PlayerbotAI* botAI) { return new AlarStrategyChangesBetweenPhasesTrigger(botAI); } + + // Void Reaver + static Trigger* void_reaver_boss_casts_pounding( + PlayerbotAI* botAI) { return new VoidReaverBossCastsPoundingTrigger(botAI); } + + static Trigger* void_reaver_knock_away_reduces_tank_aggro( + PlayerbotAI* botAI) { return new VoidReaverKnockAwayReducesTankAggroTrigger(botAI); } + + static Trigger* void_reaver_boss_launches_arcane_orbs( + PlayerbotAI* botAI) { return new VoidReaverBossLaunchesArcaneOrbsTrigger(botAI); } + + // High Astromancer Solarian + static Trigger* high_astromancer_solarian_boss_casts_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_bot_has_wrath_of_the_astromancer( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_has_vanished( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossHasVanishedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_solarium_priests_spawned( + PlayerbotAI* botAI) { return new HighAstromancerSolarianSolariumPriestsSpawnedTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_transformed_into_voidwalker( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger(botAI); } + + static Trigger* high_astromancer_solarian_boss_casts_psychic_scream( + PlayerbotAI* botAI) { return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); } + + // Kael'thas Sunstrider + static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot( + PlayerbotAI* botAI) { return new KaelthasSunstriderThaladredIsFixatedOnBotTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_pulling_tankable_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderPullingTankableAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_engaged_by_main_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarEngagedByMainTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_sanguinar_casts_bellowing_roar( + PlayerbotAI* botAI) { return new KaelthasSunstriderSanguinarCastsBellowingRoarTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration( + PlayerbotAI* botAI) { return new KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank( + PlayerbotAI* botAI) { return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_bots_have_specific_roles_in_phase_3( + PlayerbotAI* botAI) { return new KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger(botAI); } + + static Trigger* kaelthas_sunstrider_determining_advisor_kill_order( + PlayerbotAI* botAI) { return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors( + PlayerbotAI* botAI) { return new KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_alive( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreAliveTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_axe_casts_whirlwind( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_are_equipped( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_legendary_weapons_were_lost( + PlayerbotAI* botAI) { return new KaelthasSunstriderLegendaryWeaponsWereLostTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_has_entered_the_fight( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossHasEnteredTheFightTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_raid_member_is_mind_controlled( + PlayerbotAI* botAI) { return new KaelthasSunstriderRaidMemberIsMindControlledTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_phoenixes_and_eggs_are_spawning( + PlayerbotAI* botAI) { return new KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_casting_pyroblast( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsCastingPyroblastTrigger(botAI); } + + static Trigger* kaelthas_sunstrider_boss_is_manipulating_gravity( + PlayerbotAI* botAI) { return new KaelthasSunstriderBossIsManipulatingGravityTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp new file mode 100644 index 00000000000..65b9e8a9c38 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.cpp @@ -0,0 +1,162 @@ +#include "RaidTempestKeepStrategy.h" +#include "RaidTempestKeepMultipliers.h" + +void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) +{ + // General + triggers.push_back(new TriggerNode("tempest keep bot is not in combat", { + NextAction("tempest keep erase timers and trackers", ACTION_EMERGENCY + 11) })); + + // Trash + triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { + NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1) })); + + // Al'ar + triggers.push_back(new TriggerNode("al'ar pulling boss", { + NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", { + NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), + NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), + NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("al'ar embers of al'ar explode upon death", { + NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("al'ar killing embers of al'ar damages boss", { + NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("al'ar incoming flame quills", { + NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar rising from the ashes", { + NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", { + NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), + NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", { + NextAction("al'ar return to room center", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("al'ar strategy changes between phases", { + NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10) })); + + // Void Reaver + triggers.push_back(new TriggerNode("void reaver boss casts pounding", { + NextAction("void reaver tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", { + NextAction("void reaver ranged use aggro dump ability", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", { + NextAction("void reaver spread ranged", ACTION_RAID + 1) })); + + // High Astromancer Solarian + triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", { + NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", { + NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", { + NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", { + NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss transformed into voidwalker", { + NextAction("high astromancer solarian tank voidwalker", ACTION_EMERGENCY + 1), })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", { + NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2) })); + + // Kael'thas Sunstrider + triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", { + NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", { + NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", { + NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", { + NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", { + NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", { + NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", { + NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider bots have specific roles in phase 3", { + NextAction("kael'thas sunstrider handle advisor roles in phase 3", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", { + NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", { + NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", { + NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { + NextAction("kael'thas sunstrider main tank move devastation away", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { + NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", { + NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", { + NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", { + NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), + NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", { + NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", { + NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", { + NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", { + NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1) })); +} + +void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) +{ + // Alar + multipliers.push_back(new AlarMoveBetweenPlatformsMultiplier(botAI)); + multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); + multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); + multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); + + // Void Reaver + multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); + + // High Astromancer Solarian + multipliers.push_back(new HighAstromancerSolarianDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new HighAstromancerSolarianMaintainPositionMultiplier(botAI)); + + // Kael'thas Sunstrider + multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderManageTankTargetingMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderAllDpsOnBossDuringPyroblastMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); +} diff --git a/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h new file mode 100644 index 00000000000..77fd29c360a --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Strategy/RaidTempestKeepStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_ + +#include "Strategy.h" +#include "Multiplier.h" + +class RaidTempestKeepStrategy : public Strategy +{ +public: + RaidTempestKeepStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "tempestkeep"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp new file mode 100644 index 00000000000..39103ab88bc --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.cpp @@ -0,0 +1,505 @@ +#include "RaidTempestKeepTriggers.h" +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "RaidTempestKeepKaelthasBossAI.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// General + +bool TempestKeepBotIsNotInCombatTrigger::IsActive() +{ + return !bot->IsInCombat(); +} + +// Trash + +bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() +{ + return bot->getClass() == CLASS_MAGE && + AI_VALUE2(Unit*, "find target", "crimson hand centurion"); +} + +// Al'ar + +bool AlarPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && alar->GetHealthPct() > 98.0f; +} + +bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !alar->IsVisible()) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + return locationIndex != POINT_QUILL_OR_DIVE_IDX && + locationIndex != POINT_MIDDLE_IDX; +} + +bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "ember of alar"); +} + +bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() +{ + return botAI->IsRangedDps(bot) && + AI_VALUE2(Unit*, "find target", "ember of alar"); +} + +bool AlarIncomingFlameQuillsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarRisingFromTheAshesTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + return alarCreature && alarCreature->GetReactState() == REACT_PASSIVE; +} + +bool AlarEverythingIsOnFireInPhase2Trigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && isAlarInPhase2[alar->GetMap()->GetInstanceId()]; +} + +bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() +{ + if (bot->GetVictim()) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) != POINT_QUILL_OR_DIVE_IDX && + GetAlarDestinationLocationIndex(alar, dest) != POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() +{ + return IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, nullptr) && + AI_VALUE2(Unit*, "find target", "al'ar"); +} + +// Void Reaver + +bool VoidReaverBossCastsPoundingTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "void reaver"); +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + if (!botAI->IsRanged(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger::IsActive() +{ + return bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER); +} + +bool HighAstromancerSolarianBossHasVanishedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + Creature* astromancerCreature = astromancer->ToCreature(); + return astromancerCreature && + astromancerCreature->GetReactState() == REACT_PASSIVE; +} + +bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() +{ + return botAI->IsMelee(bot) && + AI_VALUE2(Unit*, "find target", "solarium priest"); +} + +bool HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + return !(botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* mainTank = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + + return mainTank && !mainTank->HasAura(SPELL_FEAR_WARD) && + botAI->CanCastSpell("fear ward", mainTank); +} + +bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + if (GetCapernianTank(bot) != bot) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() +{ + if (GetCapernianTank(bot) == bot) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + return capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!AI_VALUE2(Unit*, "find target", "master engineer telonicus") && + !AI_VALUE2(Unit*, "find target", "lord sanguinar")) + return false; + + if (!botAI->IsAssistHealOfIndex(bot, 0, true) && + !botAI->IsMainTank(bot) && + !botAI->IsAssistTankOfIndex(bot, 0, true) && + GetCapernianTank(bot) != bot) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_ALL_ADVISORS; +} + +bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!IsMechanicTrackerBot(botAI, bot, TEMPEST_KEEP_MAP_ID, GetCapernianTank(bot))) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR; +} + +bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsMainTank(bot)) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_WEAPONS; +} + +bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "devastation") && + botAI->IsMainTank(bot); +} + +bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "kael'thas sunstrider") && + IsAnyLegendaryWeaponDead(botAI, bot); +} + +bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return false; + + return bot->HasItemCount(ITEM_STAFF_OF_DISINTEGRATION, 1, false) || + bot->HasItemCount(ITEM_NETHERSTRAND_LONGBOW, 1, false) || + bot->HasItemCount(ITEM_PHASESHIFT_BULWARK, 1, false); +} + +bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() +{ + if (bot->GetMapId() != TEMPEST_KEEP_MAP_ID) + return false; + + constexpr uint32 KAELTHAS_DB_GUID = 158218; + Map* map = bot->GetMap(); + if (!map) + return false; + + auto it = map->GetCreatureBySpawnIdStore().find(KAELTHAS_DB_GUID); + if (it == map->GetCreatureBySpawnIdStore().end()) + return false; + + Creature* kaelthas = it->second; + if (!kaelthas || bot->GetExactDist2d(kaelthas) > 150.0f) + return false; + + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + bool has2HWeapon = mainHand && mainHand->GetTemplate()->InventoryType == INVTYPE_2HWEAPON; + + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + if (slot == EQUIPMENT_SLOT_BODY || slot == EQUIPMENT_SLOT_TABARD) + continue; + + if (slot == EQUIPMENT_SLOT_OFFHAND && has2HWeapon) + continue; + + if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + { + if (slot == EQUIPMENT_SLOT_OFFHAND) + { + if (!mainHand || mainHand->GetTemplate()->InventoryType != INVTYPE_WEAPON) + continue; + if (HasEquippableOffhand(bot)) + return true; + } + else if (HasEquippableItemForSlot(bot, slot)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_FINAL; +} + +bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "phoenix") || + AI_VALUE2(Unit*, "find target", "phoenix egg"); +} + +bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossIsCastingPyroblastTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas && kaelthas->HasAura(SPELL_SHOCK_BARRIER); +} + +bool KaelthasSunstriderBossIsManipulatingGravityTrigger::IsActive() +{ + return bot->HasAura(SPELL_GRAVITY_LAPSE); +} diff --git a/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h new file mode 100644 index 00000000000..d3ac3664ebb --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Trigger/RaidTempestKeepTriggers.h @@ -0,0 +1,338 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H +#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERS_H + +#include "Trigger.h" + +// General + +class TempestKeepBotIsNotInCombatTrigger : public Trigger +{ +public: + TempestKeepBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "tempest keep bot is not in combat") {} + bool IsActive() override; +}; + +// Trash + +class CrimsonHandCenturionCastsArcaneVolleyTrigger : public Trigger +{ +public: + CrimsonHandCenturionCastsArcaneVolleyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "crimson hand centurion casts arcane volley") {} + bool IsActive() override; +}; + +// Al'ar + +class AlarPullingBossTrigger : public Trigger +{ +public: + AlarPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar pulling boss") {} + bool IsActive() override; +}; + +class AlarBossIsFlyingBetweenPlatformsTrigger : public Trigger +{ +public: + AlarBossIsFlyingBetweenPlatformsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss is flying between platforms") {} + bool IsActive() override; +}; + +class AlarEmbersOfAlarExplodeUponDeathTrigger : public Trigger +{ +public: + AlarEmbersOfAlarExplodeUponDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar embers of al'ar explode upon death") {} + bool IsActive() override; +}; + +class AlarKillingEmbersOfAlarDamagesBossTrigger : public Trigger +{ +public: + AlarKillingEmbersOfAlarDamagesBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar killing embers of al'ar damages boss") {} + bool IsActive() override; +}; + +class AlarIncomingFlameQuillsTrigger : public Trigger +{ +public: + AlarIncomingFlameQuillsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar incoming flame quills") {} + bool IsActive() override; +}; + +class AlarRisingFromTheAshesTrigger : public Trigger +{ +public: + AlarRisingFromTheAshesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar rising from the ashes") {} + bool IsActive() override; +}; + +class AlarEverythingIsOnFireInPhase2Trigger : public Trigger +{ +public: + AlarEverythingIsOnFireInPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar everything is on fire in phase 2") {} + bool IsActive() override; +}; + +class AlarPhase2EncounterIsAtRoomCenterTrigger : public Trigger +{ +public: + AlarPhase2EncounterIsAtRoomCenterTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar phase 2 encounter is at room center") {} + bool IsActive() override; +}; + +class AlarStrategyChangesBetweenPhasesTrigger : public Trigger +{ +public: + AlarStrategyChangesBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar strategy changes between phases") {} + bool IsActive() override; +}; + +// Void Reaver + +class VoidReaverBossCastsPoundingTrigger : public Trigger +{ +public: + VoidReaverBossCastsPoundingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss casts pounding") {} + bool IsActive() override; +}; + +class VoidReaverKnockAwayReducesTankAggroTrigger : public Trigger +{ +public: + VoidReaverKnockAwayReducesTankAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver knock away reduces tank aggro") {} + bool IsActive() override; +}; + +class VoidReaverBossLaunchesArcaneOrbsTrigger : public Trigger +{ +public: + VoidReaverBossLaunchesArcaneOrbsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss launches arcane orbs") {} + bool IsActive() override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian bot has wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossHasVanishedTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossHasVanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss has vanished") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianSolariumPriestsSpawnedTrigger : public Trigger +{ +public: + HighAstromancerSolarianSolariumPriestsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian solarium priests spawned") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossTransformedIntoVoidwalkerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss transformed into voidwalker") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsPsychicScreamTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer boss casts psychic scream") {} + bool IsActive() override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderThaladredIsFixatedOnBotTrigger : public Trigger +{ +public: + KaelthasSunstriderThaladredIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider thaladred is fixated on bot") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPullingTankableAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderPullingTankableAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider pulling tankable advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarEngagedByMainTankTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar engaged by main tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarCastsBellowingRoarTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarCastsBellowingRoarTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar casts bellowing roar") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian should be tanked by a warlock") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian casts arcane burst and conflagration") {} + bool IsActive() override; +}; + +class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider telonicus engaged by first assist tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger : public Trigger +{ +public: + KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider bots have specific roles in phase 3") {} + bool IsActive() override; +}; + +class KaelthasSunstriderDeterminingAdvisorKillOrderTrigger : public Trigger +{ +public: + KaelthasSunstriderDeterminingAdvisorKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider determining advisor kill order") {} + bool IsActive() override; +}; + +class KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider waiting for tanks to get aggro on advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreAliveTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreAliveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are alive") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary axe casts whirlwind") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are dead and lootable") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are equipped") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsWereLostTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsWereLostTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons were lost") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossHasEnteredTheFightTrigger : public Trigger +{ +public: + KaelthasSunstriderBossHasEnteredTheFightTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss has entered the fight") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger : public Trigger +{ +public: + KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider phoenixes and eggs are spawning") {} + bool IsActive() override; +}; + +class KaelthasSunstriderRaidMemberIsMindControlledTrigger : public Trigger +{ +public: + KaelthasSunstriderRaidMemberIsMindControlledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider raid member is mind controlled") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsCastingPyroblastTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsCastingPyroblastTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is casting pyroblast") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsManipulatingGravityTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsManipulatingGravityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is manipulating gravity") {} + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp new file mode 100644 index 00000000000..5863928526e --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.cpp @@ -0,0 +1,497 @@ +#include "RaidTempestKeepHelpers.h" +#include "RaidTempestKeepActions.h" +#include "LootObjectStack.h" +#include "Playerbots.h" + +namespace TempestKeepHelpers +{ + // General + + Unit* GetNearestNonTankPlayerInRadius(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; + + if (PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member)) + { + if (memberAI->IsTank(member)) + continue; + } + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + } + + return nearestPlayer; + } + + std::vector GetAllHazardTriggers( + PlayerbotAI* botAI, Player* bot, uint32 npcEntry, float maxSearchRadius) + { + std::vector hazardTriggers; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == npcEntry && bot->GetExactDist2d(unit) < maxSearchRadius) + hazardTriggers.push_back(unit); + } + + return hazardTriggers; + } + + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float maxRadius, float hazardRadius, const Position* center) + { + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 30.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) + { + const Position& searchCenter = center ? *center : bot->GetPosition(); + float x = searchCenter.GetPositionX() + distance * std::cos(angle); + float y = searchCenter.GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* hazard : hazards) + { + if (hazard->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, 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 IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, 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* hazard : hazards) + { + float distToHazard = hazard->GetExactDist2d(checkX, checkY); + if (distToHazard < hazardRadius) + return false; + } + } + + return true; + } + + // Al'ar + + const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform + const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform + const Position ALAR_PLATFORM_2 = { 388.791f, -33.1059f, 20.2636f }; // Northeast Platform + const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform + const Position PLATFORM_POSITIONS[] = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3 + }; + const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform + const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform + const Position ALAR_GROUND_2 = { 378.583f, -27.481f, -2.385f }; // Ground counterpart to Northeast Platform + const Position ALAR_GROUND_3 = { 331.631f, -49.716f, -2.389f }; // Ground counterpart to East Platform + const Position GROUND_POSITIONS[] = + { + ALAR_GROUND_0, + ALAR_GROUND_1, + ALAR_GROUND_2, + ALAR_GROUND_3 + }; + const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; + const Position ALAR_POINT_QUILL_OR_DIVE = { 332.000f, 0.010f, 43.000f }; + const Position ALAR_POINT_MIDDLE = { 331.000f, 0.010f, -2.380f }; + const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; + const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; + const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.000f, -2.389f }; + + std::unordered_map lastRebirthState; + std::unordered_map isAlarInPhase2; + + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest) + { + if (!alar) + return LOCATION_NONE; + + float x, y, z; + if (!alar->GetMotionMaster()->GetDestination(x, y, z)) + return LOCATION_NONE; + + dest.Relocate(x, y, z); + + const Position locations[] = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < 7; ++i) + { + float dist = dest.GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + int8 GetAlarCurrentLocationIndex(Unit* alar) + { + if (!alar) + return LOCATION_NONE; + + const Position locations[] = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < 7; ++i) + { + float dist = alar->GetPosition().GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + void GetClosestPlatformAndGround(const Position& botPos, int8& closestPlatform, Position& ground) + { + float minDist = std::numeric_limits::max(); + closestPlatform = -1; + for (int8 i = 0; i < 4; ++i) + { + float dist = botPos.GetExactDist2d(&PLATFORM_POSITIONS[i]); + if (dist < minDist) + { + minDist = dist; + closestPlatform = i; + } + } + ground = GROUND_POSITIONS[closestPlatform]; + } + + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI) + { + Unit* firstEmber = nullptr; + Unit* secondEmber = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_EMBER_OF_ALAR) + { + if (!firstEmber) + firstEmber = unit; + else if (!secondEmber) + { + secondEmber = unit; + break; + } + } + } + + return { firstEmber, secondEmber }; + } + + Player* GetSecondEmberTank(PlayerbotAI* botAI, Unit* alar) + { + Player* mainTank = nullptr; + Player* assistTank = nullptr; + + if (Group* group = botAI->GetBot()->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member) + continue; + + if (botAI->IsMainTank(member)) + mainTank = member; + + if (botAI->IsAssistTankOfIndex(member, 0, false)) + assistTank = member; + } + } + + bool mainTankHasMelt = mainTank && mainTank->HasAura(SPELL_MELT_ARMOR); + bool assistTankHasMelt = assistTank && assistTank->HasAura(SPELL_MELT_ARMOR); + + if (!mainTankHasMelt && !assistTankHasMelt) + return assistTank; + + if (mainTankHasMelt) + return mainTank; + + if (assistTankHasMelt) + return assistTank; + + return nullptr; + } + + // Void Reaver + + const Position VOID_REAVER_TANK_POSITION = { 423.845f, 371.733f, 14.897f }; + + std::unordered_map initialVoidReaverPositions; + std::unordered_map hasReachedInitialVoidReaverPosition; + + // High Astromancer Solarian + // N/A + + // Kael'thas Sunstrider + + const Position SANGUINAR_TANK_POSITION = { 775.478f, 39.888f, 46.780f }; + const Position SANGUINAR_WAITING_POSITION = { 761.850f, 27.459f, 46.779f }; + const Position TELONICUS_TANK_POSITION = { 773.717f, 44.091f, 46.780f }; + const Position TELONICUS_WAITING_POSITION = { 754.347f, 31.739f, 46.796f }; + const Position ADVISOR_HEAL_POSITION = { 757.425f, 13.011f, 46.779f }; + const Position CAPERNIAN_WAITING_POSITION = { 743.897f, -11.575f, 46.779f }; + const Position KAELTHAS_TANK_POSITION = { 799.390f, -0.837f, 48.729f }; + + std::unordered_map advisorDpsWaitTimer; + + Player* GetCapernianTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) Look for an 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; + } + + // One Hunter will start on Sanguinar in Phase 3 with Melee to apply Armor Disruption + Player* GetDebuffHunter(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) Look for an assistant Hunter (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_HUNTER) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + } + + // (2) Fall back to first found bot Hunter + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_HUNTER) + continue; + + return member; + } + + return nullptr; + } + + bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot) + { + const std::pair weapons[] = + { + { "staff of disintegration", NPC_STAFF_OF_DISINTEGRATION }, + { "cosmic infuser", NPC_COSMIC_INFUSER }, + { "infinity blade", NPC_INFINITY_BLADES }, + { "warp slicer", NPC_WARP_SLICER }, + { "phaseshift bulwark", NPC_PHASESHIFT_BULWARK }, + { "netherstrand longbow", NPC_NETHERSTRAND_LONGBOW }, + { "devastation", NPC_DEVASTATION }, + }; + + for (auto const& [name, entry] : weapons) + { + Unit* weapon = + botAI->GetAiObjectContext()->GetValue("find target", name)->Get(); + if (weapon && weapon->IsAlive()) + continue; + + auto const& corpses = + botAI->GetAiObjectContext()->GetValue("nearest corpses")->Get(); + 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() == entry && !creature->IsAlive()) + return true; + } + } + } + + return false; + } + + bool HasEquippableItemForSlot(Player* bot, uint8 slot) + { + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + for (uint8 bagSlot = 0; bagSlot < MAX_BAG_SIZE; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + uint16 dest = 0; + if (bot->CanEquipItem(slot, dest, item, false)) + return true; + } + } + + return false; + } + + bool HasEquippableOffhand(Player* bot) + { + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + for (uint8 bagSlot = 0; bagSlot < MAX_BAG_SIZE; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + if (proto->InventoryType == INVTYPE_SHIELD || + proto->InventoryType == INVTYPE_WEAPONOFFHAND || + proto->InventoryType == INVTYPE_HOLDABLE) + { + uint16 dest = 0; + if (bot->CanEquipItem(EQUIPMENT_SLOT_OFFHAND, dest, item, false)) + return true; + } + } + } + + return false; + } +} diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h new file mode 100644 index 00000000000..521203cb8ce --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepHelpers.h @@ -0,0 +1,151 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPHELPERS_H_ + +#include +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace TempestKeepHelpers +{ + enum TempestKeepSpells + { + // Trash + SPELL_ARCANE_FLURRY = 37268, + + // Al'ar + SPELL_REBIRTH_PHASE2 = 34342, + SPELL_REBIRTH_DIVE = 35369, + SPELL_MELT_ARMOR = 35410, + + // High Astromancer Solarian + SPELL_SOLARIAN_TRANSFORM = 39117, + SPELL_WRATH_OF_THE_ASTROMANCER = 42783, + + // Kael'thas Sunstrider + SPELL_PERMANENT_FEIGN_DEATH = 29266, + SPELL_GRAVITY_LAPSE = 39432, + SPELL_KAEL_FULL_POWER = 36187, + SPELL_MENTAL_PROTECTION_FIELD = 36480, // Staff of Disintegration + SPELL_ARCANE_BARRIER = 36481, // Phaseshift Bulwark + SPELL_KAELTHAS_MIND_CONTROL = 36797, + SPELL_SHOCK_BARRIER = 36815, + SPELL_STAFF_FROSTBOLT = 36990, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum TempestKeepNPCs + { + // Al'ar + NPC_EMBER_OF_ALAR = 19551, + NPC_FLAME_PATCH = 20602, + + // High Astromancer Solarian + NPC_SOLARIUM_PRIEST = 18806, + + // Kael'thas Sunstrider + NPC_KAELTHAS_SUNSTRIDER = 19622, + NPC_NETHERSTRAND_LONGBOW = 21268, + NPC_DEVASTATION = 21269, + NPC_COSMIC_INFUSER = 21270, + NPC_INFINITY_BLADES = 21271, // Item is singular, but NPC is plural + NPC_WARP_SLICER = 21272, + NPC_PHASESHIFT_BULWARK = 21273, + NPC_STAFF_OF_DISINTEGRATION = 21274, + // NPC_NETHER_VAPOR = 21002, // Unimplemented in AC; method needed if fixed + NPC_PHOENIX = 21362, + NPC_PHOENIX_EGG = 21364, + NPC_FLAME_STRIKE_TRIGGER = 21369, + }; + + enum TempestKeepItems + { + // Kael'thas Sunstrider + ITEM_WARP_SLICER = 30311, + ITEM_INFINITY_BLADE = 30312, + ITEM_STAFF_OF_DISINTEGRATION = 30313, + ITEM_PHASESHIFT_BULWARK = 30314, + ITEM_DEVASTATION = 30316, + ITEM_COSMIC_INFUSER = 30317, + ITEM_NETHERSTRAND_LONGBOW = 30318, + ITEM_NETHER_SPIKES = 30319, + }; + + // General + constexpr uint32 TEMPEST_KEEP_MAP_ID = 550; + Unit* GetNearestNonTankPlayerInRadius(Player* bot, float radius); + std::vector GetAllHazardTriggers( + PlayerbotAI* botAI, Player* bot, uint32 npcEntry, float maxSearchRadius); + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float maxRadius, float hazardRadius, const Position* center = nullptr); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, + float hazardRadius); + + // Al'ar + enum AlarLocationIndex + { + PLATFORM_0_IDX, + PLATFORM_1_IDX, + PLATFORM_2_IDX, + PLATFORM_3_IDX, + POINT_QUILL_OR_DIVE_IDX, + POINT_MIDDLE_IDX, + LOCATION_NONE = -1 + }; + constexpr float ALAR_BALCONY_Z = 17.0f; + extern const Position ALAR_PLATFORM_0; + extern const Position ALAR_PLATFORM_1; + extern const Position ALAR_PLATFORM_2; + extern const Position ALAR_PLATFORM_3; + extern const Position PLATFORM_POSITIONS[4]; + extern const Position ALAR_GROUND_0; + extern const Position ALAR_GROUND_1; + extern const Position ALAR_GROUND_2; + extern const Position ALAR_GROUND_3; + extern const Position GROUND_POSITIONS[4]; + extern const Position ALAR_ROOM_CENTER; + extern const Position ALAR_POINT_QUILL_OR_DIVE; + extern const Position ALAR_POINT_MIDDLE; + extern const Position ALAR_SE_RAMP_BASE; + extern const Position ALAR_SW_RAMP_BASE; + extern const Position ALAR_ROOM_S_CENTER; + extern std::unordered_map lastRebirthState; + extern std::unordered_map isAlarInPhase2; + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest); + int8 GetAlarCurrentLocationIndex(Unit* alar); + void GetClosestPlatformAndGround( + const Position& botPos, int8& closestPlatform, Position& ground); + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI); + Player* GetSecondEmberTank(PlayerbotAI* botAI, Unit* alar); + + // Void Reaver + extern const Position VOID_REAVER_TANK_POSITION; + extern std::unordered_map initialVoidReaverPositions; + extern std::unordered_map hasReachedInitialVoidReaverPosition; + + // Kael'thas Sunstrider + extern const Position SANGUINAR_TANK_POSITION; + extern const Position SANGUINAR_WAITING_POSITION; + extern const Position TELONICUS_TANK_POSITION; + extern const Position TELONICUS_WAITING_POSITION; + extern const Position CAPERNIAN_WAITING_POSITION; + extern const Position ADVISOR_HEAL_POSITION; + extern const Position KAELTHAS_TANK_POSITION; + extern std::unordered_map advisorDpsWaitTimer; + Player* GetCapernianTank(Player* bot); + Player* GetDebuffHunter(Player* bot); + bool IsAnyLegendaryWeaponDead(PlayerbotAI* botAI, Player* bot); + bool HasEquippableItemForSlot(Player* bot, uint8 slot); + bool HasEquippableOffhand(Player* bot); +} + +#endif diff --git a/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h new file mode 100644 index 00000000000..598c80614a5 --- /dev/null +++ b/src/Ai/Raid/TempestKeep/Util/RaidTempestKeepKaelthasBossAI.h @@ -0,0 +1,54 @@ +#ifndef _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ +#define _PLAYERBOT_RAIDTEMPESTKEEPKAELTHASBOSSAI_H_ + +#include "ScriptedCreature.h" + +enum KTYells +{ +}; + +enum KTPhases +{ + PHASE_NONE = 0, + PHASE_SINGLE_ADVISOR = 1, + PHASE_WEAPONS = 2, + PHASE_TRANSITION = 3, + PHASE_ALL_ADVISORS = 4, + PHASE_FINAL = 5 +}; + +enum KTActions +{ +}; + +struct boss_kaelthas : public BossAI +{ + boss_kaelthas(Creature* creature); + + void PrepareAdvisors(); + void SetRoomState(GOState state); + void Reset() override; + void AttackStart(Unit* who) override; + void MoveInLineOfSight(Unit* who) override; + void KilledUnit(Unit* victim) override; + void JustSummoned(Creature* summon) override; + void SpellHit(Unit* caster, SpellInfo const* spell) override; + void MovementInform(uint32 type, uint32 point) override; + void ExecuteMiddleEvent(); + void IntroduceNewAdvisor(KTYells talkIntroduction, KTActions kaelAction); + void PhaseEnchantedWeaponsExecute(); + void PhaseAllAdvisorsExecute(); + void PhaseKaelExecute(); + void UpdateAI(uint32 diff) override; + bool CheckEvadeIfOutOfCombatArea() const override; + void JustDied(Unit* killer) override; + + uint32 GetPhase() const { return _phase; } // This is the only addition to the existing class + +private: + uint32 _phase; + uint8 _advisorsAlive; + bool _transitionSceneReached = false; +}; + +#endif diff --git a/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp b/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp index b20425d224e..f7eca38db1e 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 0a51ca407b7..00000000000 --- 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 6c9a468fcd6..00000000000 --- 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 592fbc80eaa..00000000000 --- 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 3b9a426cc67..0a1b76a40f1 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 81bb93c3a59..bb2feefe4af 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 a4bc2cf9a27..f14a513118c 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 129c3d4dbae..7f8cb51a83d 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 72333a079b3..fd6711cf0fc 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 00000000000..b3d49ff58cb --- /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 b304826b8ff..263d03d9b75 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 9fe078f809b..6566793fde1 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 b6d5f7de0dc..ff54b9710ec 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" @@ -39,16 +37,22 @@ #include "Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h" #include "Ai/Raid/Karazhan/RaidKarazhanActionContext.h" #include "Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" #include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" +#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" +#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepActionContext.h" +#include "Ai/Raid/TempestKeep/RaidTempestKeepTriggerContext.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" @@ -113,8 +117,10 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList PlayerbotAI::GetPlayersInGroup() +std::vector PlayerbotAI::GetRealPlayersInGroup() { std::vector members; @@ -2607,6 +2614,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 +2746,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 +2763,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 c2d4aeb75b0..3364b31ac5a 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -446,7 +446,8 @@ class PlayerbotAI : public PlayerbotAIBase 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 5c0e8c94ab2..1053e542e61 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 84eef9b99ea..748f929aa70 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -881,39 +881,63 @@ std::vector WorldPosition::getGameObjectsNear(float radiu return worker.GetResult(); } -Creature* GuidPosition::GetCreature() +CreatureTemplate const* GuidPosition::GetCreatureTemplate() +{ + return IsCreature() ? sObjectMgr->GetCreatureTemplate(GetEntry()) : nullptr; +} + +GameObjectTemplate const* GuidPosition::GetGameObjectTemplate() +{ + return IsGameObject() ? sObjectMgr->GetGameObjectTemplate(GetEntry()) : nullptr; +} + +WorldObject* GuidPosition::GetWorldObject() { if (!*this) return nullptr; - if (loadedFromDB) + switch (GetHigh()) { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; - - return nullptr; + case HighGuid::Player: + return GetPlayer(); + case HighGuid::Transport: + case HighGuid::Mo_Transport: + case HighGuid::GameObject: + return GetGameObject(); + case HighGuid::Vehicle: + case HighGuid::Unit: + return GetCreature(); + case HighGuid::Pet: + return getMap()->GetPet(*this); + case HighGuid::DynamicObject: + return getMap()->GetDynamicObject(*this); + case HighGuid::Corpse: + return getMap()->GetCorpse(*this); + default: + return nullptr; } - return getMap()->GetCreature(*this); + return nullptr; } -Unit* GuidPosition::GetUnit() +GameObject* GuidPosition::GetGameObject() { if (!*this) return nullptr; if (loadedFromDB) - { - auto creatureBounds = getMap()->GetCreatureBySpawnIdStore().equal_range(GetCounter()); - if (creatureBounds.first != creatureBounds.second) - return creatureBounds.second->second; + return ObjectAccessor::GetSpawnedGameObjectByDBGUID(GetMapId(), GetCounter()); + return getMap()->GetGameObject(*this); // fallback +} + +Unit* GuidPosition::GetUnit() +{ + if (!*this) return nullptr; - } if (IsPlayer()) - return ObjectAccessor::FindPlayer(*this); + return GetPlayer(); if (IsPet()) return getMap()->GetPet(*this); @@ -921,21 +945,15 @@ Unit* GuidPosition::GetUnit() return GetCreature(); } -GameObject* GuidPosition::GetGameObject() +Creature* GuidPosition::GetCreature() { if (!*this) return nullptr; if (loadedFromDB) - { - auto gameobjectBounds = getMap()->GetGameObjectBySpawnIdStore().equal_range(GetCounter()); - if (gameobjectBounds.first != gameobjectBounds.second) - return gameobjectBounds.second->second; + return ObjectAccessor::GetSpawnedCreatureByDBGUID(GetMapId(), GetCounter()); - return nullptr; - } - - return getMap()->GetGameObject(*this); + return getMap()->GetCreature(*this); // fallback } Player* GuidPosition::GetPlayer() @@ -949,21 +967,28 @@ Player* GuidPosition::GetPlayer() return nullptr; } -bool GuidPosition::isDead() -{ - if (!getMap()) - return false; - - if (!getMap()->IsGridLoaded(getX(), getY())) - return false; +bool GuidPosition::HasNpcFlag(NPCFlags flag) { return IsCreature() && GetCreatureTemplate()->npcflag & flag; } - if (IsUnit() && GetUnit() && GetUnit()->IsInWorld() && GetUnit()->IsAlive()) +bool GuidPosition::IsCreatureOrGOAccessible() +{ + Map* map = getMap(); + if (!map || !map->IsGridLoaded(GetPositionX(), GetPositionY())) return false; - if (IsGameObject() && GetGameObject() && GetGameObject()->IsInWorld()) - 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 true; + return false; } GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} @@ -982,47 +1007,6 @@ GuidPosition::GuidPosition(GameObjectData const& goData) loadedFromDB = true; } -CreatureTemplate const* GuidPosition::GetCreatureTemplate() -{ - return IsCreature() ? sObjectMgr->GetCreatureTemplate(GetEntry()) : nullptr; -} - -GameObjectTemplate const* GuidPosition::GetGameObjectTemplate() -{ - return IsGameObject() ? sObjectMgr->GetGameObjectTemplate(GetEntry()) : nullptr; -} - -WorldObject* GuidPosition::GetWorldObject() -{ - if (!*this) - return nullptr; - - switch (GetHigh()) - { - case HighGuid::Player: - return ObjectAccessor::FindPlayer(*this); - case HighGuid::Transport: - case HighGuid::Mo_Transport: - case HighGuid::GameObject: - return GetGameObject(); - case HighGuid::Vehicle: - case HighGuid::Unit: - return GetCreature(); - case HighGuid::Pet: - return getMap()->GetPet(*this); - case HighGuid::DynamicObject: - return getMap()->GetDynamicObject(*this); - case HighGuid::Corpse: - return getMap()->GetCorpse(*this); - default: - return nullptr; - } - - return nullptr; -} - -bool GuidPosition::HasNpcFlag(NPCFlags flag) { return IsCreature() && GetCreatureTemplate()->npcflag & flag; } - std::vector TravelDestination::getPoints(bool ignoreFull) { if (ignoreFull) diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 3223444bfee..251313736aa 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -416,14 +416,13 @@ class GuidPosition : public ObjectGuid, public WorldPosition 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 a80321d535c..ee6443cd8a1 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 @@ class GroupInviteOperation : public PlayerbotOperation 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