mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp
Crow 79562be2e5
Fix Hunter Aspect Switching + Trigger Cleanups (#2203)
# Pull Request

Note: When I reference Aspect of the Hawk below, it also means Aspect of
the Dragonhawk (the code will use Dragonhawk if the Hunter has it, Hawk
if not, they share actions, triggers, and strategies).

Hunter Aspects are currently bugged. All Hunters, regardless of spec or
strategy, are hardcoded to use Aspect of the Hawk when mana is at 70%+
and Aspect of the Viper when mana drops to "lowMana" from the config
(default is 15%), divided by 2.

This means the following:

- Hawk (bdps) and Viper (bmana) strategies are useless
- Pack (bspeed) and Wild (rnature) strategies are applied, but bots will
rapidly switch back and forth between Pack/Wild and Hawk/Viper,
depending on strategy and mana level.

This PR addresses the issues by doing the following:

- Global Hawk strategy is removed. Now you need to set bdps for Hunters
to use Hawk, but bdps remains the default Aspect strategy for all
Hunters.
- Dedicated Viper strategy is removed, leaving the global strategy.
However, Viper will be used (when lowMana/2) ONLY if the bot is set to
bdps. If the bot has the Wild or Pack strategy, they will not switch to
Viper at all. I did this because I am assuming if you are using Wild or
Pack, you need them for reasons other than to pump DPS.
- The threshold to switch back to Hawk is lowered from 70% to 60%. The
gap between lowMana/2 and 60% is now filled--if bdps is on, Hunters will
switch to Hawk whenever above the Viper threshold, _except_ for when
they have the Viper aura, in which case they will not switch to Hawk
until 60% mana. This lets the Hunter build back mana before swapping
back to Hawk (more like general player behavior) while still letting
them swap from other Aspects to Hawk without needing to be all the way
at 60% mana.
- Gets rid of a weird condition in the Hawk trigger that would make it
so that Hunters would switch to Hawk when at exactly 0 mana. I'm not
sure what the point of that is.

Also, I refactored the triggers a bit because I noticed there was some
dead code in there. I didn't do a comprehensive refactor, but there was
a lot of stuff that clearly didn't make sense even to my eyes, like
back-to-back returns. I think there's more unnecessary code even just in
the triggers, but I didn't want to get too into the weeds with this PR.

---

## Design Philosophy

We prioritize **stability, performance, and predictability** over
behavioral realism.
Complex player-mimicking logic is intentionally limited due to its
negative impact on scalability, maintainability, and
long-term robustness.

Excessive processing overhead can lead to server hiccups, increased CPU
usage, and degraded performance for all
participants. Because every action and
decision tree is executed **per bot and per trigger**, even small
increases in logic complexity can scale poorly and
negatively affect both players and
world (random) bots. Bots are not expected to behave perfectly, and
perfect simulation of human decision-making is not a
project goal. Increased behavioral
realism often introduces disproportionate cost, reduced predictability,
and significantly higher maintenance overhead.

Every additional branch of logic increases long-term responsibility. All
decision paths must be tested, validated, and
maintained continuously as the system evolves.
If advanced or AI-intensive behavior is introduced, the **default
configuration must remain the lightweight decision
model**. More complex behavior should only be
available as an **explicit opt-in option**, clearly documented as having
a measurable performance cost.

Principles:

- **Stability before intelligence**  
  A stable system is always preferred over a smarter one.

- **Performance is a shared resource**  
  Any increase in bot cost affects all players and all bots.

- **Simple logic scales better than smart logic**  
Predictable behavior under load is more valuable than perfect decisions.

- **Complexity must justify itself**  
  If a feature cannot clearly explain its cost, it should not exist.

- **Defaults must be cheap**  
  Expensive behavior must always be optional and clearly communicated.

- **Bots should look reasonable, not perfect**  
  The goal is believable behavior, not human simulation.

Before submitting, confirm that this change aligns with those
principles.

---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended
behavior?
- Describe the **cheapest implementation** that produces an acceptable
result?
- Describe the **runtime cost** when this logic executes across many
bots?

I don't expect there to be any impact on costs, and if anything this PR
removes some unneeded checks from triggers.

---

## How to Test the Changes

- Step-by-step instructions to test the change
- Any required setup (e.g. multiple players, bots, specific
configuration)
- Expected behavior and how to verify it

The easiest way is to go shoot a dummy with Volley until low on mana and
then toggle on selfbot. You can do this with various Aspects active to
test.

## 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?
- - [ ] No
- - [X] Yes (**explain why**)

Described above. Default behavior is broken.

If this introduces more advanced or AI-heavy logic:
- - [X] Lightweight mode remains the default
- - [ ] More complex behavior is optional and thereby configurable
---

## AI Assistance

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

I asked Claude some questions about the triggers to make sure I didn't
screw anything up.

If yes, please specify:

- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.)
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation)
- Which parts of the change were influenced or generated
- Whether the result was manually reviewed and adapted

AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
Any AI-influenced changes must be verified against existing CORE and PB
logic. We expect contributors to be honest
about what they do and do not understand.

---

## 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: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
2026-04-03 13:24:37 -07:00

873 lines
30 KiB
C++

#include "RaidIccMultipliers.h"
#include "ChooseTargetActions.h"
#include "DKActions.h"
#include "DruidActions.h"
#include "DruidBearActions.h"
#include "FollowActions.h"
#include "GenericActions.h"
#include "GenericSpellActions.h"
#include "HunterActions.h"
#include "MageActions.h"
#include "MovementActions.h"
#include "PaladinActions.h"
#include "PriestActions.h"
#include "RaidIccActions.h"
#include "ReachTargetActions.h"
#include "RogueActions.h"
#include "ShamanActions.h"
#include "UseMeetingStoneAction.h"
#include "WarriorActions.h"
#include "PlayerbotAI.h"
#include "RaidIccTriggers.h"
// LK global variables
namespace
{
std::map<ObjectGuid, uint32> g_plagueTimes;
std::map<ObjectGuid, bool> g_allowCure;
std::mutex g_plagueMutex; // Lock before accessing shared variables
}
// Lady Deathwhisper
float IccLadyDeathwhisperMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper");
if (!boss)
return 1.0f;
if (dynamic_cast<FleeAction*>(action) || dynamic_cast<FollowAction*>(action) || dynamic_cast<CombatFormationMoveAction*>(action))
return 0.0f;
static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE;
// Get the nearest hostile NPCs
const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs");
// Allow the IccShadeLadyDeathwhisperAction to run
if (dynamic_cast<IccShadeLadyDeathwhisperAction*>(action))
return 1.0f;
for (auto const& npcGuid : npcs)
{
Unit* shade = botAI->GetUnit(npcGuid);
if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID)
continue;
if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID())
continue;
return 0.0f; // Cancel all other actions when we need to handle Vengeful Shade
}
return 1.0f;
}
// dbs
float IccAddsDbsMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang");
if (!boss)
return 1.0f;
if (dynamic_cast<DpsAoeAction*>(action) || dynamic_cast<CastHurricaneAction*>(action) ||
dynamic_cast<CastVolleyAction*>(action) || dynamic_cast<CastBlizzardAction*>(action) ||
dynamic_cast<CastStarfallAction*>(action) || dynamic_cast<FanOfKnivesAction*>(action) ||
dynamic_cast<CastWhirlwindAction*>(action) || dynamic_cast<CastMindSearAction*>(action) ||
dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<FollowAction*>(action) ||
dynamic_cast<FleeAction*>(action))
return 0.0f;
if (botAI->IsRanged(bot))
if (dynamic_cast<ReachSpellAction*>(action))
return 0.0f;
if (botAI->IsMainTank(bot))
{
Aura* aura = botAI->GetAura("rune of blood", bot);
if (aura)
{
if (dynamic_cast<MovementAction*>(action))
return 1.0f;
else
return 0.0f;
}
}
return 1.0f;
}
// dogs
float IccDogsMultiplier::GetValue(Action* action)
{
bool bossPresent = false;
if (AI_VALUE2(Unit*, "find target", "stinky") || AI_VALUE2(Unit*, "find target", "precious"))
bossPresent = true;
if (!bossPresent)
return 1.0f;
if (botAI->IsMainTank(bot))
{
Aura* aura = botAI->GetAura("mortal wound", bot, false, true);
if (aura && aura->GetStackAmount() >= 8)
{
if (dynamic_cast<MovementAction*>(action))
return 1.0f;
else
return 0.0f;
}
}
return 1.0f;
}
// Festergut
float IccFestergutMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "festergut");
if (!boss)
return 1.0f;
if (dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<FollowAction*>(action))
return 0.0f;
if (dynamic_cast<FleeAction*>(action))
return 0.0f;
if (botAI->IsMainTank(bot))
{
Aura* aura = botAI->GetAura("gastric bloat", bot, false, true);
if (aura && aura->GetStackAmount() >= 6)
{
if (dynamic_cast<MovementAction*>(action))
return 1.0f;
else
return 0.0f;
}
}
if (dynamic_cast<IccFestergutSporeAction*>(action))
return 1.0f;
if (bot->HasAura(SPELL_GAS_SPORE))
return 0.0f;
return 1.0f;
}
// Rotface
float IccRotfaceMultiplier::GetValue(Action* action)
{
Unit* boss1 = AI_VALUE2(Unit*, "find target", "rotface");
if (!boss1)
return 1.0f;
if (dynamic_cast<CombatFormationMoveAction*>(action))
return 0.0f;
if (dynamic_cast<FleeAction*>(action) && !(bot->getClass() == CLASS_HUNTER))
return 0.0f;
if (dynamic_cast<CastBlinkBackAction*>(action))
return 0.0f;
if (botAI->IsAssistTank(bot) && (dynamic_cast<AttackRtiTargetAction*>(action) || dynamic_cast<TankAssistAction*>(action)))
return 0.0f;
Unit* boss = AI_VALUE2(Unit*, "find target", "big ooze");
if (!boss)
return 1.0f;
static std::map<ObjectGuid, uint32> lastExplosionTimes;
static std::map<ObjectGuid, bool> hasMoved;
ObjectGuid botGuid = bot->GetGUID();
// When cast starts, record the time
if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION))
{
if (lastExplosionTimes[botGuid] == 0) // Only set if not already set
{
lastExplosionTimes[botGuid] = time(nullptr);
hasMoved[botGuid] = false;
}
}
// If explosion cast is no longer active, reset the timers
if (!boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION))
{
if (lastExplosionTimes[botGuid] > 0 && time(nullptr) - lastExplosionTimes[botGuid] >= 16)
{
lastExplosionTimes[botGuid] = 0;
hasMoved[botGuid] = false;
return 1.0f; // Allow normal actions to resume
}
}
// If 9 seconds have passed since cast start and we haven't moved yet
if (lastExplosionTimes[botGuid] > 0 && !hasMoved[botGuid] && time(nullptr) - lastExplosionTimes[botGuid] >= 9)
{
if (dynamic_cast<MovementAction*>(action)
&& !dynamic_cast<IccRotfaceMoveAwayFromExplosionAction*>(action))
{
return 0.0f; // Block other movement actions
}
hasMoved[botGuid] = true; // Mark that we've initiated movement
}
// Continue blocking other movements for 7 seconds after moving
if (hasMoved[botGuid] && time(nullptr) - lastExplosionTimes[botGuid] < 16 // 9 seconds wait + 7 seconds stay
&& dynamic_cast<MovementAction*>(action)
&& !dynamic_cast<IccRotfaceMoveAwayFromExplosionAction*>(action))
return 0.0f;
return 1.0f;
}
// pp
float IccAddsPutricideMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide");
if (!boss)
return 1.0f;
bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot);
bool hasUnboundPlague = botAI->HasAura("Unbound Plague", bot);
if (!(bot->getClass() == CLASS_HUNTER) && dynamic_cast<FleeAction*>(action))
return 0.0f;
if (dynamic_cast<CombatFormationMoveAction*>(action))
return 0.0f;
if (dynamic_cast<CastDisengageAction*>(action))
return 0.0f;
if (dynamic_cast<CastBlinkBackAction*>(action))
return 0.0f;
if (botAI->IsMainTank(bot))
{
Aura* aura = botAI->GetAura("mutated plague", bot, false, true);
if (aura && aura->GetStackAmount() >= 4)
{
if (dynamic_cast<MovementAction*>(action))
return 1.0f;
else
return 0.0f;
}
}
if (hasGaseousBloat)
{
if (dynamic_cast<IccPutricideGasCloudAction*>(action))
return 1.0f;
if (dynamic_cast<IccPutricideGrowingOozePuddleAction*>(action))
return 1.0f;
if (botAI->IsHeal(bot))
return 1.0f;
else
return 0.0f; // Cancel all other actions when we need to handle Gaseous Bloat
}
if (hasUnboundPlague && boss && !boss->HealthBelowPct(35))
{
if (dynamic_cast<IccPutricideAvoidMalleableGooAction*>(action))
return 1.0f;
else
return 0.0f; // Cancel all other actions when we need to handle Unbound Plague
}
if (dynamic_cast<IccPutricideVolatileOozeAction*>(action))
{
if (dynamic_cast<IccPutricideAvoidMalleableGooAction*>(action))
return 0.0f;
if (dynamic_cast<IccPutricideGrowingOozePuddleAction*>(action) && !botAI->IsMainTank(bot))
return 0.0f;
//if (dynamic_cast<IccPutricideGasCloudAction*>(action) && !hasGaseousBloat)
//return 0.0f;
}
return 1.0f;
}
// bpc
float IccBpcAssistMultiplier::GetValue(Action* action)
{
Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth");
if (!keleseth)
return 1.0f;
if (dynamic_cast<DpsAoeAction*>(action) || dynamic_cast<CastHurricaneAction*>(action) ||
dynamic_cast<CastVolleyAction*>(action) || dynamic_cast<CastBlizzardAction*>(action) ||
dynamic_cast<CastStarfallAction*>(action) || dynamic_cast<FanOfKnivesAction*>(action) ||
dynamic_cast<CastWhirlwindAction*>(action) || dynamic_cast<CastMindSearAction*>(action) ||
dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<FollowAction*>(action))
return 0.0f;
Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true);
if (aura)
{
if (aura->GetStackAmount() > 18 && botAI->IsTank(bot))
{
if (dynamic_cast<MovementAction*>(action))
return 0.0f;
}
if (aura->GetStackAmount() > 12 && !botAI->IsTank(bot))
{
if (dynamic_cast<MovementAction*>(action))
return 0.0f;
}
}
Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar");
if (!valanar)
return 1.0f;
if (valanar && valanar->HasUnitState(UNIT_STATE_CASTING) &&
(valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) ||
valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) ||
valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) ||
valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4)))
{
if (dynamic_cast<AvoidAoeAction*>(action) || dynamic_cast<IccBpcEmpoweredVortexAction*>(action))
return 1.0f;
else
return 0.0f; // Cancel all other actions when we need to handle Empowered Vortex
}
Unit* flame1 = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f);
Unit* flame2 = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f);
bool ballOfFlame = flame1 && flame1->GetVictim() == bot;
bool infernoFlame = flame2 && flame2->GetVictim() == bot;
if (flame2)
{
if (dynamic_cast<AvoidAoeAction*>(action) || dynamic_cast<IccBpcKineticBombAction*>(action))
return 0.0f;
if (dynamic_cast<IccBpcBallOfFlameAction*>(action))
return 1.0f;
}
if (ballOfFlame || infernoFlame)
{
// If bot is tank, do nothing special
if (dynamic_cast<IccBpcBallOfFlameAction*>(action))
return 1.0f;
else
return 0.0f; // Cancel all other actions when we need to handle Ball of Flame
}
static const std::array<uint32, 4> bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3,
NPC_KINETIC_BOMB4};
const GuidVector bombs = AI_VALUE(GuidVector, "possible targets no los");
bool bombFound = false;
for (const auto entry : bombEntries)
{
for (auto const& guid : bombs)
{
if (Unit* unit = botAI->GetUnit(guid))
{
if (unit->GetEntry() == entry)
{
// Check if bomb is within valid Z-axis range
if (unit->GetPositionZ() - bot->GetPositionZ() < 25.0f)
{
bombFound = true;
break;
}
}
}
}
if (bombFound)
break;
}
if (bombFound && !(aura && aura->GetStackAmount() > 12) && !botAI->IsTank(bot))
{
// If kinetic bomb action is active, disable these actions
if (dynamic_cast<IccBpcKineticBombAction*>(action))
return 1.0f;
if (dynamic_cast<DpsAssistAction*>(action) || dynamic_cast<TankAssistAction*>(action) ||
dynamic_cast<AttackRtiTargetAction*>(action))
return 0.0f;
}
// For assist tank during BPC fight
if (botAI->IsAssistTank(bot) && !(aura && aura->GetStackAmount() > 18))
{
// Allow BPC-specific actions
if (dynamic_cast<IccBpcKelesethTankAction*>(action))
return 1.0f;
// Disable normal assist behavior
if (dynamic_cast<TankAssistAction*>(action) ||
dynamic_cast<FleeAction*>(action) ||
dynamic_cast<AttackRtiTargetAction*>(action) ||
dynamic_cast<CastConsecrationAction*>(action))
return 0.0f;
}
return 1.0f;
}
//BQL
float IccBqlMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel");
if (!boss)
return 1.0f;
Aura* aura2 = botAI->GetAura("Swarming Shadows", bot);
Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot);
if (botAI->IsRanged(bot))
if (dynamic_cast<AvoidAoeAction*>(action) || dynamic_cast<FleeAction*>(action) ||
dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<CastDisengageAction*>(action))
return 0.0f;
// If bot has Pact of Darkfallen aura, return 0 for all other actions
if (bot->HasAura(SPELL_PACT_OF_THE_DARKFALLEN))
{
if (dynamic_cast<IccBqlPactOfDarkfallenAction*>(action))
return 1.0f; // Allow Pact of Darkfallen action
else
return 0.0f; // Cancel all other actions when we need to handle Pact of Darkfallen
}
if (botAI->IsMelee(bot) && ((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f) && !aura)
{
if (dynamic_cast<IccBqlGroupPositionAction*>(action))
return 1.0f;
else
return 0.0f;
}
// If bot has frenzied bloodthirst, allow highest priority for bite action
if (aura) // If bot has frenzied bloodthirst
{
if (dynamic_cast<IccBqlVampiricBiteAction*>(action))
return 1.0f;
else
return 0.0f;
}
if (aura2 && !aura)
{
if (dynamic_cast<IccBqlGroupPositionAction*>(action))
return 1.0f;
else
return 0.0f; // Cancel all other actions when we need to handle Swarming Shadows
}
if ((boss->GetExactDist2d(ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY()) > 10.0f) &&
botAI->IsRanged(bot) && !((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f))
{
if (dynamic_cast<FleeAction*>(action) || dynamic_cast<CombatFormationMoveAction*>(action))
return 0.0f;
}
return 1.0f;
}
//VDW
float IccValithriaDreamCloudMultiplier::GetValue(Action* action)
{
Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f);
Aura* twistedNightmares = botAI->GetAura("Twisted Nightmares", bot);
Aura* emeraldVigor = botAI->GetAura("Emerald Vigor", bot);
if (!boss && !bot->HasAura(SPELL_DREAM_STATE))
return 1.0f;
if (dynamic_cast<FollowAction*>(action) || dynamic_cast<CombatFormationMoveAction*>(action))
return 0.0f;
if (botAI->IsTank(bot))
{
if (dynamic_cast<AttackRtiTargetAction*>(action))
return 0.0f;
}
if (botAI->IsHeal(bot) && (twistedNightmares || emeraldVigor))
if (dynamic_cast<DpsAssistAction*>(action) || dynamic_cast<AttackRtiTargetAction*>(action))
return 0.0f;
if (bot->HasAura(SPELL_DREAM_STATE) && !bot->HealthBelowPct(50))
{
if (dynamic_cast<IccValithriaDreamCloudAction*>(action))
return 1.0f; // Allow Dream Cloud action
else
return 0.0f; // Cancel all other actions when we need to handle Dream Cloud
}
return 1.0f;
}
//SINDRAGOSA
float IccSindragosaMultiplier::GetValue(Action* action)
{
Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f);
if (!boss)
return 1.0f;
Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true);
Difficulty diff = bot->GetRaidDifficulty();
if (boss->HealthBelowPct(95))
{
if (dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<FleeAction*>(action) ||
dynamic_cast<FollowAction*>(action) || dynamic_cast<CastStarfallAction*>(action))
return 0.0f;
}
if (aura && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) &&
!dynamic_cast<IccSindragosaFrostBombAction*>(action))
{
if (dynamic_cast<MovementAction*>(action) || dynamic_cast<IccSindragosaUnchainedMagicAction*>(action))
return 1.0f;
else
return 0.0f;
}
// Check if boss is casting blistering cold (using both normal and heroic spell IDs)
if (boss->HasUnitState(UNIT_STATE_CASTING) &&
(boss->FindCurrentSpellBySpellId(70123) || boss->FindCurrentSpellBySpellId(71047) ||
boss->FindCurrentSpellBySpellId(71048) || boss->FindCurrentSpellBySpellId(71049)))
{
// If this is the blistering cold action, give it highest priority
if (dynamic_cast<IccSindragosaBlisteringColdAction*>(action) ||
dynamic_cast<HealPartyMemberAction*>(action) ||
dynamic_cast<ReachPartyMemberToHealAction*>(action) ||
dynamic_cast<IccSindragosaTankSwapPositionAction*>(action))
return 1.0f;
// Disable all other actions while blistering cold is casting
return 0.0f;
}
// Highest priority if we have beacon
if (bot->HasAura(SPELL_FROST_BEACON))
{
if (dynamic_cast<IccSindragosaFrostBeaconAction*>(action))
return 1.0f;
else
return 0.0f;
}
Group* group = bot->GetGroup();
// Check if anyone in group has Frost Beacon (SPELL_FROST_BEACON)
bool anyoneHasFrostBeacon = false;
if (group)
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (member && member->IsAlive() && member->HasAura(SPELL_FROST_BEACON))
{
anyoneHasFrostBeacon = true;
break;
}
}
}
if (anyoneHasFrostBeacon && boss &&
boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(),
ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f &&
!boss->HealthBelowPct(25) && !boss->HealthAbovePct(99))
{
if (dynamic_cast<IccSindragosaFrostBeaconAction*>(action))
return 1.0f;
else
return 0.0f;
}
if (anyoneHasFrostBeacon && !botAI->IsMainTank(bot))
{
if (dynamic_cast<IccSindragosaGroupPositionAction*>(action))
return 0.0f;
}
if (botAI->IsMainTank(bot))
{
Aura* aura = botAI->GetAura("mystic buffet", bot, false, true);
if (aura && aura->GetStackAmount() >= 6)
{
if (dynamic_cast<MovementAction*>(action))
return 1.0f;
else
return 0.0f;
}
}
if (!botAI->IsTank(bot) && boss && boss->HealthBelowPct(35))
{
if (dynamic_cast<IccSindragosaGroupPositionAction*>(action))
return 0.0f;
}
if (boss && botAI->IsTank(bot))
{
if (boss->HealthBelowPct(35))
{
if (dynamic_cast<IccSindragosaTankSwapPositionAction*>(action) || dynamic_cast<TankFaceAction*>(action) ||
dynamic_cast<AttackAction*>(action) || dynamic_cast<MovementAction*>(action))
return 1.0f;
else
return 0.0f;
}
}
if (boss && boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99))
{
if (dynamic_cast<IccSindragosaFrostBombAction*>(action))
return 1.0f;
if (dynamic_cast<FollowAction*>(action) || dynamic_cast<IccSindragosaBlisteringColdAction*>(action) ||
dynamic_cast<IccSindragosaChilledToTheBoneAction*>(action) || dynamic_cast<IccSindragosaMysticBuffetAction*>(action) ||
dynamic_cast<IccSindragosaFrostBeaconAction*>(action) || dynamic_cast<IccSindragosaUnchainedMagicAction*>(action) ||
dynamic_cast<FleeAction*>(action) || dynamic_cast<CastDisengageAction*>(action) || dynamic_cast<PetAttackAction*>(action) ||
dynamic_cast<IccSindragosaGroupPositionAction*>(action) || dynamic_cast<TankAssistAction*>(action) ||
dynamic_cast<DpsAoeAction*>(action) || dynamic_cast<CastHurricaneAction*>(action) ||
dynamic_cast<CastVolleyAction*>(action) || dynamic_cast<CastBlizzardAction*>(action) ||
dynamic_cast<CastStarfallAction*>(action) || dynamic_cast<FanOfKnivesAction*>(action) ||
dynamic_cast<CastWhirlwindAction*>(action) || dynamic_cast<CastMindSearAction*>(action) ||
dynamic_cast<CastMagmaTotemAction*>(action) || dynamic_cast<CastConsecrationAction*>(action) ||
dynamic_cast<CastFlamestrikeAction*>(action) || dynamic_cast<CastExplosiveTrapAction*>(action) ||
dynamic_cast<CastExplosiveShotBaseAction*>(action))
return 0.0f;
}
return 1.0f;
}
float IccLichKingAddsMultiplier::GetValue(Action* action)
{
Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f);
if (!terenasMenethilHC)
if (dynamic_cast<CastStarfallAction*>(action))
return 0.0f;
if (terenasMenethilHC)
{
Unit* mainTank = AI_VALUE(Unit*, "main tank");
if (!botAI->IsMainTank(bot) && mainTank && bot->GetExactDist2d(mainTank->GetPositionX(), mainTank->GetPositionY()) < 2.0f)
{
if (dynamic_cast<MovementAction*>(action))
return 0.0f;
}
if (botAI->IsMelee(bot) || (bot->getClass() == CLASS_WARLOCK))
{
if (dynamic_cast<MovementAction*>(action) || dynamic_cast<IccLichKingAddsAction*>(action))
return 1.0f;
else
return 0.0f;
}
if (dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<FollowAction*>(action) ||
dynamic_cast<FleeAction*>(action) || dynamic_cast<CastBlinkBackAction*>(action) ||
dynamic_cast<CastDisengageAction*>(action) || dynamic_cast<CastChargeAction*>(action) ||
dynamic_cast<CastFeralChargeBearAction*>(action) || dynamic_cast<CastIceBlockAction*>(action) ||
dynamic_cast<CastRevivePetAction*>(action) || dynamic_cast<TankAssistAction*>(action))
return 0.0f;
}
Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king");
if (!boss)
return 1.0f;
// Handle cure actions
if (dynamic_cast<CurePartyMemberAction*>(action) || dynamic_cast<CastCleanseDiseaseAction*>(action) ||
dynamic_cast<CastCleanseDiseaseOnPartyAction*>(action) ||
dynamic_cast<CastCleanseSpiritCurseOnPartyAction*>(action) || dynamic_cast<CastCleanseSpiritAction*>(action))
{
Group* group = bot->GetGroup();
if (!group)
return 1.0f;
// Check if any bot in the group has plague
bool anyBotHasPlague = false;
ObjectGuid plaguedPlayerGuid; // Track who has plague
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
if (Player* member = ref->GetSource())
{
if (botAI->HasAura("Necrotic Plague", member))
{
anyBotHasPlague = true;
plaguedPlayerGuid = member->GetGUID(); // Changed from GetObjectGuid()
break;
}
}
}
uint32 currentTime = getMSTime();
// Reset state if no one has plague
if (!anyBotHasPlague)
{
std::lock_guard<std::mutex> lock(g_plagueMutex); // Properly initialized
g_plagueTimes.clear();
g_allowCure.clear();
return 1.0f;
}
{ // New scope for lock_guard
std::lock_guard<std::mutex> lock(g_plagueMutex); // Properly initialized
// Start timer if this is a new plague
if (g_plagueTimes.find(plaguedPlayerGuid) == g_plagueTimes.end())
{
g_plagueTimes[plaguedPlayerGuid] = currentTime;
g_allowCure[plaguedPlayerGuid] = false;
return 0.0f;
}
// Once we allow cure, keep allowing it until plague is gone
if (g_allowCure[plaguedPlayerGuid])
{
return 1.0f;
}
// Check if enough time has passed (2,5 seconds)
if (currentTime - g_plagueTimes[plaguedPlayerGuid] >= 2500)
{
g_allowCure[plaguedPlayerGuid] = true;
return 1.0f;
}
} // lock_guard is automatically released here
return 0.0f;
}
if (dynamic_cast<FleeAction*>(action) && (bot->getClass() != CLASS_HUNTER))
return 0.0f;
if (dynamic_cast<CombatFormationMoveAction*>(action) || dynamic_cast<FollowAction*>(action) ||
dynamic_cast<CastBlinkBackAction*>(action) || dynamic_cast<CastDisengageAction*>(action))
return 0.0f;
if (boss && !boss->HealthBelowPct(71))
{
if (!botAI->IsTank(bot))
if (dynamic_cast<CastConsecrationAction*>(action))
return 0.0f;
if (dynamic_cast<DpsAoeAction*>(action) || dynamic_cast<CastHurricaneAction*>(action) ||
dynamic_cast<CastVolleyAction*>(action) || dynamic_cast<CastBlizzardAction*>(action) ||
dynamic_cast<CastStarfallAction*>(action) || dynamic_cast<FanOfKnivesAction*>(action) ||
dynamic_cast<CastWhirlwindAction*>(action) || dynamic_cast<CastMindSearAction*>(action) ||
dynamic_cast<CastMagmaTotemAction*>(action) || dynamic_cast<CastFlamestrikeAction*>(action) ||
dynamic_cast<CastExplosiveTrapAction*>(action) || dynamic_cast<CastExplosiveShotBaseAction*>(action))
return 0.0f;
}
Unit* currentTarget = AI_VALUE(Unit*, "current target");
bool hasWinterAura = false;
if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) ||
boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4)))
hasWinterAura = true;
bool hasWinter2Aura = false;
if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) ||
boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8)))
hasWinter2Aura = true;
bool isCasting = false;
if (boss && boss->HasUnitState(UNIT_STATE_CASTING))
isCasting = true;
bool isWinter = false;
if (boss && (boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER1) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER2) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER5) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER6) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER3) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER4) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER7) ||
boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER8)))
isWinter = true;
if (hasWinterAura || hasWinter2Aura || (isCasting && isWinter))
{
if (dynamic_cast<IccLichKingWinterAction*>(action) || dynamic_cast<SetFacingTargetAction*>(action))
return 1.0f;
if (botAI->IsAssistTank(bot) && dynamic_cast<TankAssistAction*>(action))
return 0.0f;
if (dynamic_cast<IccLichKingAddsAction*>(action))
return 0.0f;
if (currentTarget && boss && bot->GetDistance2d(boss->GetPositionX(), boss->GetPositionY()) > 50.0f && currentTarget == boss)
{
if (dynamic_cast<AttackRtiTargetAction*>(action) || dynamic_cast<ReachSpellAction*>(action) ||
dynamic_cast<ReachMeleeAction*>(action) || dynamic_cast<ReachTargetAction*>(action) ||
dynamic_cast<TankAssistAction*>(action) || dynamic_cast<DpsAssistAction*>(action) ||
dynamic_cast<MovementAction*>(action))
return 0.0f;
}
if (currentTarget && (currentTarget->GetEntry() == NPC_ICE_SPHERE1 || currentTarget->GetEntry() == NPC_ICE_SPHERE2 ||
currentTarget->GetEntry() == NPC_ICE_SPHERE3 || currentTarget->GetEntry() == NPC_ICE_SPHERE4))
{
if (dynamic_cast<MovementAction*>(action) || dynamic_cast<ReachMeleeAction*>(action) ||
dynamic_cast<TankAssistAction*>(action))
return 0.0f;
}
}
if (botAI->IsRanged(bot) && !botAI->GetAura("Harvest Soul", bot, false, false))
{
// Check for defile presence
GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs");
bool defilePresent = false;
for (auto& npc : npcs)
{
Unit* unit = botAI->GetUnit(npc);
if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) // Defile entry
{
defilePresent = true;
break;
}
}
// Only disable movement if defile is present
if (defilePresent && (
dynamic_cast<CombatFormationMoveAction*>(action) ||
dynamic_cast<FollowAction*>(action) ||
dynamic_cast<FleeAction*>(action) ||
dynamic_cast<MoveRandomAction*>(action) ||
dynamic_cast<MoveFromGroupAction*>(action)))
{
return 0.0f;
}
}
if (botAI->IsAssistTank(bot) && boss && !boss->HealthBelowPct(71) && currentTarget == boss)
{
if (dynamic_cast<AttackRtiTargetAction*>(action))
return 0.0f;
}
return 1.0f;
}