diff --git a/src/strategy/raids/ulduar/RaidUlduarActionContext.h b/src/strategy/raids/ulduar/RaidUlduarActionContext.h index ddcac039..c7d1a3c7 100644 --- a/src/strategy/raids/ulduar/RaidUlduarActionContext.h +++ b/src/strategy/raids/ulduar/RaidUlduarActionContext.h @@ -17,11 +17,25 @@ public: { creators["flame leviathan vehicle"] = &RaidUlduarActionContext::flame_leviathan_vehicle; creators["flame leviathan enter vehicle"] = &RaidUlduarActionContext::flame_leviathan_enter_vehicle; + creators["razorscale avoid devouring flames"] = &RaidUlduarActionContext::razorscale_avoid_devouring_flames; + creators["razorscale avoid sentinel"] = &RaidUlduarActionContext::razorscale_avoid_sentinel; + creators["razorscale ignore flying alone"] = &RaidUlduarActionContext::razorscale_ignore_flying_alone; + creators["razorscale avoid whirlwind"] = &RaidUlduarActionContext::razorscale_avoid_whirlwind; + creators["razorscale grounded"] = &RaidUlduarActionContext::razorscale_grounded; + creators["razorscale harpoon action"] = &RaidUlduarActionContext::razorscale_harpoon_action; + creators["razorscale fuse armor action"] = &RaidUlduarActionContext::razorscale_fuse_armor_action; } private: static Action* flame_leviathan_vehicle(PlayerbotAI* ai) { return new FlameLeviathanVehicleAction(ai); } static Action* flame_leviathan_enter_vehicle(PlayerbotAI* ai) { return new FlameLeviathanEnterVehicleAction(ai); } + static Action* razorscale_avoid_devouring_flames(PlayerbotAI* ai) { return new RazorscaleAvoidDevouringFlameAction(ai); } + static Action* razorscale_avoid_sentinel(PlayerbotAI* ai) { return new RazorscaleAvoidSentinelAction(ai); } + static Action* razorscale_ignore_flying_alone(PlayerbotAI* ai) { return new RazorscaleIgnoreBossAction(ai); } + static Action* razorscale_avoid_whirlwind(PlayerbotAI* ai) { return new RazorscaleAvoidWhirlwindAction(ai); } + static Action* razorscale_grounded(PlayerbotAI* ai) { return new RazorscaleGroundedAction(ai); } + static Action* razorscale_harpoon_action(PlayerbotAI* ai) { return new RazorscaleHarpoonAction(ai); } + static Action* razorscale_fuse_armor_action(PlayerbotAI* ai) { return new RazorscaleFuseArmorAction(ai); } }; -#endif \ No newline at end of file +#endif diff --git a/src/strategy/raids/ulduar/RaidUlduarActions.cpp b/src/strategy/raids/ulduar/RaidUlduarActions.cpp index 84c2bf1f..6e378bd4 100644 --- a/src/strategy/raids/ulduar/RaidUlduarActions.cpp +++ b/src/strategy/raids/ulduar/RaidUlduarActions.cpp @@ -3,17 +3,22 @@ #include +#include "AiObjectContext.h" #include "DBCEnums.h" #include "GameObject.h" +#include "Group.h" #include "LastMovementValue.h" #include "ObjectDefines.h" #include "ObjectGuid.h" +#include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" +#include "Player.h" #include "Playerbots.h" #include "Position.h" #include "RaidUlduarBossHelper.h" #include "RaidUlduarScripts.h" #include "RaidUlduarStrategy.h" +#include "RtiValue.h" #include "ScriptedCreature.h" #include "ServerFacade.h" #include "SharedDefines.h" @@ -392,4 +397,760 @@ bool FlameLeviathanEnterVehicleAction::AllMainVehiclesOnUse() Difficulty diff = bot->GetRaidDifficulty(); int maxC = (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_10MAN_HEROIC) ? 2 : 5; return demolisher >= maxC && siege >= maxC; -} \ No newline at end of file +} +bool RazorscaleAvoidDevouringFlameAction::Execute(Event event) +{ + RazorscaleBossHelper razorscaleHelper(botAI); + + if (!razorscaleHelper.UpdateBossAI()) + { + return false; + } + + bool isMainTank = botAI->IsMainTank(bot); + const float flameRadius = 3.5f; + + // Main tank moves further so they can hold adds away from flames, but only during the air phases + const float safeDistanceMultiplier = (isMainTank && !razorscaleHelper.IsGroundPhase()) ? 2.3f : 1.0f; + const float safeDistance = flameRadius * safeDistanceMultiplier; + + // Get the boss + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + { + return false; + } + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* closestFlame = nullptr; + float closestDistance = std::numeric_limits::max(); + + // Find the closest Devouring Flame + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DEVOURING_FLAME) + { + float distance = bot->GetDistance2d(unit); + if (distance < closestDistance) + { + closestDistance = distance; + closestFlame = unit; + } + } + } + + // Off tanks are following the main tank during grounded and should prioritise stacking + if (razorscaleHelper.IsGroundPhase() && (botAI->IsTank(bot) && !botAI->IsMainTank(bot))) + { + return false; + } + + // Handle movement from flames + if (closestDistance < safeDistance) + { + return MoveAway(closestFlame, safeDistance); + } + return false; +} + +bool RazorscaleAvoidDevouringFlameAction::isUseful() +{ + bool isMainTank = botAI->IsMainTank(bot); + + const float flameRadius = 3.5f; + const float safeDistanceMultiplier = isMainTank ? 2.3f : 1.0f; + const float safeDistance = flameRadius * safeDistanceMultiplier; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DEVOURING_FLAME) + { + float distance = bot->GetDistance2d(unit); + if (distance < safeDistance) + { + return true; // Bot is within the danger distance + } + } + } + + return false; // No nearby flames or bot is at a safe distance +} + +bool RazorscaleAvoidSentinelAction::Execute(Event event) +{ + bool isTank = botAI->IsTank(bot); + bool isMainTank = botAI->IsMainTank(bot); + bool isRanged = botAI->IsRanged(bot); + const float radius = 8.0f; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* lowestHealthSentinel = nullptr; + uint32 lowestHealth = UINT32_MAX; + bool movedAway = false; + + // Iterate through all nearby NPCs + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) + { + // Check if this sentinel has the lowest health + if (unit->GetHealth() < lowestHealth) + { + lowestHealth = unit->GetHealth(); + lowestHealthSentinel = unit; + } + + // Move away if ranged and too close + if (isRanged && bot->GetDistance2d(unit) < radius) + { + movedAway = MoveAway(unit, radius) || movedAway; + } + } + } + + // Check if the main tank is a human player + Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); + Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; + + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a real player + { + // Iterate through the first 3 bot tanks to assign the Skull marker + for (int i = 0; i < 3; ++i) + { + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + { + Group* group = bot->GetGroup(); + if (group && lowestHealthSentinel) + { + int8 skullIndex = 7; // Skull + ObjectGuid currentSkullTarget = group->GetTargetIcon(skullIndex); + + // If there's no skull set yet, or the skull is on a different target, set the sentinel + if (!currentSkullTarget || (lowestHealthSentinel->GetGUID() != currentSkullTarget)) + { + group->SetTargetIcon(skullIndex, bot->GetGUID(), lowestHealthSentinel->GetGUID()); + } + } + break; // Stop after finding the first valid bot tank + } + } + } + else if (isMainTank && lowestHealthSentinel) // Bot is the main tank + { + Group* group = bot->GetGroup(); + if (group) + { + int8 skullIndex = 7; // Skull + ObjectGuid currentSkullTarget = group->GetTargetIcon(skullIndex); + + // If there's no skull set yet, or the skull is on a different target, set the sentinel + if (!currentSkullTarget || (lowestHealthSentinel->GetGUID() != currentSkullTarget)) + { + group->SetTargetIcon(skullIndex, bot->GetGUID(), lowestHealthSentinel->GetGUID()); + } + } + } + + + return movedAway; // Return true if moved +} + +bool RazorscaleAvoidSentinelAction::isUseful() +{ + bool isMainTank = botAI->IsMainTank(bot); + Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); + Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; + + // If this bot is the main tank, it should always try to mark + if (isMainTank) + { + return true; + } + + // If the main tank is a human, check if this bot is one of the first three valid bot tanks + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player + { + for (int i = 0; i < 3; ++i) + { + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + { + return true; // This bot should assist with marking + } + } + } + + bool isRanged = botAI->IsRanged(bot); + const float radius = 8.0f; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) + { + if (isRanged && bot->GetDistance2d(unit) < radius) + { + return true; + } + } + } + + return false; +} + +bool RazorscaleAvoidWhirlwindAction::Execute(Event event) +{ + if (botAI->IsTank(bot)) + { + return false; + } + + const float radius = 8.0f; + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) + { + float currentDistance = bot->GetDistance2d(unit); + if (currentDistance < radius) + { + return MoveAway(unit, radius); + } + } + } + return false; +} + +bool RazorscaleAvoidWhirlwindAction::isUseful() +{ + // Tanks do not avoid Whirlwind + if (botAI->IsTank(bot)) + { + return false; + } + + const float radius = 8.0f; + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) + { + if (unit->HasAura(RazorscaleBossHelper::SPELL_SENTINEL_WHIRLWIND) || unit->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + { + if (bot->GetDistance2d(unit) < radius) + { + return true; + } + } + } + } + + return false; +} + +bool RazorscaleIgnoreBossAction::isUseful() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + { + return false; + } + + // Check if the boss is flying + if (boss->GetPositionZ() >= RazorscaleBossHelper::RAZORSCALE_FLYING_Z_THRESHOLD) + { + // Check if the bot is outside the designated area + if (bot->GetDistance2d( + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y) > RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS + 25.0f) + { + return true; // Movement to the center is the top priority for all bots + } + + if (!botAI->IsTank(bot)) + { + return false; + } + + Group* group = bot->GetGroup(); + if (!group) + { + return false; + } + + // Check if the boss is already set as the moon marker + int8 moonIndex = 4; // Moon marker index + ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); + if (currentMoonTarget == boss->GetGUID()) + { + return false; // Moon marker is already correctly set, no further action needed + } + + // Proceed to tank-specific logic + Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); + Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; + + // If this bot is the main tank, it needs to set the moon marker + if (mainTankUnit == bot) + { + return true; + } + + // If the main tank is a human, check if this bot is the lowest-indexed bot tank + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player + { + for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes + { + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Valid bot tank + { + return true; // This bot should assign the marker + } + } + } + } + + return false; +} + +bool RazorscaleIgnoreBossAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + { + return false; + } + + Group* group = bot->GetGroup(); + if (!group) + { + return false; + } + + // Check if the bot is outside the designated area and move inside first + if (bot->GetDistance2d( + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y) > RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS + 25.0f) + { + return MoveInside( + ULDUAR_MAP_ID, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, + bot->GetPositionZ(), + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 10.0f, + MovementPriority::MOVEMENT_NORMAL + ); + } + + if (!botAI->IsTank(bot)) + { + return false; + } + + // Check if the boss is already set as the moon marker + int8 moonIndex = 4; + ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); + if (currentMoonTarget == boss->GetGUID()) + { + return false; // Moon marker is already correctly set + } + + // Get the main tank and determine role + Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); + Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; + + // If the main tank is a human, assign the moon marker using the lowest-indexed bot tank + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a real player + { + for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes + { + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + { + group->SetTargetIcon(moonIndex, bot->GetGUID(), boss->GetGUID()); + SetNextMovementDelay(1000); + break; // Assign the moon marker and stop + } + } + } + else if (mainTankUnit == bot) // If this bot is the main tank + { + group->SetTargetIcon(moonIndex, bot->GetGUID(), boss->GetGUID()); + SetNextMovementDelay(1000); + } + + // Tanks move inside the arena + return MoveInside( + ULDUAR_MAP_ID, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, + bot->GetPositionZ(), + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 10.0f, + MovementPriority::MOVEMENT_NORMAL + ); +} + +bool RazorscaleGroundedAction::isUseful() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss || !boss->IsAlive() || boss->GetPositionZ() > RazorscaleBossHelper::RAZORSCALE_FLYING_Z_THRESHOLD) + { + return false; + } + + if (botAI->IsMainTank(bot)) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Check if the boss is marked with Moon + int8 moonIndex = 4; + ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); + + // Useful only if the boss is currently marked with Moon + return currentMoonTarget == boss->GetGUID(); + } + + if (botAI->IsTank(bot) && !botAI->IsMainTank(bot)) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Find the main tank + 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) + { + constexpr float maxDistance = 2.0f; + float distanceToMainTank = bot->GetDistance2d(mainTank); + return (distanceToMainTank > maxDistance); + } + } + + if (botAI->IsMelee(bot)) + { + return false; + } + + if (botAI->IsRanged(bot)) + { + constexpr float landingX = 588.0f; + constexpr float landingY = -166.0f; + constexpr float landingZ = 391.1f; + + float bossX = boss->GetPositionX(); + float bossY = boss->GetPositionY(); + float bossZ = boss->GetPositionZ(); + + bool atInitialLandingPosition = (fabs(bossX - landingX) < 2.0f) && + (fabs(bossY - landingY) < 2.0f) && + (fabs(bossZ - landingZ) < 1.0f); + + constexpr float initialLandingRadius = 14.0f; + constexpr float normalRadius = 12.0f; + + if (atInitialLandingPosition) + { + float adjustedCenterX = RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X; + float adjustedCenterY = RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y - 20.0f; + + float distanceToAdjustedCenter = bot->GetDistance2d(adjustedCenterX, adjustedCenterY); + return distanceToAdjustedCenter > initialLandingRadius; + } + + float distanceToCenter = bot->GetDistance2d(RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y); + return distanceToCenter > normalRadius; + } + + return false; +} + +bool RazorscaleGroundedAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss || !boss->IsAlive() || boss->GetPositionZ() > RazorscaleBossHelper::RAZORSCALE_FLYING_Z_THRESHOLD) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); + Player* mainTank = mainTankUnit ? mainTankUnit->ToPlayer() : nullptr; + + if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player + { + // Iterate through the first 3 bot tanks to handle the moon marker + for (int i = 0; i < 3; ++i) + { + if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank + { + int8 moonIndex = 4; + ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); + + // If the moon marker is set to the boss, reset it + if (currentMoonTarget == boss->GetGUID()) + { + group->SetTargetIcon(moonIndex, bot->GetGUID(), ObjectGuid::Empty); + SetNextMovementDelay(1000); + return true; + } + } + } + } + else if (botAI->IsMainTank(bot)) // Bot is the main tank + { + int8 moonIndex = 4; + ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); + + // If the moon marker is set to the boss, reset it + if (currentMoonTarget == boss->GetGUID()) + { + group->SetTargetIcon(moonIndex, bot->GetGUID(), ObjectGuid::Empty); + SetNextMovementDelay(1000); + return true; + } + } + + + if (mainTank && (botAI->IsTank(bot) && !botAI->IsMainTank(bot))) + { + + constexpr float followDistance = 2.0f; + return MoveNear(mainTank, followDistance, MovementPriority::MOVEMENT_COMBAT); + } + + if (botAI->IsRanged(bot)) + { + constexpr float landingX = 588.0f; + constexpr float landingY = -166.0f; + constexpr float landingZ = 391.1f; + + float bossX = boss->GetPositionX(); + float bossY = boss->GetPositionY(); + float bossZ = boss->GetPositionZ(); + + bool atInitialLandingPosition = (fabs(bossX - landingX) < 2.0f) && + (fabs(bossY - landingY) < 2.0f) && + (fabs(bossZ - landingZ) < 1.0f); + + if (atInitialLandingPosition) + { + // If at the initial landing position, use 12-yard radius with a + // 20 yard offset on the Y axis so everyone is behind the boss + return MoveInside( + ULDUAR_MAP_ID, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y - 20.0f, + bot->GetPositionZ(), + RazorscaleBossHelper::RAZORSCALE_ARENA_RADIUS - 12.0f, + MovementPriority::MOVEMENT_COMBAT + ); + } + + // Otherwise, move inside a 12-yard radius around the arena center + return MoveInside( + ULDUAR_MAP_ID, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_X, + RazorscaleBossHelper::RAZORSCALE_ARENA_CENTER_Y, + bot->GetPositionZ(), + 12.0f, + MovementPriority::MOVEMENT_COMBAT + ); + } + return false; +} + +bool RazorscaleHarpoonAction::Execute(Event event) +{ + RazorscaleBossHelper razorscaleHelper(botAI); + + // Update the boss AI context + if (!razorscaleHelper.UpdateBossAI()) + return false; + + Unit* boss = razorscaleHelper.GetBoss(); + if (!boss || !boss->IsAlive()) + return false; + + // Retrieve harpoon data from the helper + const std::vector& harpoonData = razorscaleHelper.GetHarpoonData(); + + GameObject* closestHarpoon = nullptr; + float minDistance = std::numeric_limits::max(); + + // Find the nearest harpoon that hasn't been fired and is not on cooldown + for (const auto& harpoon : harpoonData) + { + if (razorscaleHelper.IsHarpoonFired(harpoon.chainSpellId)) + continue; + + if (GameObject* harpoonGO = bot->FindNearestGameObject(harpoon.gameObjectEntry, 200.0f)) + { + if (RazorscaleBossHelper::IsHarpoonReady(harpoonGO)) + { + float distance = bot->GetDistance2d(harpoonGO); + if (distance < minDistance) + { + minDistance = distance; + closestHarpoon = harpoonGO; + } + } + } + } + + if (!closestHarpoon) + return false; + + // Find the nearest ranged DPS (not a healer) to the harpoon + Player* closestRangedDPS = nullptr; + minDistance = std::numeric_limits::max(); + GuidVector groupBots = AI_VALUE(GuidVector, "group members"); + + for (auto& guid : groupBots) + { + Player* member = ObjectAccessor::FindPlayer(guid); + if (member && member->IsAlive() && botAI->IsRanged(member) && botAI->IsDps(member) && !botAI->IsHeal(member)) + { + float distance = member->GetDistance2d(closestHarpoon); + if (distance < minDistance) + { + minDistance = distance; + closestRangedDPS = member; + } + } + } + + // Only proceed if this bot is the closest ranged DPS + if (closestRangedDPS != bot) + return false; + + float botDist = bot->GetDistance(closestHarpoon); + if (botDist > INTERACTION_DISTANCE - 1.0f) + { + return MoveTo(bot->GetMapId(), + closestHarpoon->GetPositionX(), + closestHarpoon->GetPositionY(), + closestHarpoon->GetPositionZ()); + } + + SetNextMovementDelay(1000); + + // Interact with the harpoon + { + WorldPacket usePacket(CMSG_GAMEOBJ_USE); + usePacket << closestHarpoon->GetGUID(); + bot->GetSession()->HandleGameObjectUseOpcode(usePacket); + } + + { + WorldPacket reportPacket(CMSG_GAMEOBJ_REPORT_USE); + reportPacket << closestHarpoon->GetGUID(); + bot->GetSession()->HandleGameobjectReportUse(reportPacket); + } + + RazorscaleBossHelper::SetHarpoonOnCooldown(closestHarpoon); + + return true; +} + +bool RazorscaleHarpoonAction::isUseful() +{ + RazorscaleBossHelper razorscaleHelper(botAI); + + // Update the boss AI context to ensure we have the latest info + if (!razorscaleHelper.UpdateBossAI()) + return false; + + Unit* boss = razorscaleHelper.GetBoss(); + if (!boss || !boss->IsAlive()) + return false; + + const std::vector& harpoonData = razorscaleHelper.GetHarpoonData(); + + for (const auto& harpoon : harpoonData) + { + if (razorscaleHelper.IsHarpoonFired(harpoon.chainSpellId)) + continue; + + if (GameObject* harpoonGO = bot->FindNearestGameObject(harpoon.gameObjectEntry, 200.0f)) + { + if (RazorscaleBossHelper::IsHarpoonReady(harpoonGO)) + { + // Check if this bot is a ranged DPS (not a healer) + if (botAI->IsRanged(bot) && botAI->IsDps(bot) && !botAI->IsHeal(bot)) + return true; + } + } + } + + return false; +} + +bool RazorscaleFuseArmorAction::isUseful() +{ + // If this bot cannot tank at all, no need to do anything + if (!botAI->IsTank(bot)) + return false; + + // If this bot is the main tank AND has Fuse Armor at the threshold, return true immediately + if (botAI->IsMainTank(bot)) + { + Aura* fuseArmor = bot->GetAura(RazorscaleBossHelper::SPELL_FUSEARMOR); + if (fuseArmor && fuseArmor->GetStackAmount() >= RazorscaleBossHelper::FUSEARMOR_THRESHOLD) + return true; + } + + // Otherwise, check if there's any other main tank with high Fuse Armor + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member) + continue; + + if (botAI->IsMainTank(member) && member != bot) + { + Aura* fuseArmor = member->GetAura(RazorscaleBossHelper::SPELL_FUSEARMOR); + if (fuseArmor && fuseArmor->GetStackAmount() >= RazorscaleBossHelper::FUSEARMOR_THRESHOLD) + { + // There is another main tank with high Fuse Armor + return true; + } + } + } + + return false; +} + +bool RazorscaleFuseArmorAction::Execute(Event event) +{ + // We already know from isUseful() that: + // 1) This bot can tank, AND + // 2) There is at least one main tank (possibly this bot) with Fuse Armor >= threshold. + + RazorscaleBossHelper bossHelper(botAI); + + // Attempt to reassign the roles based on health/Fuse Armor debuff + bossHelper.AssignRolesBasedOnHealth(); + return true; +} diff --git a/src/strategy/raids/ulduar/RaidUlduarActions.h b/src/strategy/raids/ulduar/RaidUlduarActions.h index 0019ff8a..34b0484e 100644 --- a/src/strategy/raids/ulduar/RaidUlduarActions.h +++ b/src/strategy/raids/ulduar/RaidUlduarActions.h @@ -11,6 +11,10 @@ #include "RaidUlduarBossHelper.h" #include "Vehicle.h" +// +// Flame Leviathan +// + class FlameLeviathanVehicleAction : public MovementAction { public: @@ -42,4 +46,64 @@ protected: bool AllMainVehiclesOnUse(); }; -#endif \ No newline at end of file +// +// Razorscale +// + +class RazorscaleAvoidDevouringFlameAction : public MovementAction +{ +public: + RazorscaleAvoidDevouringFlameAction(PlayerbotAI* botAI) : MovementAction(botAI, "razorscale avoid devouring flames") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RazorscaleAvoidSentinelAction : public MovementAction +{ +public: + RazorscaleAvoidSentinelAction(PlayerbotAI* botAI) : MovementAction(botAI, "razorscale avoid sentinel") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RazorscaleIgnoreBossAction : public AttackAction +{ +public: + RazorscaleIgnoreBossAction(PlayerbotAI* botAI) : AttackAction(botAI, "razorscale ignore flying alone") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RazorscaleAvoidWhirlwindAction : public MovementAction +{ +public: + RazorscaleAvoidWhirlwindAction(PlayerbotAI* botAI) : MovementAction(botAI, "razorscale avoid whirlwind") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RazorscaleGroundedAction : public AttackAction +{ +public: + RazorscaleGroundedAction(PlayerbotAI* botAI) : AttackAction(botAI, "razorscale grounded") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RazorscaleHarpoonAction : public MovementAction +{ +public: + RazorscaleHarpoonAction(PlayerbotAI* botAI) : MovementAction(botAI, "razorscale harpoon action") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RazorscaleFuseArmorAction : public MovementAction +{ +public: + RazorscaleFuseArmorAction(PlayerbotAI* botAI) : MovementAction(botAI, "razorscale fuse armor action") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +#endif diff --git a/src/strategy/raids/ulduar/RaidUlduarBossHelper.cpp b/src/strategy/raids/ulduar/RaidUlduarBossHelper.cpp new file mode 100644 index 00000000..5ebb58a6 --- /dev/null +++ b/src/strategy/raids/ulduar/RaidUlduarBossHelper.cpp @@ -0,0 +1,226 @@ +#include "ChatHelper.h" +#include "RaidUlduarBossHelper.h" +#include "ObjectAccessor.h" +#include "GameObject.h" +#include "Group.h" +#include "ScriptedCreature.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "World.h" + +// Prevent harpoon spam +std::unordered_map RazorscaleBossHelper::_harpoonCooldowns; +// Prevent role assignment spam +std::unordered_map RazorscaleBossHelper::_lastRoleSwapTime; +const std::time_t RazorscaleBossHelper::_roleSwapCooldown; + +bool RazorscaleBossHelper::UpdateBossAI() +{ + _boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (_boss) + { + Group* group = bot->GetGroup(); + if (group && !AreRolesAssigned()) + { + AssignRolesBasedOnHealth(); + } + return true; + } + return false; +} + +Unit* RazorscaleBossHelper::GetBoss() const +{ + return _boss; +} + +bool RazorscaleBossHelper::IsGroundPhase() const +{ + return _boss && _boss->IsAlive() && + (_boss->GetPositionZ() <= RAZORSCALE_FLYING_Z_THRESHOLD) && + (_boss->GetHealthPct() < 50.0f) && + !_boss->HasAura(SPELL_STUN_AURA); +} + +bool RazorscaleBossHelper::IsFlyingPhase() const +{ + return _boss && (!IsGroundPhase() || _boss->GetPositionZ() >= RAZORSCALE_FLYING_Z_THRESHOLD); +} + +bool RazorscaleBossHelper::IsHarpoonFired(uint32 chainSpellId) const +{ + return _boss && _boss->HasAura(chainSpellId); +} + +bool RazorscaleBossHelper::IsHarpoonReady(GameObject* harpoonGO) +{ + if (!harpoonGO) + return false; + + auto it = _harpoonCooldowns.find(harpoonGO->GetGUID()); + if (it != _harpoonCooldowns.end()) + { + time_t currentTime = std::time(nullptr); + time_t elapsedTime = currentTime - it->second; + if (elapsedTime < HARPOON_COOLDOWN_DURATION) + return false; + } + + return harpoonGO->GetGoState() == GO_STATE_READY; +} + +void RazorscaleBossHelper::SetHarpoonOnCooldown(GameObject* harpoonGO) +{ + if (!harpoonGO) + return; + + time_t currentTime = std::time(nullptr); + _harpoonCooldowns[harpoonGO->GetGUID()] = currentTime; +} + +GameObject* RazorscaleBossHelper::FindNearestHarpoon(float x, float y, float z) const +{ + GameObject* nearestHarpoon = nullptr; + float minDistanceSq = std::numeric_limits::max(); + + for (const auto& harpoon : GetHarpoonData()) + { + if (GameObject* harpoonGO = bot->FindNearestGameObject(harpoon.gameObjectEntry, 200.0f)) + { + float dx = harpoonGO->GetPositionX() - x; + float dy = harpoonGO->GetPositionY() - y; + float dz = harpoonGO->GetPositionZ() - z; + float distanceSq = dx * dx + dy * dy + dz * dz; + + if (distanceSq < minDistanceSq) + { + minDistanceSq = distanceSq; + nearestHarpoon = harpoonGO; + } + } + } + + return nearestHarpoon; +} + +const std::vector& RazorscaleBossHelper::GetHarpoonData() +{ + static const std::vector harpoonData = + { + { GO_RAZORSCALE_HARPOON_1, SPELL_CHAIN_1 }, + { GO_RAZORSCALE_HARPOON_2, SPELL_CHAIN_2 }, + { GO_RAZORSCALE_HARPOON_3, SPELL_CHAIN_3 }, + { GO_RAZORSCALE_HARPOON_4, SPELL_CHAIN_4 }, + }; + return harpoonData; +} + +bool RazorscaleBossHelper::AreRolesAssigned() const +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Retrieve the group member slot list (GUID + flags + other info) + Group::MemberSlotList const& slots = group->GetMemberSlots(); + for (auto const& slot : slots) + { + // Check if this member has the MAINTANK flag + if (slot.flags & MEMBER_FLAG_MAINTANK) + { + return true; + } + } + + return false; +} + +bool RazorscaleBossHelper::CanSwapRoles() const +{ + // Identify the GUID of the current bot + ObjectGuid botGuid = bot->GetGUID(); + if (!botGuid) + return false; + + // If no entry exists yet for this bot, initialize it to 0 + auto it = _lastRoleSwapTime.find(botGuid); + if (it == _lastRoleSwapTime.end()) + { + _lastRoleSwapTime[botGuid] = 0; + it = _lastRoleSwapTime.find(botGuid); + } + + // Compare the current time against the stored time + std::time_t currentTime = std::time(nullptr); + std::time_t lastSwapTime = it->second; + + return (currentTime - lastSwapTime) >= _roleSwapCooldown; +} + + +void RazorscaleBossHelper::AssignRolesBasedOnHealth() +{ + // Check if enough time has passed since last swap + if (!CanSwapRoles()) + return; + + Group* group = bot->GetGroup(); + if (!group) + return; + + // Gather all tank-capable players (bots + real players), excluding those with too many Fuse Armor stacks + std::vector tankCandidates; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsTank(member, true) || !member->IsAlive()) + continue; + + Aura* fuseArmor = member->GetAura(SPELL_FUSEARMOR); + if (fuseArmor && fuseArmor->GetStackAmount() >= FUSEARMOR_THRESHOLD) + continue; + + tankCandidates.push_back(member); + } + + // If there are no viable tanks, do nothing + if (tankCandidates.empty()) + return; + + // Sort by highest max health first + std::sort(tankCandidates.begin(), tankCandidates.end(), + [](Player* a, Player* b) + { + return a->GetMaxHealth() > b->GetMaxHealth(); + } + ); + + // Pick the top candidate + Player* newMainTank = tankCandidates[0]; + if (!newMainTank) // Safety check + return; + + // Unflag everyone from main tank + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsMainTank(member)) + group->SetGroupMemberFlag(member->GetGUID(), false, MEMBER_FLAG_MAINTANK); + } + + // Assign the single main tank + group->SetGroupMemberFlag(newMainTank->GetGUID(), true, MEMBER_FLAG_MAINTANK); + + // Yell a message regardless of whether the new main tank is a bot or a real player + const std::string playerName = newMainTank->GetName(); + const std::string text = playerName + " set as main tank!"; + bot->Yell(text, LANG_UNIVERSAL); + + ObjectGuid botGuid = bot->GetGUID(); + if (!botGuid) + return; + + // Set current time in the cooldown map for this bot to start cooldown + _lastRoleSwapTime[botGuid] = std::time(nullptr); +} diff --git a/src/strategy/raids/ulduar/RaidUlduarBossHelper.h b/src/strategy/raids/ulduar/RaidUlduarBossHelper.h index 3580444d..2fb84573 100644 --- a/src/strategy/raids/ulduar/RaidUlduarBossHelper.h +++ b/src/strategy/raids/ulduar/RaidUlduarBossHelper.h @@ -2,6 +2,11 @@ #define _PLAYERBOT_RAIDULDUARBOSSHELPER_H #include +#include +#include +#include +#include +#include #include "AiObject.h" #include "AiObjectContext.h" @@ -16,6 +21,88 @@ 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 // { diff --git a/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp b/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp index 0c6ed182..c9f9916b 100644 --- a/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp +++ b/src/strategy/raids/ulduar/RaidUlduarStrategy.cpp @@ -4,7 +4,9 @@ void RaidUlduarStrategy::InitTriggers(std::vector& triggers) { + // // Flame Leviathan + // triggers.push_back(new TriggerNode( "flame leviathan vehicle near", NextAction::array(0, new NextAction("flame leviathan enter vehicle", ACTION_RAID + 2), nullptr))); @@ -12,7 +14,38 @@ void RaidUlduarStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "flame leviathan on vehicle", NextAction::array(0, new NextAction("flame leviathan vehicle", ACTION_RAID + 1), nullptr))); + + // + // Razorscale + // + triggers.push_back(new TriggerNode( + "razorscale avoid devouring flames", + NextAction::array(0, new NextAction("razorscale avoid devouring flames", ACTION_RAID + 1), nullptr))); + + triggers.push_back(new TriggerNode( + "razorscale avoid sentinel", + NextAction::array(0, new NextAction("razorscale avoid sentinel", ACTION_RAID + 2), nullptr))); + triggers.push_back(new TriggerNode( + "razorscale flying alone", + NextAction::array(0, new NextAction("razorscale ignore flying alone", ACTION_MOVE + 5), nullptr))); + + triggers.push_back(new TriggerNode( + "razorscale avoid whirlwind", + NextAction::array(0, new NextAction("razorscale avoid whirlwind", ACTION_RAID + 3), nullptr))); + + triggers.push_back(new TriggerNode( + "razorscale grounded", + NextAction::array(0, new NextAction("razorscale grounded", ACTION_RAID), nullptr))); + + triggers.push_back(new TriggerNode( + "razorscale harpoon trigger", + NextAction::array(0, new NextAction("razorscale harpoon action", ACTION_MOVE), nullptr))); + + triggers.push_back(new TriggerNode( + "razorscale fuse armor trigger", + NextAction::array(0, new NextAction("razorscale fuse armor action", ACTION_RAID + 2), nullptr))); + } void RaidUlduarStrategy::InitMultipliers(std::vector& multipliers) diff --git a/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h b/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h index 22c0f044..4b42c401 100644 --- a/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h +++ b/src/strategy/raids/ulduar/RaidUlduarTriggerContext.h @@ -17,11 +17,25 @@ public: { creators["flame leviathan on vehicle"] = &RaidUlduarTriggerContext::flame_leviathan_on_vehicle; creators["flame leviathan vehicle near"] = &RaidUlduarTriggerContext::flame_leviathan_vehicle_near; + creators["razorscale flying alone"] = &RaidUlduarTriggerContext::razorscale_flying_alone; + creators["razorscale avoid devouring flames"] = &RaidUlduarTriggerContext::razorscale_avoid_devouring_flames; + creators["razorscale avoid sentinel"] = &RaidUlduarTriggerContext::razorscale_avoid_sentinel; + creators["razorscale avoid whirlwind"] = &RaidUlduarTriggerContext::razorscale_avoid_whirlwind; + creators["razorscale grounded"] = &RaidUlduarTriggerContext::razorscale_grounded; + creators["razorscale harpoon trigger"] = &RaidUlduarTriggerContext::razorscale_harpoon_trigger; + creators["razorscale fuse armor trigger"] = &RaidUlduarTriggerContext::razorscale_fuse_armor_trigger; } private: static Trigger* flame_leviathan_on_vehicle(PlayerbotAI* ai) { return new FlameLeviathanOnVehicleTrigger(ai); } static Trigger* flame_leviathan_vehicle_near(PlayerbotAI* ai) { return new FlameLeviathanVehicleNearTrigger(ai); } + static Trigger* razorscale_flying_alone(PlayerbotAI* ai) { return new RazorscaleFlyingAloneTrigger(ai); } + static Trigger* razorscale_avoid_devouring_flames(PlayerbotAI* ai) { return new RazorscaleDevouringFlamesTrigger(ai); } + static Trigger* razorscale_avoid_sentinel(PlayerbotAI* ai) { return new RazorscaleAvoidSentinelTrigger(ai); } + static Trigger* razorscale_avoid_whirlwind(PlayerbotAI* ai) { return new RazorscaleAvoidWhirlwindTrigger(ai); } + static Trigger* razorscale_grounded(PlayerbotAI* ai) { return new RazorscaleGroundedTrigger(ai); } + static Trigger* razorscale_harpoon_trigger(PlayerbotAI* ai) { return new RazorscaleHarpoonAvailableTrigger(ai); } + static Trigger* razorscale_fuse_armor_trigger(PlayerbotAI* ai) { return new RazorscaleFuseArmorTrigger(ai); } }; #endif diff --git a/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp b/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp index b231f716..d2da0940 100644 --- a/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp +++ b/src/strategy/raids/ulduar/RaidUlduarTriggers.cpp @@ -1,8 +1,11 @@ #include "RaidUlduarTriggers.h" #include "EventMap.h" +#include "GameObject.h" #include "Object.h" +#include "PlayerbotAI.h" #include "Playerbots.h" +#include "RaidUlduarBossHelper.h" #include "RaidUlduarScripts.h" #include "ScriptedCreature.h" #include "SharedDefines.h" @@ -43,3 +46,198 @@ bool FlameLeviathanVehicleNearTrigger::IsActive() return true; } + +bool RazorscaleFlyingAloneTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + { + return false; + } + + // Check if the boss is flying + if (boss->GetPositionZ() < RazorscaleBossHelper::RAZORSCALE_FLYING_Z_THRESHOLD) + { + return false; + } + + // Get the list of attackers + GuidVector attackers = context->GetValue("attackers")->Get(); + if (attackers.empty()) + { + return true; // No attackers implies flying alone + } + + std::vector dark_rune_adds; + + // Loop through attackers to find dark rune adds + for (ObjectGuid const& guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit) + continue; + + uint32 entry = unit->GetEntry(); + + // Check for valid dark rune entries + if (entry == RazorscaleBossHelper::UNIT_DARK_RUNE_WATCHER || + entry == RazorscaleBossHelper::UNIT_DARK_RUNE_GUARDIAN || + entry == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) + { + dark_rune_adds.push_back(unit); + } + } + + // Return whether there are no dark rune adds + return dark_rune_adds.empty(); +} + + +bool RazorscaleDevouringFlamesTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + return false; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DEVOURING_FLAME) + { + return true; + } + } + + return false; +} + +bool RazorscaleAvoidSentinelTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + return false; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) + { + return true; + } + } + + return false; +} + +bool RazorscaleAvoidWhirlwindTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + return false; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL && + (unit->HasAura(RazorscaleBossHelper::SPELL_SENTINEL_WHIRLWIND) || unit->GetCurrentSpell(CURRENT_CHANNELED_SPELL))) + { + return true; + } + } + + return false; +} + +bool RazorscaleGroundedTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss) + { + return false; + } + + // Check if the boss is flying + if (boss->GetPositionZ() < RazorscaleBossHelper::RAZORSCALE_FLYING_Z_THRESHOLD) + { + return true; + } + return false; +} + +bool RazorscaleHarpoonAvailableTrigger::IsActive() +{ + // Get harpoon data from the helper + const std::vector& harpoonData = RazorscaleBossHelper::GetHarpoonData(); + + // Get the boss entity + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss || !boss->IsAlive()) + { + return false; + } + + // Update the boss AI context in the helper + RazorscaleBossHelper razorscaleHelper(botAI); + + if (!razorscaleHelper.UpdateBossAI()) + { + return false; + } + + // Check each harpoon entry + for (const auto& harpoon : harpoonData) + { + // Skip harpoons whose chain spell is already active on the boss + if (razorscaleHelper.IsHarpoonFired(harpoon.chainSpellId)) + { + continue; + } + + // Find the nearest harpoon GameObject within 200 yards + if (GameObject* harpoonGO = bot->FindNearestGameObject(harpoon.gameObjectEntry, 200.0f)) + { + if (RazorscaleBossHelper::IsHarpoonReady(harpoonGO)) + { + return true; // At least one harpoon is available and ready to be fired + } + } + } + + // No harpoons are available or need to be fired + return false; +} + +bool RazorscaleFuseArmorTrigger::IsActive() +{ + // Get the boss entity + Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); + if (!boss || !boss->IsAlive()) + { + return false; + } + + // Only proceed if this bot can actually tank + if (!botAI->IsTank(bot)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Iterate through group members to find the main tank with Fuse Armor + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member || !botAI->IsMainTank(member)) + continue; + + Aura* fuseArmor = member->GetAura(RazorscaleBossHelper::SPELL_FUSEARMOR); + if (fuseArmor && fuseArmor->GetStackAmount() >= RazorscaleBossHelper::FUSEARMOR_THRESHOLD) + return true; + } + + return false; +} diff --git a/src/strategy/raids/ulduar/RaidUlduarTriggers.h b/src/strategy/raids/ulduar/RaidUlduarTriggers.h index 89b784cf..2dc1e7a6 100644 --- a/src/strategy/raids/ulduar/RaidUlduarTriggers.h +++ b/src/strategy/raids/ulduar/RaidUlduarTriggers.h @@ -7,7 +7,9 @@ #include "RaidUlduarBossHelper.h" #include "Trigger.h" - +// +// Flame Levi +// class FlameLeviathanOnVehicleTrigger : public Trigger { public: @@ -22,4 +24,56 @@ public: bool IsActive() override; }; -#endif \ No newline at end of file +// +// Razorscale +// +class RazorscaleFlyingAloneTrigger : public Trigger +{ +public: + RazorscaleFlyingAloneTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale flying alone") {} + bool IsActive() override; +}; + +class RazorscaleDevouringFlamesTrigger : public Trigger +{ +public: + RazorscaleDevouringFlamesTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale avoid devouring flames") {} + bool IsActive() override; +}; + +class RazorscaleAvoidSentinelTrigger : public Trigger +{ +public: + RazorscaleAvoidSentinelTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale avoid sentinel") {} + bool IsActive() override; +}; + +class RazorscaleAvoidWhirlwindTrigger : public Trigger +{ +public: + RazorscaleAvoidWhirlwindTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale avoid whirlwind") {} + bool IsActive() override; +}; + +class RazorscaleGroundedTrigger : public Trigger +{ +public: + RazorscaleGroundedTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale grounded") {} + bool IsActive() override; +}; + +class RazorscaleHarpoonAvailableTrigger : public Trigger +{ +public: + RazorscaleHarpoonAvailableTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale harpoon trigger") {} + bool IsActive() override; +}; + +class RazorscaleFuseArmorTrigger : public Trigger +{ +public: + RazorscaleFuseArmorTrigger(PlayerbotAI* ai) : Trigger(ai, "razorscale fuse armor trigger") {} + bool IsActive() override; +}; + +#endif