mirror of
https://github.com/mod-playerbots/mod-playerbots
synced 2025-11-29 15:58:20 +08:00
Merge pull request #795 from avirar/razorscale_final
Ulduar-Razorscale strategy
This commit is contained in:
@@ -17,11 +17,25 @@ public:
|
|||||||
{
|
{
|
||||||
creators["flame leviathan vehicle"] = &RaidUlduarActionContext::flame_leviathan_vehicle;
|
creators["flame leviathan vehicle"] = &RaidUlduarActionContext::flame_leviathan_vehicle;
|
||||||
creators["flame leviathan enter vehicle"] = &RaidUlduarActionContext::flame_leviathan_enter_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:
|
private:
|
||||||
static Action* flame_leviathan_vehicle(PlayerbotAI* ai) { return new FlameLeviathanVehicleAction(ai); }
|
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* 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
|
#endif
|
||||||
|
|||||||
@@ -3,17 +3,22 @@
|
|||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "AiObjectContext.h"
|
||||||
#include "DBCEnums.h"
|
#include "DBCEnums.h"
|
||||||
#include "GameObject.h"
|
#include "GameObject.h"
|
||||||
|
#include "Group.h"
|
||||||
#include "LastMovementValue.h"
|
#include "LastMovementValue.h"
|
||||||
#include "ObjectDefines.h"
|
#include "ObjectDefines.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
|
#include "PlayerbotAI.h"
|
||||||
#include "PlayerbotAIConfig.h"
|
#include "PlayerbotAIConfig.h"
|
||||||
|
#include "Player.h"
|
||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
#include "Position.h"
|
#include "Position.h"
|
||||||
#include "RaidUlduarBossHelper.h"
|
#include "RaidUlduarBossHelper.h"
|
||||||
#include "RaidUlduarScripts.h"
|
#include "RaidUlduarScripts.h"
|
||||||
#include "RaidUlduarStrategy.h"
|
#include "RaidUlduarStrategy.h"
|
||||||
|
#include "RtiValue.h"
|
||||||
#include "ScriptedCreature.h"
|
#include "ScriptedCreature.h"
|
||||||
#include "ServerFacade.h"
|
#include "ServerFacade.h"
|
||||||
#include "SharedDefines.h"
|
#include "SharedDefines.h"
|
||||||
@@ -392,4 +397,760 @@ bool FlameLeviathanEnterVehicleAction::AllMainVehiclesOnUse()
|
|||||||
Difficulty diff = bot->GetRaidDifficulty();
|
Difficulty diff = bot->GetRaidDifficulty();
|
||||||
int maxC = (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_10MAN_HEROIC) ? 2 : 5;
|
int maxC = (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_10MAN_HEROIC) ? 2 : 5;
|
||||||
return demolisher >= maxC && siege >= maxC;
|
return demolisher >= maxC && siege >= maxC;
|
||||||
}
|
}
|
||||||
|
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<float>::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<RazorscaleBossHelper::HarpoonData>& harpoonData = razorscaleHelper.GetHarpoonData();
|
||||||
|
|
||||||
|
GameObject* closestHarpoon = nullptr;
|
||||||
|
float minDistance = std::numeric_limits<float>::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<float>::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<RazorscaleBossHelper::HarpoonData>& 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
#include "RaidUlduarBossHelper.h"
|
#include "RaidUlduarBossHelper.h"
|
||||||
#include "Vehicle.h"
|
#include "Vehicle.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Flame Leviathan
|
||||||
|
//
|
||||||
|
|
||||||
class FlameLeviathanVehicleAction : public MovementAction
|
class FlameLeviathanVehicleAction : public MovementAction
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -42,4 +46,64 @@ protected:
|
|||||||
bool AllMainVehiclesOnUse();
|
bool AllMainVehiclesOnUse();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
//
|
||||||
|
// 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
|
||||||
|
|||||||
226
src/strategy/raids/ulduar/RaidUlduarBossHelper.cpp
Normal file
226
src/strategy/raids/ulduar/RaidUlduarBossHelper.cpp
Normal file
@@ -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<ObjectGuid, time_t> RazorscaleBossHelper::_harpoonCooldowns;
|
||||||
|
// Prevent role assignment spam
|
||||||
|
std::unordered_map<ObjectGuid, std::time_t> 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<float>::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::HarpoonData>& RazorscaleBossHelper::GetHarpoonData()
|
||||||
|
{
|
||||||
|
static const std::vector<HarpoonData> 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<Player*> 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);
|
||||||
|
}
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H
|
#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
#include <cmath>
|
||||||
|
#include <ctime>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "AiObject.h"
|
#include "AiObject.h"
|
||||||
#include "AiObjectContext.h"
|
#include "AiObjectContext.h"
|
||||||
@@ -16,6 +21,88 @@
|
|||||||
|
|
||||||
const uint32 ULDUAR_MAP_ID = 603;
|
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<HarpoonData>& 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<ObjectGuid, std::time_t> _lastRoleSwapTime;
|
||||||
|
|
||||||
|
// The cooldown that applies to every bot
|
||||||
|
static const std::time_t _roleSwapCooldown = 10;
|
||||||
|
|
||||||
|
static std::unordered_map<ObjectGuid, time_t> _harpoonCooldowns;
|
||||||
|
};
|
||||||
|
|
||||||
// template <class BossAiType>
|
// template <class BossAiType>
|
||||||
// class GenericBossHelper : public AiObject
|
// class GenericBossHelper : public AiObject
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
void RaidUlduarStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
void RaidUlduarStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
// Flame Leviathan
|
// Flame Leviathan
|
||||||
|
//
|
||||||
triggers.push_back(new TriggerNode(
|
triggers.push_back(new TriggerNode(
|
||||||
"flame leviathan vehicle near",
|
"flame leviathan vehicle near",
|
||||||
NextAction::array(0, new NextAction("flame leviathan enter vehicle", ACTION_RAID + 2), nullptr)));
|
NextAction::array(0, new NextAction("flame leviathan enter vehicle", ACTION_RAID + 2), nullptr)));
|
||||||
@@ -12,7 +14,38 @@ void RaidUlduarStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
|
|||||||
triggers.push_back(new TriggerNode(
|
triggers.push_back(new TriggerNode(
|
||||||
"flame leviathan on vehicle",
|
"flame leviathan on vehicle",
|
||||||
NextAction::array(0, new NextAction("flame leviathan vehicle", ACTION_RAID + 1), nullptr)));
|
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<Multiplier*>& multipliers)
|
void RaidUlduarStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)
|
||||||
|
|||||||
@@ -17,11 +17,25 @@ public:
|
|||||||
{
|
{
|
||||||
creators["flame leviathan on vehicle"] = &RaidUlduarTriggerContext::flame_leviathan_on_vehicle;
|
creators["flame leviathan on vehicle"] = &RaidUlduarTriggerContext::flame_leviathan_on_vehicle;
|
||||||
creators["flame leviathan vehicle near"] = &RaidUlduarTriggerContext::flame_leviathan_vehicle_near;
|
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:
|
private:
|
||||||
static Trigger* flame_leviathan_on_vehicle(PlayerbotAI* ai) { return new FlameLeviathanOnVehicleTrigger(ai); }
|
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* 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
|
#endif
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#include "RaidUlduarTriggers.h"
|
#include "RaidUlduarTriggers.h"
|
||||||
|
|
||||||
#include "EventMap.h"
|
#include "EventMap.h"
|
||||||
|
#include "GameObject.h"
|
||||||
#include "Object.h"
|
#include "Object.h"
|
||||||
|
#include "PlayerbotAI.h"
|
||||||
#include "Playerbots.h"
|
#include "Playerbots.h"
|
||||||
|
#include "RaidUlduarBossHelper.h"
|
||||||
#include "RaidUlduarScripts.h"
|
#include "RaidUlduarScripts.h"
|
||||||
#include "ScriptedCreature.h"
|
#include "ScriptedCreature.h"
|
||||||
#include "SharedDefines.h"
|
#include "SharedDefines.h"
|
||||||
@@ -43,3 +46,198 @@ bool FlameLeviathanVehicleNearTrigger::IsActive()
|
|||||||
|
|
||||||
return true;
|
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<GuidVector>("attackers")->Get();
|
||||||
|
if (attackers.empty())
|
||||||
|
{
|
||||||
|
return true; // No attackers implies flying alone
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Unit*> 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<RazorscaleBossHelper::HarpoonData>& 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
#include "RaidUlduarBossHelper.h"
|
#include "RaidUlduarBossHelper.h"
|
||||||
#include "Trigger.h"
|
#include "Trigger.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Flame Levi
|
||||||
|
//
|
||||||
class FlameLeviathanOnVehicleTrigger : public Trigger
|
class FlameLeviathanOnVehicleTrigger : public Trigger
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -22,4 +24,56 @@ public:
|
|||||||
bool IsActive() override;
|
bool IsActive() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
//
|
||||||
|
// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user