Files
mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp
bashermens 13fff46fa0 Improper singletons migration to clean Meyer's singletons (cherry-pick) (#2082)
# Pull Request

- Applies the clean and corrected singletons, Meyer pattern. (cherry
picked from @SmashingQuasar )

Testing by just playing the game in various ways. Been tested by myself
@Celandriel and @SmashingQuasar
---

## Complexity & Impact

- Does this change add new decision branches?
    - [x] No
    - [ ] Yes (**explain below**)

- Does this change increase per-bot or per-tick processing?
    - [x] No
    - [ ] Yes (**describe and justify impact**)

- Could this logic scale poorly under load?
    - [x] No
    - [ ] Yes (**explain why**)

---

## Defaults & Configuration

- Does this change modify default bot behavior?
    - [x] No
    - [ ] Yes (**explain why**)

---

## AI Assistance

- Was AI assistance (e.g. ChatGPT or similar tools) used while working
on this change?
    - [x] No
    - [ ] Yes (**explain below**)
---

## Final Checklist

- [x] Stability is not compromised
- [x] Performance impact is understood, tested, and acceptable
- [x] Added logic complexity is justified and explained
- [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.

---------

Co-authored-by: Nicolas Lebacq <nicolas.cordier@outlook.com>
Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2026-01-30 21:49:37 +01:00

1494 lines
50 KiB
C++

#include "RaidKarazhanActions.h"
#include "RaidKarazhanHelpers.h"
#include "Playerbots.h"
#include "PlayerbotTextMgr.h"
using namespace KarazhanHelpers;
// Trash
// Mana Warps blow up when they die for massive raid damage
// But they cannot cast the ability if they are stunned
bool ManaWarpStunCreatureBeforeWarpBreachAction::Execute(Event event)
{
Unit* manaWarp = GetFirstAliveUnitByEntry(botAI, NPC_MANA_WARP);
if (!manaWarp)
return false;
static const std::array<const char*, 8> spells =
{
"bash",
"concussion blow",
"hammer of justice",
"kidney shot",
"maim",
"revenge stun",
"shadowfury",
"shockwave"
};
for (const char* spell : spells)
{
if (botAI->CanCastSpell(spell, manaWarp))
return botAI->CastSpell(spell, manaWarp);
}
return false;
}
// Attumen the Huntsman
// Prioritize Midnight until Attumen is mounted
bool AttumenTheHuntsmanMarkTargetAction::Execute(Event event)
{
Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED);
if (attumenMounted)
{
if (IsInstanceTimerManager(botAI, bot))
MarkTargetWithStar(bot, attumenMounted);
SetRtiTarget(botAI, "star", attumenMounted);
if (bot->GetTarget() != attumenMounted->GetGUID())
{
bot->SetTarget(attumenMounted->GetGUID());
return Attack(attumenMounted);
}
}
else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"))
{
if (IsInstanceTimerManager(botAI, bot))
MarkTargetWithStar(bot, midnight);
if (!botAI->IsAssistTankOfIndex(bot, 0))
{
SetRtiTarget(botAI, "star", midnight);
if (bot->GetTarget() != midnight->GetGUID())
{
bot->SetTarget(midnight->GetGUID());
return Attack(midnight);
}
}
}
return false;
}
// Off tank should move Attumen out of the way so he doesn't cleave bots
bool AttumenTheHuntsmanSplitBossesAction::Execute(Event event)
{
Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight");
if (!midnight)
return false;
Unit* attumen = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN);
if (!attumen)
return false;
MarkTargetWithSquare(bot, attumen);
SetRtiTarget(botAI, "square", attumen);
if (bot->GetVictim() != attumen)
return Attack(attumen);
if (attumen->GetVictim() == bot && midnight->GetVictim() != bot)
{
const float safeDistance = 6.0f;
Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance);
if (nearestPlayer && attumen->GetExactDist2d(nearestPlayer) < safeDistance)
return MoveFromGroup(safeDistance + 2.0f);
}
return false;
}
// Stack behind mounted Attumen (inside minimum range of Berserker Charge)
bool AttumenTheHuntsmanStackBehindAction::Execute(Event event)
{
Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED);
if (!attumenMounted)
return false;
const float distanceBehind = botAI->IsRanged(bot) ? 6.0f : 2.0f;
float orientation = attumenMounted->GetOrientation() + M_PI;
float rearX = attumenMounted->GetPositionX() + std::cos(orientation) * distanceBehind;
float rearY = attumenMounted->GetPositionY() + std::sin(orientation) * distanceBehind;
if (bot->GetExactDist2d(rearX, rearY) > 1.0f)
{
return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, attumenMounted->GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
// Reset timer for bots to pause DPS when Attumen mounts Midnight
bool AttumenTheHuntsmanManageDpsTimerAction::Execute(Event event)
{
Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight");
if (!midnight)
return false;
const uint32 instanceId = midnight->GetMap()->GetInstanceId();
if (midnight && midnight->GetHealth() == midnight->GetMaxHealth())
attumenDpsWaitTimer.erase(instanceId);
// Midnight is still present as a separate (invisible) unit after Attumen mounts
// So this block can be reached
Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED);
if (!attumenMounted)
return false;
const time_t now = std::time(nullptr);
if (attumenMounted)
attumenDpsWaitTimer.try_emplace(instanceId, now);
return false;
}
// Moroes
bool MoroesMainTankAttackBossAction::Execute(Event event)
{
Unit* moroes = AI_VALUE2(Unit*, "find target", "moroes");
if (!moroes)
return false;
MarkTargetWithCircle(bot, moroes);
SetRtiTarget(botAI, "circle", moroes);
if (bot->GetVictim() != moroes)
return Attack(moroes);
return false;
}
// Mark targets with skull in the recommended kill order
bool MoroesMarkTargetAction::Execute(Event event)
{
Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe");
Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi");
Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck");
Unit* rafe = AI_VALUE2(Unit*, "find target", "baron rafe dreuger");
Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris");
Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference");
Unit* target = GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin});
if (target)
{
if (IsInstanceTimerManager(botAI, bot))
MarkTargetWithSkull(bot, target);
SetRtiTarget(botAI, "skull", target);
}
return false;
}
// Maiden of Virtue
// Tank the boss in the center of the room
// Move to healers after Repentenace to break the stun
bool MaidenOfVirtueMoveBossToHealerAction::Execute(Event event)
{
Unit* maiden = AI_VALUE2(Unit*, "find target", "maiden of virtue");
if (!maiden)
return false;
if (bot->GetVictim() != maiden)
return Attack(maiden);
Unit* healer = nullptr;
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!member || !member->IsAlive() || !botAI->IsHeal(member) ||
!member->HasAura(SPELL_REPENTANCE))
continue;
healer = member;
break;
}
}
if (healer)
{
float angle = healer->GetOrientation();
float targetX = healer->GetPositionX() + std::cos(angle) * 6.0f;
float targetY = healer->GetPositionY() + std::sin(angle) * 6.0f;
{
return MoveTo(KARAZHAN_MAP_ID, targetX, targetY, healer->GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true, false);
}
}
const Position& position = MAIDEN_OF_VIRTUE_BOSS_POSITION;
const float maxDistance = 2.0f;
float distanceToPosition = maiden->GetExactDist2d(position);
if (distanceToPosition > maxDistance)
{
float dX = position.GetPositionX() - maiden->GetPositionX();
float dY = position.GetPositionY() - maiden->GetPositionY();
float mX = position.GetPositionX() + (dX / distanceToPosition) * maxDistance;
float mY = position.GetPositionY() + (dY / distanceToPosition) * maxDistance;
{
return MoveTo(KARAZHAN_MAP_ID, mX, mY, position.GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true, false);
}
}
return false;
}
// Spread out ranged DPS between the pillars
bool MaidenOfVirtuePositionRangedAction::Execute(Event event)
{
const uint8 maxIndex = 7;
uint8 index = 0;
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!member || !botAI->IsRanged(member))
continue;
if (member == bot)
break;
if (index >= maxIndex)
{
index = 0;
continue;
}
index++;
}
}
const Position& position = MAIDEN_OF_VIRTUE_RANGED_POSITION[index];
if (bot->GetExactDist2d(position) > 2.0f)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false);
}
return false;
}
// The Big Bad Wolf
// Tank the boss at the front left corner of the stage
bool BigBadWolfPositionBossAction::Execute(Event event)
{
Unit* wolf = AI_VALUE2(Unit*, "find target", "the big bad wolf");
if (!wolf)
return false;
if (bot->GetVictim() != wolf)
return Attack(wolf);
if (wolf->GetVictim() == bot)
{
const Position& position = BIG_BAD_WOLF_BOSS_POSITION;
float distanceToPosition = wolf->GetExactDist2d(position);
if (distanceToPosition > 2.0f)
{
float dX = position.GetPositionX() - wolf->GetPositionX();
float dY = position.GetPositionY() - wolf->GetPositionY();
float moveDist = std::min(5.0f, distanceToPosition);
float moveX = wolf->GetPositionX() + (dX / distanceToPosition) * moveDist;
float moveY = wolf->GetPositionY() + (dY / distanceToPosition) * moveDist;
return MoveTo(KARAZHAN_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true, true);
}
}
return false;
}
// Run away, little girl, run away
bool BigBadWolfRunAwayFromBossAction::Execute(Event event)
{
const ObjectGuid botGuid = bot->GetGUID();
uint8 index = bigBadWolfRunIndex.count(botGuid) ? bigBadWolfRunIndex[botGuid] : 0;
while (bot->GetExactDist2d(BIG_BAD_WOLF_RUN_POSITION[index].GetPositionX(),
BIG_BAD_WOLF_RUN_POSITION[index].GetPositionY()) < 1.0f)
{
index = (index + 1) % 4;
}
bigBadWolfRunIndex[botGuid] = index;
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
const Position& position = BIG_BAD_WOLF_RUN_POSITION[index];
return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false);
}
// Romulo and Julianne
// Keep the couple within 10% HP of each other
bool RomuloAndJulianneMarkTargetAction::Execute(Event event)
{
Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo");
if (!romulo)
return false;
Unit* julianne = AI_VALUE2(Unit*, "find target", "julianne");
if (!julianne)
return false;
Unit* target = nullptr;
const float maxPctDifference = 10.0f;
if (julianne->GetHealthPct() + maxPctDifference < romulo->GetHealthPct() || julianne->GetHealthPct() < 1.0f)
target = romulo;
else if (romulo->GetHealthPct() + maxPctDifference < julianne->GetHealthPct() || romulo->GetHealthPct() < 1.0f)
target = julianne;
else
target = (romulo->GetHealthPct() >= julianne->GetHealthPct()) ? romulo : julianne;
if (target)
MarkTargetWithSkull(bot, target);
return false;
}
// The Wizard of Oz
// Mark targets with skull in the recommended kill order
bool WizardOfOzMarkTargetAction::Execute(Event event)
{
Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee");
Unit* tito = AI_VALUE2(Unit*, "find target", "tito");
Unit* roar = AI_VALUE2(Unit*, "find target", "roar");
Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman");
Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead");
Unit* crone = AI_VALUE2(Unit*, "find target", "the crone");
Unit* target = GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone});
if (target)
MarkTargetWithSkull(bot, target);
return false;
}
// Mages spam Scorch on Strawman to disorient him
bool WizardOfOzScorchStrawmanAction::Execute(Event event)
{
Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman");
if (strawman && botAI->CanCastSpell("scorch", strawman))
return botAI->CastSpell("scorch", strawman);
return false;
}
// The Curator
// Prioritize destroying Astral Flares
bool TheCuratorMarkAstralFlareAction::Execute(Event event)
{
Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare");
if (!flare)
return false;
if (IsInstanceTimerManager(botAI, bot))
MarkTargetWithSkull(bot, flare);
SetRtiTarget(botAI, "skull", flare);
return false;
}
// Tank the boss in the center of the hallway near the Guardian's Library
// Main tank and off tank will attack the boss; others will focus on Astral Flares
bool TheCuratorPositionBossAction::Execute(Event event)
{
Unit* curator = AI_VALUE2(Unit*, "find target", "the curator");
if (!curator)
return false;
MarkTargetWithCircle(bot, curator);
SetRtiTarget(botAI, "circle", curator);
if (bot->GetVictim() != curator)
return Attack(curator);
if (curator->GetVictim() == bot)
{
const Position& position = THE_CURATOR_BOSS_POSITION;
float distanceToPosition = curator->GetExactDist2d(position);
if (distanceToPosition > 2.0f)
{
float dX = position.GetPositionX() - curator->GetPositionX();
float dY = position.GetPositionY() - curator->GetPositionY();
float moveDist = std::min(10.0f, distanceToPosition);
float moveX = position.GetPositionX() + (dX / distanceToPosition) * moveDist;
float moveY = position.GetPositionY() + (dY / distanceToPosition) * moveDist;
return MoveTo(KARAZHAN_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true, false);
}
}
return false;
}
// Spread out ranged DPS to avoid Arcing Sear damage
bool TheCuratorSpreadRangedAction::Execute(Event event)
{
const float minDistance = 5.0f;
Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance);
if (nearestPlayer)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return FleePosition(nearestPlayer->GetPosition(), minDistance);
}
return false;
}
// Terestian Illhoof
// Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof
bool TerestianIllhoofMarkTargetAction::Execute(Event event)
{
Unit* demonChains = AI_VALUE2(Unit*, "find target", "demon chains");
Unit* kilrek = AI_VALUE2(Unit*, "find target", "kil'rek");
Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof");
Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof});
if (target)
MarkTargetWithSkull(bot, target);
return false;
}
// Shade of Aran
// Run to the edge of the room to avoid Arcane Explosion
bool ShadeOfAranRunAwayFromArcaneExplosionAction::Execute(Event event)
{
Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran");
if (!aran)
return false;
const float safeDistance = 20.0f;
float distance = bot->GetDistance2d(aran);
if (distance < safeDistance)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveAway(aran, safeDistance - distance);
}
return false;
}
// I will not move when Flame Wreath is cast or the raid blows up
bool ShadeOfAranStopMovingDuringFlameWreathAction::Execute(Event event)
{
AI_VALUE(LastMovement&, "last movement").Set(nullptr);
if (bot->isMoving())
{
bot->GetMotionMaster()->Clear();
bot->StopMoving();
return true;
}
return false;
}
// Mark Conjured Elementals with skull so DPS can burn them down
bool ShadeOfAranMarkConjuredElementalAction::Execute(Event event)
{
Unit* elemental = GetFirstAliveUnitByEntry(botAI, NPC_CONJURED_ELEMENTAL);
if (elemental)
MarkTargetWithSkull(bot, elemental);
return false;
}
// Don't get closer than 11 yards to Aran to avoid counterspell
// Don't get farther than 15 yards from Aran to avoid getting stuck in alcoves
bool ShadeOfAranRangedMaintainDistanceAction::Execute(Event event)
{
Unit* aran = AI_VALUE2(Unit*, "find target", "shade of aran");
if (!aran)
return false;
Group* group = bot->GetGroup();
if (!group)
return false;
const float minDist = 11.0f;
const float maxDist = 15.0f;
const float ringIncrement = M_PI / 8;
const float distIncrement = 0.5f;
float bestX = 0, bestY = 0, bestMoveDist = std::numeric_limits<float>::max();
bool found = false;
for (float dist = minDist; dist <= maxDist; dist += distIncrement)
{
for (float angle = 0; angle < 2 * M_PI; angle += ringIncrement)
{
float x = aran->GetPositionX() + std::cos(angle) * dist;
float y = aran->GetPositionY() + std::sin(angle) * dist;
bool tooClose = false;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!member || member == bot || !member->IsAlive())
continue;
if (member->GetExactDist2d(x, y) < 3.0f)
{
tooClose = true;
break;
}
}
if (tooClose)
continue;
float moveDist = bot->GetExactDist2d(x, y);
if (moveDist < bestMoveDist)
{
bestMoveDist = moveDist;
bestX = x;
bestY = y;
found = true;
}
}
}
if (found && bestMoveDist > 0.5f)
{
return MoveTo(KARAZHAN_MAP_ID, bestX, bestY, bot->GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true, false);
}
return false;
}
// Netherspite
// One tank bot per phase will dance in and out of the red beam (5 seconds in, 5 seconds out)
// Tank bots will ignore void zones--their positioning is too important to risk losing beam control
bool NetherspiteBlockRedBeamAction::Execute(Event event)
{
Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite");
if (!netherspite)
return false;
Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f);
if (!redPortal)
return false;
const ObjectGuid botGuid = bot->GetGUID();
auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot);
bool isBlockingNow = (bot == redBlocker);
auto it = _wasBlockingRedBeam.find(botGuid);
bool wasBlocking = (it != _wasBlockingRedBeam.end()) ? it->second : false;
Position beamPos = GetPositionOnBeam(netherspite, redPortal, 18.0f);
if (isBlockingNow)
{
if (!wasBlocking)
{
std::map<std::string, std::string> placeholders{{"%player", bot->GetName()}};
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"netherspite_beam_blocking_red", "%player is moving to block the red beam!", placeholders);
bot->Yell(text, LANG_UNIVERSAL);
}
_wasBlockingRedBeam[botGuid] = true;
const uint8 intervalSecs = 5;
if (std::time(nullptr) - redBeamMoveTimer[botGuid] >= intervalSecs)
{
lastBeamMoveSideways[botGuid] = !lastBeamMoveSideways[botGuid];
redBeamMoveTimer[botGuid] = std::time(nullptr);
}
if (!lastBeamMoveSideways[botGuid])
return MoveTo(KARAZHAN_MAP_ID, beamPos.GetPositionX(), beamPos.GetPositionY(), beamPos.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false);
else
{
float length = netherspite->GetExactDist2d(redPortal);
if (length == 0.0f)
return false;
float dx = (redPortal->GetPositionX() - netherspite->GetPositionX()) / length;
float dy = (redPortal->GetPositionY() - netherspite->GetPositionY()) / length;
float perpDx = -dy;
float perpDy = dx;
float sideX = beamPos.GetPositionX() + perpDx * 3.0f;
float sideY = beamPos.GetPositionY() + perpDy * 3.0f;
float sideZ = beamPos.GetPositionZ();
return MoveTo(KARAZHAN_MAP_ID, sideX, sideY, sideZ, false, false, false, false,
MovementPriority::MOVEMENT_FORCED, true, false);
}
}
_wasBlockingRedBeam[botGuid] = false;
return false;
}
Position NetherspiteBlockRedBeamAction::GetPositionOnBeam(Unit* netherspite, Unit* portal, float distanceFromBoss)
{
float bx = netherspite->GetPositionX();
float by = netherspite->GetPositionY();
float bz = netherspite->GetPositionZ();
float px = portal->GetPositionX();
float py = portal->GetPositionY();
float dx = px - bx;
float dy = py - by;
float length = netherspite->GetExactDist2d(px, py);
if (length == 0.0f)
return Position(bx, by, bz);
dx /= length;
dy /= length;
float targetX = bx + dx * distanceFromBoss;
float targetY = by + dy * distanceFromBoss;
float targetZ = bz;
return Position(targetX, targetY, targetZ);
}
// Two non-Rogue/Warrior DPS bots will block the blue beam for each phase (swap at 25 debuff stacks)
// When avoiding void zones, blocking bots will move along the beam to continue blocking
bool NetherspiteBlockBlueBeamAction::Execute(Event event)
{
Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite");
if (!netherspite)
return false;
Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f);
if (!bluePortal)
return false;
const ObjectGuid botGuid = bot->GetGUID();
auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot);
bool isBlockingNow = (bot == blueBlocker);
auto it = _wasBlockingBlueBeam.find(botGuid);
bool wasBlocking = (it != _wasBlockingBlueBeam.end()) ? it->second : false;
if (wasBlocking && !isBlockingNow)
{
std::map<std::string, std::string> placeholders{{"%player", bot->GetName()}};
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"netherspite_beam_leaving_blue", "%player is leaving the blue beam--next blocker up!", placeholders);
bot->Yell(text, LANG_UNIVERSAL);
_wasBlockingBlueBeam[botGuid] = false;
}
if (isBlockingNow)
{
if (!wasBlocking)
{
std::map<std::string, std::string> placeholders{{"%player", bot->GetName()}};
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"netherspite_beam_blocking_blue", "%player is moving to block the blue beam!", placeholders);
bot->Yell(text, LANG_UNIVERSAL);
}
_wasBlockingBlueBeam[botGuid] = true;
float idealDistance = botAI->IsRanged(bot) ? 25.0f : 18.0f;
std::vector<Unit*> voidZones = GetAllVoidZones(botAI, bot);
float bx = netherspite->GetPositionX();
float by = netherspite->GetPositionY();
float bz = netherspite->GetPositionZ();
float px = bluePortal->GetPositionX();
float py = bluePortal->GetPositionY();
float dx = px - bx;
float dy = py - by;
float length = netherspite->GetExactDist2d(bluePortal);
if (length == 0.0f)
return false;
dx /= length;
dy /= length;
float bestDist = 150.0f;
Position bestPos;
bool found = false;
for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f)
{
float candidateX = bx + dx * dist;
float candidateY = by + dy * dist;
float candidateZ = bz;
if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f))
continue;
float distToIdeal = fabs(dist - idealDistance);
if (!found || distToIdeal < bestDist)
{
bestDist = distToIdeal;
bestPos = Position(candidateX, candidateY, candidateZ);
found = true;
}
}
if (found)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
_wasBlockingBlueBeam[botGuid] = false;
return false;
}
// Two healer bots will block the green beam for each phase (swap at 25 debuff stacks)
// OR one rogue or DPS warrior bot will block the green beam for an entire phase (if they begin the phase as the blocker)
// When avoiding void zones, blocking bots will move along the beam to continue blocking
bool NetherspiteBlockGreenBeamAction::Execute(Event event)
{
Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite");
if (!netherspite)
return false;
Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f);
if (!greenPortal)
return false;
const ObjectGuid botGuid = bot->GetGUID();
auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot);
bool isBlockingNow = (bot == greenBlocker);
auto it = _wasBlockingGreenBeam.find(botGuid);
bool wasBlocking = (it != _wasBlockingGreenBeam.end()) ? it->second : false;
if (wasBlocking && !isBlockingNow)
{
std::map<std::string, std::string> placeholders{{"%player", bot->GetName()}};
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"netherspite_beam_leaving_green", "%player is leaving the green beam--next blocker up!", placeholders);
bot->Yell(text, LANG_UNIVERSAL);
_wasBlockingGreenBeam[botGuid] = false;
}
if (isBlockingNow)
{
if (!wasBlocking)
{
std::map<std::string, std::string> placeholders{{"%player", bot->GetName()}};
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"netherspite_beam_blocking_green", "%player is moving to block the green beam!", placeholders);
bot->Yell(text, LANG_UNIVERSAL);
}
_wasBlockingGreenBeam[botGuid] = true;
std::vector<Unit*> voidZones = GetAllVoidZones(botAI, bot);
float bx = netherspite->GetPositionX();
float by = netherspite->GetPositionY();
float bz = netherspite->GetPositionZ();
float px = greenPortal->GetPositionX();
float py = greenPortal->GetPositionY();
float dx = px - bx;
float dy = py - by;
float length = netherspite->GetExactDist2d(greenPortal);
if (length == 0.0f)
return false;
dx /= length;
dy /= length;
float bestDist = 150.0f;
Position bestPos;
bool found = false;
for (float dist = 18.0f; dist <= 30.0f; dist += 0.5f)
{
float candidateX = bx + dx * dist;
float candidateY = by + dy * dist;
float candidateZ = bz;
if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f))
continue;
float distToIdeal = fabs(dist - 18.0f);
if (!found || distToIdeal < bestDist)
{
bestDist = distToIdeal;
bestPos = Position(candidateX, candidateY, candidateZ);
found = true;
}
}
if (found)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
_wasBlockingGreenBeam[botGuid] = false;
return false;
}
// All bots not currently blocking a beam will avoid beams and void zones
bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event event)
{
Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite");
if (!netherspite)
return false;
auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot);
std::vector<Unit*> voidZones = GetAllVoidZones(botAI, bot);
bool nearVoidZone = !IsSafePosition(bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), voidZones, 4.0f);
std::vector<BeamAvoid> beams;
Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f);
Unit* bluePortal = bot->FindNearestCreature(NPC_BLUE_PORTAL, 150.0f);
Unit* greenPortal = bot->FindNearestCreature(NPC_GREEN_PORTAL, 150.0f);
if (redPortal)
{
float length = netherspite->GetExactDist2d(redPortal);
beams.push_back({redPortal, 0.0f, length});
}
if (bluePortal)
{
float length = netherspite->GetExactDist2d(bluePortal);
beams.push_back({bluePortal, 0.0f, length});
}
if (greenPortal)
{
float length = netherspite->GetExactDist2d(greenPortal);
beams.push_back({greenPortal, 0.0f, length});
}
bool nearBeam = !IsAwayFromBeams(bot->GetPositionX(), bot->GetPositionY(), beams, netherspite);
if (!nearVoidZone && !nearBeam)
return false;
const float minMoveDist = 2.0f;
const float minMoveDistSq = minMoveDist * minMoveDist;
const float maxSearchDist = 30.0f, stepAngle = M_PI/18.0f, stepDist = 0.5f;
float netherspiteZ = netherspite->GetPositionZ();
Position bestCandidate;
float bestDistSq = std::numeric_limits<float>::max();
bool found = false;
const float botX = bot->GetPositionX();
const float botY = bot->GetPositionY();
for (float angle = 0; angle < 2 * M_PI; angle += stepAngle)
{
for (float dist = 2.0f; dist <= maxSearchDist; dist += stepDist)
{
float cx = botX + std::cos(angle) * dist;
float cy = botY + std::sin(angle) * dist;
float cz = netherspiteZ;
if (!IsSafePosition(cx, cy, cz, voidZones, 4.0f) ||
!IsAwayFromBeams(cx, cy, beams, netherspite))
continue;
float dx = cx - botX;
float dy = cy - botY;
float moveDistSq = dx*dx + dy*dy;
if (moveDistSq < minMoveDistSq)
continue;
if (!found || moveDistSq < bestDistSq)
{
bestCandidate = Position(cx, cy, cz);
bestDistSq = moveDistSq;
found = true;
}
}
}
if (found)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, bestCandidate.GetPositionX(), bestCandidate.GetPositionY(),
bestCandidate.GetPositionZ(), false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true, false);
}
return false;
}
bool NetherspiteAvoidBeamAndVoidZoneAction::IsAwayFromBeams(
float x, float y, const std::vector<BeamAvoid>& beams, Unit* netherspite)
{
for (auto const& beam : beams)
{
float bx = netherspite->GetPositionX(), by = netherspite->GetPositionY();
float px = beam.portal->GetPositionX(), py = beam.portal->GetPositionY();
float dx = px - bx, dy = py - by;
float length = netherspite->GetExactDist2d(beam.portal);
if (length == 0.0f)
continue;
dx /= length; dy /= length;
float botdx = x - bx, botdy = y - by;
float distanceAlongBeam = (botdx * dx + botdy * dy);
float beamX = bx + dx * distanceAlongBeam, beamY = by + dy * distanceAlongBeam;
float distToBeam = sqrt((x - beamX) * (x - beamX) + (y - beamY) * (y - beamY));
if (distToBeam < 5.0f && distanceAlongBeam > beam.minDist && distanceAlongBeam < beam.maxDist)
return false;
}
return true;
}
bool NetherspiteBanishPhaseAvoidVoidZoneAction::Execute(Event event)
{
std::vector<Unit*> voidZones = GetAllVoidZones(botAI, bot);
for (Unit* vz : voidZones)
{
if (vz->GetEntry() == NPC_VOID_ZONE && bot->GetExactDist2d(vz) < 4.0f)
return FleePosition(vz->GetPosition(), 4.0f);
}
return false;
}
bool NetherspiteManageTimersAndTrackersAction::Execute(Event event)
{
Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite");
if (!netherspite)
return false;
const uint32 instanceId = netherspite->GetMap()->GetInstanceId();
const ObjectGuid botGuid = bot->GetGUID();
const time_t now = std::time(nullptr);
// DpsWaitTimer is for pausing DPS during phase transitions
// redBeamMoveTimer and lastBeamMoveSideways are for tank dancing in/out of the red beam
if (netherspite->GetHealth() == netherspite->GetMaxHealth() &&
!netherspite->HasAura(SPELL_GREEN_BEAM_HEAL))
{
if (IsInstanceTimerManager(botAI, bot))
netherspiteDpsWaitTimer.insert_or_assign(instanceId, now);
if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF))
{
redBeamMoveTimer.erase(botGuid);
lastBeamMoveSideways.erase(botGuid);
}
}
else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED))
{
if (IsInstanceTimerManager(botAI, bot))
netherspiteDpsWaitTimer.erase(instanceId);
if (botAI->IsTank(bot))
{
redBeamMoveTimer.erase(botGuid);
lastBeamMoveSideways.erase(botGuid);
}
}
else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED))
{
if (IsInstanceTimerManager(botAI, bot))
netherspiteDpsWaitTimer.try_emplace(instanceId, now);
if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF))
{
redBeamMoveTimer.try_emplace(botGuid, now);
lastBeamMoveSideways.try_emplace(botGuid, false);
}
}
return false;
}
// Move away from the boss to avoid Shadow Nova when Enfeebled
// Do not cross within Infernal Hellfire radius while doing so
bool PrinceMalchezaarEnfeebledAvoidHazardAction::Execute(Event event)
{
Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar");
if (!malchezaar)
return false;
std::vector<Unit*> infernals = GetSpawnedInfernals(botAI);
const float minSafeBossDistance = 34.0f;
const float minSafeBossDistanceSq = minSafeBossDistance * minSafeBossDistance;
const float maxSafeBossDistance = 60.0f;
const float safeInfernalDistance = 23.0f;
const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance;
const float distIncrement = 0.5f;
const uint8 numAngles = 64;
float bx = bot->GetPositionX();
float by = bot->GetPositionY();
float bz = bot->GetPositionZ();
float malchezaarX = malchezaar->GetPositionX();
float malchezaarY = malchezaar->GetPositionY();
float malchezaarZ = malchezaar->GetPositionZ();
float bestMoveDistSq = std::numeric_limits<float>::max();
float bestDestX = 0.0f, bestDestY = 0.0f, bestDestZ = bz;
bool found = false;
for (int i = 0; i < numAngles; ++i)
{
float angle = (2 * M_PI * i) / numAngles;
float dx = std::cos(angle);
float dy = std::sin(angle);
for (float dist = minSafeBossDistance; dist <= maxSafeBossDistance; dist += distIncrement)
{
float x = malchezaarX + dx * dist;
float y = malchezaarY + dy * dist;
float destZ = malchezaarZ;
float destX = x, destY = y;
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bx, by, bz, destX, destY, destZ, true))
continue;
float ddx = destX - malchezaarX;
float ddy = destY - malchezaarY;
float distFromBossSq = ddx*ddx + ddy*ddy;
if (distFromBossSq < minSafeBossDistanceSq)
continue;
bool pathSafe = IsStraightPathSafe(Position(bx, by, bz), Position(destX, destY, destZ),
infernals, safeInfernalDistance, distIncrement);
float mdx = destX - bx;
float mdy = destY - by;
float moveDistSq = mdx*mdx + mdy*mdy;
if (pathSafe && moveDistSq < bestMoveDistSq)
{
bestMoveDistSq = moveDistSq;
bestDestX = destX;
bestDestY = destY;
bestDestZ = destZ;
found = true;
}
}
}
if (found)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false,
MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
// Move away from infernals while staying within range of the boss
// Prioritize finding a safe path to the new location, but will fallback to just finding a safe location if needed
bool PrinceMalchezaarNonTankAvoidInfernalAction::Execute(Event event)
{
Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar");
if (!malchezaar)
return false;
std::vector<Unit*> infernals = GetSpawnedInfernals(botAI);
const float safeInfernalDistance = 23.0f;
const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance;
const float maxSafeBossDistance = 35.0f;
float bx = bot->GetPositionX();
float by = bot->GetPositionY();
float bz = bot->GetPositionZ();
float malchezaarX = malchezaar->GetPositionX();
float malchezaarY = malchezaar->GetPositionY();
float malchezaarZ = malchezaar->GetPositionZ();
bool nearInfernal = false;
for (Unit* infernal : infernals)
{
float dx = bx - infernal->GetPositionX();
float dy = by - infernal->GetPositionY();
float infernalDistSq = dx*dx + dy*dy;
if (infernalDistSq < safeInfernalDistanceSq)
{
nearInfernal = true;
break;
}
}
float bestDestX = bx, bestDestY = by, bestDestZ = bz;
bool found = false;
if (nearInfernal)
{
const float distIncrement = 0.5f;
const uint8 numAngles = 64;
// 1. Try to find a safe position with a safe path
found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ,
infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance,
true, bestDestX, bestDestY, bestDestZ);
// 2. Fallback: try to find a safe position (ignore path safety)
if (!found)
{
found = TryFindSafePositionWithSafePath(bot, bx, by, bz, malchezaarX, malchezaarY, malchezaarZ,
infernals, safeInfernalDistance, distIncrement, numAngles, maxSafeBossDistance,
false, bestDestX, bestDestY, bestDestZ);
}
if (found)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false,
MovementPriority::MOVEMENT_FORCED, true, false);
}
}
return false;
}
// This is similar to the non-tank avoid infernal action, but the movement is based on the bot's location
// And the safe distance from infernals is larger to give melee more room to maneuver
bool PrinceMalchezaarMainTankMovementAction::Execute(Event event)
{
Unit* malchezaar = AI_VALUE2(Unit*, "find target", "prince malchezaar");
if (!malchezaar)
return false;
if (bot->GetVictim() != malchezaar)
return Attack(malchezaar);
std::vector<Unit*> infernals = GetSpawnedInfernals(botAI);
const float safeInfernalDistance = 30.0f;
const float safeInfernalDistanceSq = safeInfernalDistance * safeInfernalDistance;
const float maxSampleDist = 75.0f;
float bx = bot->GetPositionX();
float by = bot->GetPositionY();
float bz = bot->GetPositionZ();
bool nearInfernal = false;
for (Unit* infernal : infernals)
{
float dx = bx - infernal->GetPositionX();
float dy = by - infernal->GetPositionY();
float infernalDistSq = dx*dx + dy*dy;
if (infernalDistSq < safeInfernalDistanceSq)
{
nearInfernal = true;
break;
}
}
float bestDestX = bx, bestDestY = by, bestDestZ = bz;
bool found = false;
if (nearInfernal)
{
const float distIncrement = 0.5f;
const uint8 numAngles = 64;
// 1. Try to find a safe position with a safe path
found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz,
infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist,
true, bestDestX, bestDestY, bestDestZ);
// 2. Fallback: try to find a safe position (ignore path safety)
if (!found)
{
found = TryFindSafePositionWithSafePath( bot, bx, by, bz, bx, by, bz,
infernals, safeInfernalDistance, distIncrement, numAngles, maxSampleDist,
false, bestDestX, bestDestY, bestDestZ);
}
if (found)
{
bot->AttackStop();
return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false,
MovementPriority::MOVEMENT_FORCED, true, false);
}
}
return false;
}
// The tank position is near the Southeastern area of the Master's Terrace
// The tank moves Nightbane into position in two steps to try to get Nightbane to face sideways to the raid
bool NightbaneGroundPhasePositionBossAction::Execute(Event event)
{
Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane");
if (!nightbane)
return false;
MarkTargetWithSkull(bot, nightbane);
if (bot->GetVictim() != nightbane)
return Attack(nightbane);
const ObjectGuid botGuid = bot->GetGUID();
uint8 step = nightbaneTankStep.count(botGuid) ? nightbaneTankStep[botGuid] : 0;
if (nightbane->GetVictim() == bot)
{
const Position tankPositions[2] =
{
NIGHTBANE_TRANSITION_BOSS_POSITION,
NIGHTBANE_FINAL_BOSS_POSITION
};
const Position& position = tankPositions[step];
const float maxDistance = 0.5f;
float distanceToTarget = bot->GetExactDist2d(position);
if ((distanceToTarget > maxDistance) && bot->IsWithinMeleeRange(nightbane))
return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, true);
if (step == 0 && distanceToTarget <= maxDistance)
nightbaneTankStep[botGuid] = 1;
if (step == 1 && distanceToTarget <= maxDistance)
{
float orientation = atan2(nightbane->GetPositionY() - bot->GetPositionY(),
nightbane->GetPositionX() - bot->GetPositionX());
bot->SetFacingTo(orientation);
}
}
return false;
}
// Ranged bots rotate between 3 positions to avoid standing in Charred Earth, which lasts for
// 30s and has a minimum cooldown of 18s (so there can be 2 active at once)
// Ranged positions are near the Northeastern door to the tower
bool NightbaneGroundPhaseRotateRangedPositionsAction::Execute(Event event)
{
const ObjectGuid botGuid = bot->GetGUID();
uint8 index = nightbaneRangedStep.count(botGuid) ? nightbaneRangedStep[botGuid] : 0;
const Position rangedPositions[3] =
{
NIGHTBANE_RANGED_POSITION1,
NIGHTBANE_RANGED_POSITION2,
NIGHTBANE_RANGED_POSITION3
};
const Position& position = rangedPositions[index];
const float maxDistance = 2.0f;
float distanceToTarget = bot->GetExactDist2d(position);
if (distanceToTarget <= maxDistance &&
bot->HasAura(SPELL_CHARRED_EARTH) && !bot->HasAura(SPELL_BELLOWING_ROAR))
{
index = (index + 1) % 3;
nightbaneRangedStep[botGuid] = index;
const Position& newPosition = rangedPositions[index];
float newDistanceToTarget = bot->GetExactDist2d(newPosition);
if (newDistanceToTarget > maxDistance)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, newPosition.GetPositionX(), newPosition.GetPositionY(), newPosition.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
if (distanceToTarget > maxDistance)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
// For countering Bellowing Roars during the ground phase
bool NightbaneCastFearWardOnMainTankAction::Execute(Event event)
{
Player* mainTank = nullptr;
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (member && botAI->IsMainTank(member))
{
mainTank = member;
break;
}
}
}
if (mainTank && botAI->CanCastSpell("fear ward", mainTank))
return botAI->CastSpell("fear ward", mainTank);
return false;
}
// Put pets on passive during the flight phase so they don't try to chase Nightbane off the map
bool NightbaneControlPetAggressionAction::Execute(Event event)
{
Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane");
if (!nightbane)
return false;
Pet* pet = bot->GetPet();
if (!pet)
return false;
if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z && pet->GetReactState() == REACT_PASSIVE)
pet->SetReactState(REACT_DEFENSIVE);
if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z && pet->GetReactState() != REACT_PASSIVE)
{
pet->AttackStop();
pet->SetReactState(REACT_PASSIVE);
}
return false;
}
// 1. Stack at the "Flight Stack Position" near Nightbane so he doesn't use Fireball Barrage
// 2. Once Rain of Bones hits, the whole party moves to a new stack position
// This action lasts for the first 35 seconds of the flight phase, after which Nightbane gets
// ready to land, and the player will need to lead the bots over near the ground phase position
bool NightbaneFlightPhaseMovementAction::Execute(Event event)
{
Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane");
if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z)
return false;
MarkTargetWithMoon(bot, nightbane);
Unit* botTarget = botAI->GetUnit(bot->GetTarget());
if (botTarget && botTarget == nightbane)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
}
const ObjectGuid botGuid = bot->GetGUID();
bool hasRainOfBones = bot->HasAura(SPELL_RAIN_OF_BONES);
if (hasRainOfBones)
nightbaneRainOfBonesHit[botGuid] = true;
float destX, destY, destZ;
if (nightbaneRainOfBonesHit[botGuid])
{
destX = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionX();
destY = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionY();
destZ = NIGHTBANE_RAIN_OF_BONES_POSITION.GetPositionZ();
}
else
{
destX = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionX();
destY = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionY();
destZ = NIGHTBANE_FLIGHT_STACK_POSITION.GetPositionZ();
}
if (bot->GetExactDist2d(destX, destY) > 2.0f)
{
bot->AttackStop();
bot->InterruptNonMeleeSpells(true);
return MoveTo(KARAZHAN_MAP_ID, destX, destY, destZ, false, false, false, false,
MovementPriority::MOVEMENT_FORCED, true, false);
}
return false;
}
bool NightbaneManageTimersAndTrackersAction::Execute(Event event)
{
Unit* nightbane = AI_VALUE2(Unit*, "find target", "nightbane");
if (!nightbane)
return false;
const uint32 instanceId = nightbane->GetMap()->GetInstanceId();
const ObjectGuid botGuid = bot->GetGUID();
const time_t now = std::time(nullptr);
// Erase DPS wait timer and tank and ranged position tracking on encounter reset
if (nightbane->GetHealth() == nightbane->GetMaxHealth())
{
if (botAI->IsMainTank(bot))
nightbaneTankStep.erase(botGuid);
if (botAI->IsRanged(bot))
nightbaneRangedStep.erase(botGuid);
if (IsInstanceTimerManager(botAI, bot))
nightbaneDpsWaitTimer.erase(instanceId);
}
// Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer
else if (nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z)
{
nightbaneRainOfBonesHit.erase(botGuid);
if (IsInstanceTimerManager(botAI, bot))
{
nightbaneFlightPhaseStartTimer.erase(instanceId);
nightbaneDpsWaitTimer.try_emplace(instanceId, now);
}
}
// Erase DPS wait timer and tank and ranged position tracking and start flight phase timer
// at beginning of flight phase
else if (nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z)
{
if (botAI->IsMainTank(bot))
nightbaneTankStep.erase(botGuid);
if (botAI->IsRanged(bot))
nightbaneRangedStep.erase(botGuid);
if (IsInstanceTimerManager(botAI, bot))
{
nightbaneDpsWaitTimer.erase(instanceId);
nightbaneFlightPhaseStartTimer.try_emplace(instanceId, now);
}
}
return false;
}