[HOT FIX] MS build issues regarding folder / command lenght usage or rc.exe (#2038)

This commit is contained in:
bashermens
2026-01-19 22:45:28 +01:00
committed by GitHub
parent fd07e02a8a
commit 41c53365ae
1119 changed files with 27 additions and 27 deletions

View File

@@ -0,0 +1,109 @@
#include "Playerbots.h"
#include "AzjolNerubActions.h"
#include "AzjolNerubStrategy.h"
bool AttackWebWrapAction::isUseful() { return !botAI->IsHeal(bot); }
bool AttackWebWrapAction::Execute(Event event)
{
Unit* webWrap = nullptr;
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_WEB_WRAP)
{
webWrap = unit;
break;
}
}
if (!webWrap || AI_VALUE(Unit*, "current target") == webWrap)
{
return false;
}
return Attack(webWrap);
}
bool WatchersTargetAction::isUseful() { return !botAI->IsHeal(bot); }
bool WatchersTargetAction::Execute(Event event)
{
// Always prioritise web wraps
Unit* currTarget = AI_VALUE(Unit*, "current target");
if (currTarget && currTarget->GetEntry() == NPC_WEB_WRAP) { return false; }
// Do not search all units in range!
// There are many adds we don't want to aggro in close proximity,
// only check in-combat adds now.
GuidVector attackers = AI_VALUE(GuidVector, "attackers");
Unit* priorityTargets[4] = {nullptr, nullptr, nullptr, nullptr};
for (auto& attacker : attackers)
{
Unit* npc = botAI->GetUnit(attacker);
if (!npc)
{
continue;
}
switch (npc->GetEntry())
{
// Focus skirmishers first
case NPC_WATCHER_SKIRMISHER:
priorityTargets[0] = npc;
break;
// Then shadowcaster. This doesn't work so well for the shadowcaster
// + skirmisher pack - ideally we would kill the watcher second.
// But don't want to make this unnecessarily complex and rigid...
// Will revisit if this causes problems in heroic.
case NPC_WATCHER_SHADOWCASTER:
priorityTargets[1] = npc;
break;
// Named watcher next
case NPC_WATCHER_SILTHIK:
case NPC_WATCHER_GASHRA:
case NPC_WATCHER_NARJIL:
priorityTargets[2] = npc;
break;
// Warrior last
case NPC_WATCHER_WARRIOR:
priorityTargets[3] = npc;
break;
}
}
for (Unit* target : priorityTargets)
{
// Attack the first valid split target in the priority list
if (target)
{
if (currTarget != target)
{
// bot->Yell("ATTACKING "+target->GetName(), LANG_UNIVERSAL);
return Attack(target);
}
// Don't continue loop here, the target exists so we don't
// want to move down the prio list. We just don't need to send attack
// command again, just return false and exit the loop that way
return false;
}
}
return false;
}
bool AnubarakDodgePoundAction::isUseful() { return !AI_VALUE2(bool, "behind", "current target"); }
bool AnubarakDodgePoundAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "anub'arak");
if (!boss) { return false; }
float distance = bot->GetExactDist2d(boss->GetPosition());
// Extra units to move into the boss, instead of being just 1 pixel past his midpoint.
// Can be adjusted - this value tends to mirror how a human would play,
// and visibly ensures you won't get hit while not creating excessive movements.
float distanceExtra = 2.0f;
return Move(bot->GetAngle(boss), distance + distanceExtra);
}

View File

@@ -0,0 +1,34 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONANACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONANACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "AzjolNerubTriggers.h"
class AttackWebWrapAction : public AttackAction
{
public:
AttackWebWrapAction(PlayerbotAI* ai) : AttackAction(ai, "attack web wrap") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class WatchersTargetAction : public AttackAction
{
public:
WatchersTargetAction(PlayerbotAI* ai) : AttackAction(ai, "krik'thir priority") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class AnubarakDodgePoundAction : public AttackAction
{
public:
AnubarakDodgePoundAction(PlayerbotAI* ai) : AttackAction(ai, "anub'arak dodge pound") {}
bool Execute(Event event) override;
bool isUseful() override;
};
#endif

View File

@@ -0,0 +1,22 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONANACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONANACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "AzjolNerubActions.h"
class WotlkDungeonANActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonANActionContext() {
creators["attack web wrap"] = &WotlkDungeonANActionContext::attack_web_wrap;
creators["krik'thir priority"] = &WotlkDungeonANActionContext::krikthir_priority;
creators["dodge pound"] = &WotlkDungeonANActionContext::dodge_pound;
}
private:
static Action* attack_web_wrap(PlayerbotAI* ai) { return new AttackWebWrapAction(ai); }
static Action* krikthir_priority(PlayerbotAI* ai) { return new WatchersTargetAction(ai); }
static Action* dodge_pound(PlayerbotAI* ai) { return new AnubarakDodgePoundAction(ai); }
};
#endif

View File

@@ -0,0 +1,25 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONANTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONANTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "AzjolNerubTriggers.h"
class WotlkDungeonANTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonANTriggerContext()
{
creators["krik'thir web wrap"] = &WotlkDungeonANTriggerContext::krikthir_web_wrap;
creators["krik'thir watchers"] = &WotlkDungeonANTriggerContext::krikthir_watchers;
// creators["anub'arak impale"] = &WotlkDungeonANTriggerContext::anubarak_impale;
creators["anub'arak pound"] = &WotlkDungeonANTriggerContext::anubarak_pound;
}
private:
static Trigger* krikthir_web_wrap(PlayerbotAI* ai) { return new KrikthirWebWrapTrigger(ai); }
static Trigger* krikthir_watchers(PlayerbotAI* ai) { return new KrikthirWatchersTrigger(ai); }
// static Trigger* anubarak_impale(PlayerbotAI* ai) { return new AnubarakImpaleTrigger(ai); }
static Trigger* anubarak_pound(PlayerbotAI* ai) { return new AnubarakPoundTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,54 @@
#include "AzjolNerubMultipliers.h"
#include "AzjolNerubActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "AzjolNerubTriggers.h"
#include "Action.h"
float KrikthirMultiplier::GetValue(Action* action)
{
if (!botAI->IsDps(bot)) { return 1.0f; }
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
Unit* boss = nullptr;
Unit* watcher = nullptr;
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (!unit) { continue; }
switch (unit->GetEntry())
{
case NPC_KRIKTHIR:
boss = unit;
continue;
case NPC_WATCHER_SILTHIK:
case NPC_WATCHER_GASHRA:
case NPC_WATCHER_NARJIL:
case NPC_WATCHER_SKIRMISHER:
case NPC_WATCHER_SHADOWCASTER:
case NPC_WATCHER_WARRIOR:
watcher = unit;
continue;
}
}
if (boss && watcher)
{
// Do not target swap
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
if (action->getThreatType() == Action::ActionThreatType::Aoe)
{
return 0.0f;
}
}
return 1.0f;
}

View File

@@ -0,0 +1,15 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONANMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONANMULTIPLIERS_H
#include "Multiplier.h"
class KrikthirMultiplier : public Multiplier
{
public:
KrikthirMultiplier(PlayerbotAI* ai) : Multiplier(ai, "krik'thir the gatewatcher") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,30 @@
#include "AzjolNerubStrategy.h"
#include "AzjolNerubMultipliers.h"
void WotlkDungeonANStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Krik'thir the Gatewatcher
// TODO: Add CC trigger while web wraps are casting?
// TODO: Bring healer closer than ranged dps to avoid fixates?
triggers.push_back(new TriggerNode("krik'thir web wrap",
{ NextAction("attack web wrap", ACTION_RAID + 5) }));
triggers.push_back(new TriggerNode("krik'thir watchers",
{ NextAction("krik'thir priority", ACTION_RAID + 4) }));
// Hadronox
// The core AC triggers are very buggy with this boss, but default strat seems to play correctly
//Anub'arak
// TODO: No clear way to track these spikes. They don't seem to appear as gameobjects or triggers,
// and cast time is instant so no way to check currently casting location.
// May need to hook boss AI.. might be able to just heal through it for now.
// triggers.push_back(new TriggerNode("anub'arak impale",
// { NextAction("TODO", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("anub'arak pound",
{ NextAction("dodge pound", ACTION_MOVE + 5) }));
}
void WotlkDungeonANStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new KrikthirMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONANSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONANSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonANStrategy : public Strategy
{
public:
WotlkDungeonANStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "azjol'nerub"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,68 @@
#include "Playerbots.h"
#include "AzjolNerubTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool KrikthirWebWrapTrigger::IsActive()
{
if (!botAI->IsDps(bot)) { return false; }
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_WEB_WRAP)
{
return true;
}
}
return false;
}
bool KrikthirWatchersTrigger::IsActive()
{
if (!botAI->IsDps(bot)) { return false; }
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_KRIKTHIR)
{
return true;
}
}
return false;
}
// bool AnubarakImpaleTrigger::IsActive()
// {
// Unit* boss = AI_VALUE2(Unit*, "find target", "anub'arak");
// if (!boss) { return false; }
// GuidVector triggers = AI_VALUE(GuidVector, "possible triggers");
// for (auto i = triggers.begin(); i != triggers.end(); i++)
// {
// Unit* unit = botAI->GetUnit(*i);
// if (unit)
// {
// bot->Yell("TRIGGER="+unit->GetName(), LANG_UNIVERSAL);
// }
// }
// return false;
// }
bool AnubarakPoundTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "anub'arak");
if (!boss) { return false; }
return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_POUND);
}

View File

@@ -0,0 +1,61 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONANTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONANTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum AzjolNerubIDs
{
// Krik'thir the Gatewatcher
NPC_KRIKTHIR = 28684,
NPC_WATCHER_SILTHIK = 28731,
NPC_WATCHER_GASHRA = 28730,
NPC_WATCHER_NARJIL = 28729,
NPC_WATCHER_SKIRMISHER = 28734,
NPC_WATCHER_SHADOWCASTER = 28733,
NPC_WATCHER_WARRIOR = 28732,
DEBUFF_WEB_WRAP = 52086,
NPC_WEB_WRAP = 28619,
// Anub'arak
// Not sure how to track this - first one is cast as a buff on himself,
// which triggers periodic casts of the spikes spell.
SPELL_IMPALE_PERIODIC = 53456,
SPELL_IMPALE_SPIKES = 53457,
SPELL_POUND_N = 53472,
SPELL_POUND_H = 59433,
};
#define SPELL_POUND DUNGEON_MODE(bot, SPELL_POUND_N, SPELL_POUND_H)
class KrikthirWebWrapTrigger : public Trigger
{
public:
KrikthirWebWrapTrigger(PlayerbotAI* ai) : Trigger(ai, "krik'thir web wrap") {}
bool IsActive() override;
};
class KrikthirWatchersTrigger : public Trigger
{
public:
KrikthirWatchersTrigger(PlayerbotAI* ai) : Trigger(ai, "krik'thir watchers") {}
bool IsActive() override;
};
// class AnubarakImpaleTrigger : public Trigger
// {
// public:
// AnubarakImpaleTrigger(PlayerbotAI* ai) : Trigger(ai, "anub'arak impale") {}
// bool IsActive() override;
// };
class AnubarakPoundTrigger : public Trigger
{
public:
AnubarakPoundTrigger(PlayerbotAI* ai) : Trigger(ai, "anub'arak pound") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,53 @@
#include "Playerbots.h"
#include "CullingOfStratholmeActions.h"
#include "CullingOfStratholmeStrategy.h"
bool ExplodeGhoulSpreadAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "salramm the fleshcrafter");
if (!boss) { return false; }
float distance = 10.0f;
float distanceExtra = 2.0f;
GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses");
for (auto i = corpses.begin(); i != corpses.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_GHOUL_MINION)
{
float currentDistance = bot->GetExactDist2d(unit);
if (currentDistance < distance + distanceExtra)
{
return MoveAway(unit, distance + distanceExtra - currentDistance);
}
}
}
return false;
}
bool EpochStackAction::isUseful()
{
// Minimum hunter range is 5, but values too close to this seem to cause issues..
// Hunter bots will try and melee in between ranged attacks, or just melee entirely at 5 as they are in range.
// 7.5 or 8.0 solves this for this boss.
// Unfortunately at this range the boss will charge. So I guess just don't stack as a hunter..
// if (bot->getClass() == CLASS_HUNTER)
// {
// return AI_VALUE2(float, "distance", "current target") > 7.5f;
// }
// else
return !(bot->getClass() == CLASS_HUNTER) && AI_VALUE2(float, "distance", "current target") > 5.0f;
}
bool EpochStackAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "chrono-lord epoch");
if (!boss) { return false; }
float maxMovement = 10.0f;
// if (bot->getClass() == CLASS_HUNTER)
// {
// return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss) - 6.5f, maxMovement));
// }
// else
return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss), maxMovement));
}

View File

@@ -0,0 +1,26 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONCOSACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONCOSACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "GenericSpellActions.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "CullingOfStratholmeTriggers.h"
class ExplodeGhoulSpreadAction : public MovementAction
{
public:
ExplodeGhoulSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "explode ghoul spread") {}
bool Execute(Event event) override;
};
class EpochStackAction : public MovementAction
{
public:
EpochStackAction(PlayerbotAI* ai) : MovementAction(ai, "epoch stack") {}
bool Execute(Event event) override;
bool isUseful() override;
};
#endif

View File

@@ -0,0 +1,20 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONCOSACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONCOSACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "CullingOfStratholmeActions.h"
class WotlkDungeonCoSActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonCoSActionContext() {
creators["explode ghoul spread"] = &WotlkDungeonCoSActionContext::explode_ghoul_spread;
creators["epoch stack"] = &WotlkDungeonCoSActionContext::epoch_stack;
}
private:
static Action* explode_ghoul_spread(PlayerbotAI* ai) { return new ExplodeGhoulSpreadAction(ai); }
static Action* epoch_stack(PlayerbotAI* ai) { return new EpochStackAction(ai); }
};
#endif

View File

@@ -0,0 +1,22 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "CullingOfStratholmeTriggers.h"
class WotlkDungeonCoSTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonCoSTriggerContext()
{
creators["explode ghoul"] = &WotlkDungeonCoSTriggerContext::explode_ghoul;
creators["epoch ranged"] = &WotlkDungeonCoSTriggerContext::epoch_ranged;
}
private:
static Trigger* explode_ghoul(PlayerbotAI* ai) { return new ExplodeGhoulTrigger(ai); }
static Trigger* epoch_ranged(PlayerbotAI* ai) { return new EpochRangedTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,19 @@
#include "CullingOfStratholmeMultipliers.h"
#include "CullingOfStratholmeActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "CullingOfStratholmeTriggers.h"
#include "Action.h"
float EpochMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "chrono-lord epoch");
if (!boss) { return 1.0f; }
if (bot->getClass() == CLASS_HUNTER) { return 1.0f; }
if (dynamic_cast<FleeAction*>(action)) { return 0.0f; }
return 1.0f;
}

View File

@@ -0,0 +1,15 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONCOSMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONCOSMULTIPLIERS_H
#include "Multiplier.h"
class EpochMultiplier : public Multiplier
{
public:
EpochMultiplier(PlayerbotAI* ai) : Multiplier(ai, "chrono-lord epoch") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,26 @@
#include "CullingOfStratholmeStrategy.h"
#include "CullingOfStratholmeMultipliers.h"
void WotlkDungeonCoSStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Meathook
// Can tank this in a fixed position to allow healer to LoS the stun, probably not necessary
// Salramm the Fleshcrafter
triggers.push_back(new TriggerNode("explode ghoul",
{ NextAction("explode ghoul spread", ACTION_MOVE + 5) }));
// Chrono-Lord Epoch
// Not sure if this actually works, I think I've seen him charge melee characters..?
triggers.push_back(new TriggerNode("epoch ranged",
{ NextAction("epoch stack", ACTION_MOVE + 5) }));
// Mal'Ganis
// Infinite Corruptor (Heroic only)
}
void WotlkDungeonCoSStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new EpochMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONCOSSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONCOSSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonCoSStrategy : public Strategy
{
public:
WotlkDungeonCoSStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "culling of stratholme"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,31 @@
#include "Playerbots.h"
#include "CullingOfStratholmeTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool ExplodeGhoulTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "salramm the fleshcrafter");
if (!boss) { return false; }
float distance = 10.0f;
float distanceExtra = 2.0f;
GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses");
for (auto i = corpses.begin(); i != corpses.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_RISEN_GHOUL)
{
if (bot->GetExactDist2d(unit) < distance + distanceExtra)
{
return true;
}
}
}
return false;
}
bool EpochRangedTrigger::IsActive()
{
return !botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "chrono-lord epoch");
}

View File

@@ -0,0 +1,29 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum CullingOfStratholmeIDs
{
// Salramm the Fleshcrafter
NPC_GHOUL_MINION = 27733,
};
class ExplodeGhoulTrigger : public Trigger
{
public:
ExplodeGhoulTrigger(PlayerbotAI* ai) : Trigger(ai, "explode ghoul") {}
bool IsActive() override;
};
class EpochRangedTrigger : public Trigger
{
public:
EpochRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "chrono-lord epoch ranged") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,175 @@
#include "Playerbots.h"
#include "DrakTharonKeepActions.h"
#include "DrakTharonKeepStrategy.h"
bool CorpseExplodeSpreadAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "trollgore");
if (!boss) { return false; }
float distance = 5.0f;
float distanceExtra = 2.0f;
GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses");
for (auto i = corpses.begin(); i != corpses.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_DRAKKARI_INVADER)
{
float currentDistance = bot->GetExactDist2d(unit);
if (currentDistance < distance + distanceExtra)
{
return MoveAway(unit, distance + distanceExtra - currentDistance);
}
}
}
return false;
}
bool AvoidArcaneFieldAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner");
if (!boss) { return false; }
float distance = 12.0f; // 11 unit radius, 1 unit added as buffer
if (bot->GetExactDist2d(boss) < distance)
{
return MoveAway(boss, distance - bot->GetExactDist2d(boss));
}
return false;
}
bool NovosDefaultPositionAction::isUseful()
{
// Distance to tether to centre of room
float threshold = 15.0f;
return bot->GetDistance(NOVOS_PARTY_POSITION) > threshold;
}
bool NovosDefaultPositionAction::Execute(Event event)
{
float clusterDistance = 4.0f;
// Only reposition if we're not killing anything
if (!bot->GetTarget())
{
return MoveNear(bot->GetMap()->GetId(),
NOVOS_PARTY_POSITION.GetPositionX(),
NOVOS_PARTY_POSITION.GetPositionY(),
NOVOS_PARTY_POSITION.GetPositionZ(),
clusterDistance, MovementPriority::MOVEMENT_NORMAL);
}
return false;
}
bool NovosTargetPriorityAction::Execute(Event event)
{
// TODO: This can be improved, some parts are still buggy.
// But it works for now and this fight is very easy
// Designate a dps char to handle the stairs adds.
// This is probably better as a melee, so just pick the first
// melee dps in the party. If none exist, pick the first ranged.
// TODO: Switch to botAI->Index instead, cleaner
Player* stairsDps = nullptr;
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Player* groupMember = botAI->GetPlayer(member);
if (!groupMember) { continue; }
if (botAI->IsDps(groupMember))
{
if (botAI->IsMelee(groupMember))
{
// Found our first melee dps, grab handle and break
stairsDps = groupMember;
break;
}
else
{
// Ranged dps, only set if none already assigned.
// Don't break, we want to keep searching for a melee instead.
if (!stairsDps)
{
stairsDps = groupMember;
}
}
}
}
Unit* selectedTargets[2] = {nullptr, nullptr};
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (!unit) { continue; }
uint32 creatureId = unit->GetEntry();
// Tank priority:
// Hulking Corpse -> Crystal Handler
if (botAI->IsTank(bot))
{
if (creatureId == NPC_HULKING_CORPSE)
{
selectedTargets[0] = unit;
}
else if (creatureId == NPC_CRYSTAL_HANDLER)
{
selectedTargets[1] = unit;
}
}
// Dedicated stairs dps is assigned.
// Priority: Risen Shadowcaster -> Fetid Troll Corpse
else if (stairsDps && bot == stairsDps)
{
if (creatureId == NPC_RISEN_SHADOWCASTER)
{
if (!selectedTargets[0] || bot->GetDistance(unit) < bot->GetDistance(selectedTargets[0]) - 5.0f)
{
selectedTargets[0] = unit;
}
}
else if (creatureId == NPC_FETID_TROLL_CORPSE)
{
if (!selectedTargets[1] || bot->GetDistance(unit) < bot->GetDistance(selectedTargets[1]) - 5.0f)
{
selectedTargets[1] = unit;
}
}
}
// All other dps priority:
// Crystal Handler -> Hulking Corpse
else if (botAI->IsDps(bot))
{
if (creatureId == NPC_CRYSTAL_HANDLER)
{
selectedTargets[0] = unit;
}
else if (creatureId == NPC_HULKING_CORPSE)
{
selectedTargets[1] = unit;
}
}
}
for (Unit* primaryTarget : selectedTargets)
{
// Attack the first valid target in the priority list
if (primaryTarget)
{
if (AI_VALUE(Unit*, "current target") != primaryTarget)
{
// bot->Yell(primaryTarget->GetName(), LANG_UNIVERSAL);
return Attack(primaryTarget);
}
// Don't continue loop here, the target exists so we don't
// want to move down the prio list. We just don't need to send attack
// command again, just return false and exit the loop that way
return false;
}
}
return false;
}

View File

@@ -0,0 +1,67 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONDTKACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONDTKACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "GenericSpellActions.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "DrakTharonKeepTriggers.h"
const Position NOVOS_PARTY_POSITION = Position(-378.852f, -760.349f, 28.587f);
class CorpseExplodeSpreadAction : public MovementAction
{
public:
CorpseExplodeSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "corpse explode spread") {}
bool Execute(Event event) override;
};
class AvoidArcaneFieldAction : public MovementAction
{
public:
AvoidArcaneFieldAction(PlayerbotAI* ai) : MovementAction(ai, "avoid arcane field") {}
bool Execute(Event event) override;
};
class NovosDefaultPositionAction : public MovementAction
{
public:
NovosDefaultPositionAction(PlayerbotAI* ai) : MovementAction(ai, "novos default position") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class NovosTargetPriorityAction : public AttackAction
{
public:
NovosTargetPriorityAction(PlayerbotAI* ai) : AttackAction(ai, "novos target priority") {}
bool Execute(Event event) override;
// bool isUseful() override;
};
class CastSlayingStrikeAction : public CastMeleeSpellAction
{
public:
CastSlayingStrikeAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "slaying strike") {}
};
class CastTauntAction : public CastSpellAction
{
public:
CastTauntAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "taunt") {}
};
class CastBoneArmorAction : public CastSpellAction
{
public:
CastBoneArmorAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "bone armor") {}
};
class CastTouchOfLifeAction : public CastSpellAction
{
public:
CastTouchOfLifeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "touch of life") {}
};
#endif

View File

@@ -0,0 +1,32 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONDTKACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONDTKACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "DrakTharonKeepActions.h"
class WotlkDungeonDTKActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonDTKActionContext() {
creators["corpse explode spread"] = &WotlkDungeonDTKActionContext::corpse_explode_spread;
creators["avoid arcane field"] = &WotlkDungeonDTKActionContext::avoid_arcane_field;
creators["novos positioning"] = &WotlkDungeonDTKActionContext::novos_positioning;
creators["novos target priority"] = &WotlkDungeonDTKActionContext::novos_target_priority;
creators["slaying strike"] = &WotlkDungeonDTKActionContext::slaying_strike;
creators["tharonja taunt"] = &WotlkDungeonDTKActionContext::taunt;
creators["bone armor"] = &WotlkDungeonDTKActionContext::bone_armor;
creators["touch of life"] = &WotlkDungeonDTKActionContext::touch_of_life;
}
private:
static Action* corpse_explode_spread(PlayerbotAI* ai) { return new CorpseExplodeSpreadAction(ai); }
static Action* avoid_arcane_field(PlayerbotAI* ai) { return new AvoidArcaneFieldAction(ai); }
static Action* novos_positioning(PlayerbotAI* ai) { return new NovosDefaultPositionAction(ai); }
static Action* novos_target_priority(PlayerbotAI* ai) { return new NovosTargetPriorityAction(ai); }
static Action* slaying_strike(PlayerbotAI* ai) { return new CastSlayingStrikeAction(ai); }
static Action* taunt(PlayerbotAI* ai) { return new CastTauntAction(ai); }
static Action* bone_armor(PlayerbotAI* ai) { return new CastBoneArmorAction(ai); }
static Action* touch_of_life(PlayerbotAI* ai) { return new CastTouchOfLifeAction(ai); }
};
#endif

View File

@@ -0,0 +1,28 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "DrakTharonKeepTriggers.h"
class WotlkDungeonDTKTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonDTKTriggerContext()
{
creators["corpse explode"] = &WotlkDungeonDTKTriggerContext::corpse_explode;
creators["arcane field"] = &WotlkDungeonDTKTriggerContext::arcane_field;
// creators["crystal handler"] = &WotlkDungeonDTKTriggerContext::crystal_handler;
creators["gift of tharon'ja"] = &WotlkDungeonDTKTriggerContext::gift_of_tharonja;
creators["tharon'ja out of melee"] = &WotlkDungeonDTKTriggerContext::tharonja_out_of_melee;
}
private:
static Trigger* corpse_explode(PlayerbotAI* ai) { return new CorpseExplodeTrigger(ai); }
static Trigger* arcane_field(PlayerbotAI* ai) { return new ArcaneFieldTrigger(ai); }
// static Trigger* crystal_handler(PlayerbotAI* ai) { return new CrystalHandlerTrigger(ai); }
static Trigger* gift_of_tharonja(PlayerbotAI* ai) { return new GiftOfTharonjaTrigger(ai); }
static Trigger* tharonja_out_of_melee(PlayerbotAI* ai) { return new TwoTriggers(ai, "gift of tharon'ja", "enemy out of melee"); }
};
#endif

View File

@@ -0,0 +1,62 @@
#include "DrakTharonKeepMultipliers.h"
#include "DrakTharonKeepActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "DrakTharonKeepTriggers.h"
#include "Action.h"
float NovosMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner");
if (!boss) { return 1.0f; }
if (boss->FindCurrentSpellBySpellId(SPELL_ARCANE_FIELD) && bot->GetTarget())
{
if (dynamic_cast<DpsAssistAction*>(action)
|| dynamic_cast<TankAssistAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float TharonjaMultiplier::GetValue(Action* action)
{
if (!bot->HasAura(SPELL_GIFT_OF_THARONJA)) { return 1.0f; }
// Suppress all skills that are not enabled in skeleton form.
// Still allow non-ability actions such as movement
if (dynamic_cast<CastSpellAction*>(action)
&& !dynamic_cast<CastSlayingStrikeAction*>(action)
&& !dynamic_cast<CastTauntAction*>(action)
&& !dynamic_cast<CastBoneArmorAction*>(action)
&& !dynamic_cast<CastTouchOfLifeAction*>(action))
{
return 0.0f;
}
// Also suppress FleeAction to prevent ranged characters from avoiding melee range
if (dynamic_cast<FleeAction*>(action))
{
return 0.0f;
}
// Tanks should only taunt, no slaying strike
if (botAI->IsTank(bot))
{
if (dynamic_cast<CastSlayingStrikeAction*>(action))
{
return 0.0f;
}
}
// Dps & healer should not taunt
else
{
if (dynamic_cast<CastTauntAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}

View File

@@ -0,0 +1,24 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONDTKMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONDTKMULTIPLIERS_H
#include "Multiplier.h"
class NovosMultiplier : public Multiplier
{
public:
NovosMultiplier(PlayerbotAI* ai) : Multiplier(ai, "novos the summoner") {}
public:
virtual float GetValue(Action* action);
};
class TharonjaMultiplier : public Multiplier
{
public:
TharonjaMultiplier(PlayerbotAI* ai) : Multiplier(ai, "the prophet tharon'ja") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,40 @@
#include "DrakTharonKeepStrategy.h"
#include "DrakTharonKeepMultipliers.h"
void WotlkDungeonDTKStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Trollgore
triggers.push_back(new TriggerNode("corpse explode",
{ NextAction("corpse explode spread", ACTION_MOVE + 5) }));
// Novos the Summoner
// TODO: Can be improved - it's a pretty easy fight but complex to program, revisit if needed
triggers.push_back(new TriggerNode("arcane field",
{ NextAction("avoid arcane field", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("arcane field",
{ NextAction("novos positioning", ACTION_MOVE + 4) }));
triggers.push_back(new TriggerNode("arcane field",
{ NextAction("novos target priority", ACTION_NORMAL + 1) }));
// King Dred
// TODO: Fear ward / tremor totem, or general anti-fear strat development
//The Prophet Tharon'ja
triggers.push_back(new TriggerNode("gift of tharon'ja",
{ NextAction("touch of life", ACTION_NORMAL + 5) }));
triggers.push_back(new TriggerNode("gift of tharon'ja",
{ NextAction("bone armor", ACTION_NORMAL + 4) }));
// Run ranged chars (who would normally stand at range) into melee, to dps in skeleton form
triggers.push_back(new TriggerNode("tharon'ja out of melee",
{ NextAction("reach melee", ACTION_NORMAL + 3) }));
triggers.push_back(new TriggerNode("gift of tharon'ja",
{ NextAction("taunt", ACTION_NORMAL + 2) }));
triggers.push_back(new TriggerNode("gift of tharon'ja",
{ NextAction("slaying strike", ACTION_NORMAL + 2) }));
}
void WotlkDungeonDTKStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new NovosMultiplier(botAI));
multipliers.push_back(new TharonjaMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONDTKSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONDTKSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonDTKStrategy : public Strategy
{
public:
WotlkDungeonDTKStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "drak'tharon keep"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,60 @@
#include "Playerbots.h"
#include "DrakTharonKeepTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool CorpseExplodeTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "trollgore");
if (!boss) { return false; }
float distance = 6.0f; // 5 unit radius, 1 unit added as buffer
GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses");
for (auto i = corpses.begin(); i != corpses.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_DRAKKARI_INVADER)
{
if (bot->GetExactDist2d(unit) < distance)
{
return true;
}
}
}
return false;
}
bool ArcaneFieldTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner");
if (boss)
{
return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_FIELD);
}
return false;
}
// bool CrystalHandlerTrigger::IsActive()
// {
// Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner");
// if (!boss) { return false; }
// // Target is not findable from threat table using AI_VALUE2(),
// // therefore need to search manually for the unit name
// GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
// for (auto i = targets.begin(); i != targets.end(); ++i)
// {
// Unit* unit = botAI->GetUnit(*i);
// if (unit && unit->GetEntry() == NPC_CRYSTAL_HANDLER)
// {
// return true;
// }
// }
// return false;
// }
bool GiftOfTharonjaTrigger::IsActive()
{
return bot->HasAura(SPELL_GIFT_OF_THARONJA);
}

View File

@@ -0,0 +1,54 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum DrakTharonIDs
{
// Trollgore
NPC_DRAKKARI_INVADER = 27709,
// Novos the Summoner
NPC_NOVOS = 26631,
SPELL_ARCANE_FIELD = 47346,
NPC_CRYSTAL_HANDLER = 26627,
NPC_HULKING_CORPSE = 27597,
NPC_RISEN_SHADOWCASTER = 27600,
NPC_FETID_TROLL_CORPSE = 27598,
// The Prophet Tharon'ja
SPELL_GIFT_OF_THARONJA = 52509,
};
class CorpseExplodeTrigger : public Trigger
{
public:
CorpseExplodeTrigger(PlayerbotAI* ai) : Trigger(ai, "corpse explode") {}
bool IsActive() override;
};
class ArcaneFieldTrigger : public Trigger
{
public:
ArcaneFieldTrigger(PlayerbotAI* ai) : Trigger(ai, "arcane field") {}
bool IsActive() override;
};
// class CrystalHandlerTrigger : public Trigger
// {
// public:
// CrystalHandlerTrigger(PlayerbotAI* ai) : Trigger(ai, "crystal handler") {}
// bool IsActive() override;
// };
class GiftOfTharonjaTrigger : public Trigger
{
public:
GiftOfTharonjaTrigger(PlayerbotAI* ai) : Trigger(ai, "gift of tharon'ja") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,87 @@
#ifndef _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H
#define _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H
#include "Strategy.h"
#include "UtgardeKeep/Strategy/UtgardeKeepStrategy.h"
#include "Nexus/Strategy/NexusStrategy.h"
#include "AzjolNerub/Strategy/AzjolNerubStrategy.h"
#include "OldKingdom/Strategy/OldKingdomStrategy.h"
#include "DraktharonKeep/Strategy/DrakTharonKeepStrategy.h"
#include "VioletHold/Strategy/VioletHoldStrategy.h"
#include "Gundrak/Strategy/GundrakStrategy.h"
#include "HallsOfStone/Strategy/HallsOfStoneStrategy.h"
#include "HallsOfLightning/Strategy/HallsOfLightningStrategy.h"
#include "Oculus/Strategy/OculusStrategy.h"
#include "UtgardePinnacle/Strategy/UtgardePinnacleStrategy.h"
#include "CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.h"
#include "ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.h"
#include "PitOfSaron/Strategy/PitOfSaronStrategy.h"
#include "TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.h"
/*
Full list/TODO:
Trial of the Champion - ToC
Alliance Champions: Deathstalker Visceri, Eressea Dawnsinger, Mokra the Skullcrusher, Runok Wildmane, Zul'tore
Horde Champions: Ambrose Boltspark, Colosos, Jacob Alerius, Jaelyne Evensong, Lana Stouthammer
Argent Champion: Argent Confessor Paletress/Eadric the Pure
The Black Knight
Halls of Reflection - HoR
Falric, Marwyn, The Lich King
Pit of Saron - PoS
Forgemaster Garfrost, Krick & Ick, Scourgelord Tyrannus
The Forge of Souls - FoS
Bronjahm, Devourer of Souls
*/
class DungeonStrategyContext : public NamedObjectContext<Strategy>
{
public:
DungeonStrategyContext() : NamedObjectContext<Strategy>(false, true)
{
// Vanilla
// ...
// Burning Crusade
// ...
// Wrath of the Lich King
creators["wotlk-uk"] = &DungeonStrategyContext::wotlk_uk; // Utgarde Keep
creators["wotlk-nex"] = &DungeonStrategyContext::wotlk_nex; // The Nexus
creators["wotlk-an"] = &DungeonStrategyContext::wotlk_an; // Azjol-Nerub
creators["wotlk-ok"] = &DungeonStrategyContext::wotlk_ok; // Ahn'kahet: The Old Kingdom
creators["wotlk-dtk"] = &DungeonStrategyContext::wotlk_dtk; // Drak'Tharon Keep
creators["wotlk-vh"] = &DungeonStrategyContext::wotlk_vh; // The Violet Hold
creators["wotlk-gd"] = &DungeonStrategyContext::wotlk_gd; // Gundrak
creators["wotlk-hos"] = &DungeonStrategyContext::wotlk_hos; // Halls of Stone
creators["wotlk-hol"] = &DungeonStrategyContext::wotlk_hol; // Halls of Lightning
creators["wotlk-occ"] = &DungeonStrategyContext::wotlk_occ; // The Oculus
creators["wotlk-up"] = &DungeonStrategyContext::wotlk_up; // Utgarde Pinnacle
creators["wotlk-cos"] = &DungeonStrategyContext::wotlk_cos; // The Culling of Stratholme
creators["wotlk-toc"] = &DungeonStrategyContext::wotlk_toc; // Trial of the Champion
creators["wotlk-hor"] = &DungeonStrategyContext::wotlk_hor; // Halls of Reflection
creators["wotlk-pos"] = &DungeonStrategyContext::wotlk_pos; // Pit of Saron
creators["wotlk-fos"] = &DungeonStrategyContext::wotlk_fos; // The Forge of Souls
}
private:
static Strategy* wotlk_uk(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); }
static Strategy* wotlk_nex(PlayerbotAI* botAI) { return new WotlkDungeonNexStrategy(botAI); }
static Strategy* wotlk_an(PlayerbotAI* botAI) { return new WotlkDungeonANStrategy(botAI); }
static Strategy* wotlk_ok(PlayerbotAI* botAI) { return new WotlkDungeonOKStrategy(botAI); }
static Strategy* wotlk_dtk(PlayerbotAI* botAI) { return new WotlkDungeonDTKStrategy(botAI); }
static Strategy* wotlk_vh(PlayerbotAI* botAI) { return new WotlkDungeonVHStrategy(botAI); }
static Strategy* wotlk_gd(PlayerbotAI* botAI) { return new WotlkDungeonGDStrategy(botAI); }
static Strategy* wotlk_hos(PlayerbotAI* botAI) { return new WotlkDungeonHoSStrategy(botAI); }
static Strategy* wotlk_hol(PlayerbotAI* botAI) { return new WotlkDungeonHoLStrategy(botAI); }
static Strategy* wotlk_occ(PlayerbotAI* botAI) { return new WotlkDungeonOccStrategy(botAI); }
static Strategy* wotlk_up(PlayerbotAI* botAI) { return new WotlkDungeonUPStrategy(botAI); }
static Strategy* wotlk_cos(PlayerbotAI* botAI) { return new WotlkDungeonCoSStrategy(botAI); }
static Strategy* wotlk_fos(PlayerbotAI* botAI) { return new WotlkDungeonFoSStrategy(botAI); }
static Strategy* wotlk_pos(PlayerbotAI* botAI) { return new WotlkDungeonPoSStrategy(botAI); }
static Strategy* wotlk_toc(PlayerbotAI* botAI) { return new WotlkDungeonToCStrategy(botAI); }
// NYI from here down
static Strategy* wotlk_hor(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); }
};
#endif

View File

@@ -0,0 +1,20 @@
#ifndef _PLAYERBOT_DUNGEONUTILS_H
#define _PLAYERBOT_DUNGEONUTILS_H
template<class T> inline
const T& DUNGEON_MODE(Player* bot, const T& normal5, const T& heroic10)
{
switch (bot->GetMap()->GetDifficulty())
{
case DUNGEON_DIFFICULTY_NORMAL:
return normal5;
case DUNGEON_DIFFICULTY_HEROIC:
return heroic10;
default:
break;
}
return heroic10;
}
#endif

View File

@@ -0,0 +1,165 @@
#include "Playerbots.h"
#include "ForgeOfSoulsActions.h"
#include "ForgeOfSoulsStrategy.h"
#include "SharedDefines.h"
bool MoveFromBronjahmAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "bronjahm");
if (!boss)
return false;
if (bot->GetExactDist2d(boss) < 10.0f)
return FleePosition(boss->GetPosition(), 15.0f, 2000U);
else
return true;
return false;
}
bool AttackCorruptedSoulFragmentAction::Execute(Event event)
{
Unit* currentTarget = AI_VALUE(Unit*, "current target");
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
// If no valid skull target, search for corrupted soul fragment
Unit* empoweredPrince = nullptr;
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (!unit || !unit->IsAlive())
continue;
if (unit->GetEntry() == NPC_CORRUPTED_SOUL_FRAGMENT)
{
empoweredPrince = unit;
// Mark corrupted soul fragment with skull if in group and not already marked
if (Group* group = bot->GetGroup())
{
ObjectGuid currentSkullGuid = group->GetTargetIcon(7);
if (currentSkullGuid.IsEmpty() || currentSkullGuid != unit->GetGUID())
{
group->SetTargetIcon(7, bot->GetGUID(), unit->GetGUID()); // 7 = skull
}
}
break;
}
}
return false;
}
bool BronjahmGroupPositionAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "bronjahm");
if (!boss)
return false;
Aura* aura = botAI->GetAura("soulstorm", boss);
bool hasAura = aura;
Unit* corruptedSoul = bot->FindNearestCreature(NPC_CORRUPTED_SOUL_FRAGMENT, 50.0f);
bool activeSoulExists = corruptedSoul && corruptedSoul->IsAlive();
if (botAI->IsTank(bot) && botAI->HasAggro(boss))
{
// If any corrupted soul exists, handle positioning carefully
if (activeSoulExists)
{
GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs");
for (auto& npc : npcs)
{
Unit* unit = botAI->GetUnit(npc);
if (unit && unit->GetEntry() == NPC_CORRUPTED_SOUL_FRAGMENT)
{
// Soul exists - check positions
float soulToBossDist = unit->GetExactDist2d(boss);
float tankToBossDist = bot->GetExactDist2d(boss);
float soulToTankDist = unit->GetExactDist2d(bot);
// Handle edge case: if soul is between tank and boss
// This happens when: (tankToBossDist > soulToBossDist) AND (soulToTankDist < tankToBossDist)
if (tankToBossDist > soulToBossDist && soulToTankDist < tankToBossDist)
{
// Calculate position 5 yards behind boss from the soul's perspective
float angle = boss->GetAngle(unit); // Angle from boss to soul
float oppositeAngle = Position::NormalizeOrientation(angle + M_PI); // Opposite direction
// Calculate position 5 yards behind boss
float x = boss->GetPositionX() + 5.0f * cos(oppositeAngle);
float y = boss->GetPositionY() + 5.0f * sin(oppositeAngle);
float z = boss->GetPositionZ();
return MoveTo(bot->GetMapId(), x, y, z, false, false, false, true,
MovementPriority::MOVEMENT_NORMAL);
}
// If soul is near boss, flee from boss
if (soulToBossDist < 10.0f)
return FleePosition(unit->GetPosition(), 13.0f, 1000U);
// If soul exists but none of the above conditions, don't move to tank position yet
bot->SetFacingToObject(boss);
return true;
}
}
}
else
{
// No souls exist, safe to move to tank position
if (bot->GetExactDist2d(BRONJAHM_TANK_POSITION) > 5.0f)
{
return MoveTo(bot->GetMapId(), BRONJAHM_TANK_POSITION.GetPositionX(),
BRONJAHM_TANK_POSITION.GetPositionY(), BRONJAHM_TANK_POSITION.GetPositionZ(), false,
false, false, true, MovementPriority::MOVEMENT_NORMAL);
}
}
}
else
{
float maxMovement = 10.0f;
if (bot->GetExactDist2d(boss) > maxMovement && !activeSoulExists && (hasAura || boss->FindCurrentSpellBySpellId(SPELL_SOULSTORM_VISUAL) || boss->FindCurrentSpellBySpellId(SPELL_SOULSTORM_VISUAL2)))
{
if (botAI->IsRanged(bot))
{
return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss) - 6.5f, maxMovement));
}
else
{
return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss) - 2.0f, maxMovement));
}
}
else
return false;
}
return false;
}
bool BronjahmGroupPositionAction::isUseful() { return true; }
bool DevourerOfSoulsAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "devourer of souls");
if (!boss)
return false;
Aura* aura = botAI->GetAura("mirrored soul", boss);
bool hasAura = aura;
if (!botAI->IsTank(bot) && !botAI->IsHeal(bot) && hasAura)
{
// Calculate the opposite direction
float angle = bot->GetAngle(boss);
float newAngle = Position::NormalizeOrientation(angle + M_PI); // Add 180 degrees (PI radians)
// Set the bot's orientation to face away from DoS = no attacks
bot->SetFacingTo(newAngle);
return true;
}
return false;
}

View File

@@ -0,0 +1,51 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONFOSACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONFOSACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "ForgeOfSoulsTriggers.h"
const Position BRONJAHM_TANK_POSITION = Position(5297.920f, 2506.698f, 686.068f);
class MoveFromBronjahmAction : public MovementAction
{
public:
MoveFromBronjahmAction(PlayerbotAI* ai) : MovementAction(ai, "move from bronjahm") {}
bool Execute(Event event) override;
};
class AttackCorruptedSoulFragmentAction : public AttackAction
{
public:
AttackCorruptedSoulFragmentAction(PlayerbotAI* ai) : AttackAction(ai, "attack corrupted soul fragment") {}
bool Execute(Event event) override;
};
class BronjahmGroupPositionAction : public AttackAction
{
public:
BronjahmGroupPositionAction(PlayerbotAI* ai) : AttackAction(ai, "bronjahm group position") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class DevourerOfSoulsAction : public AttackAction
{
public:
DevourerOfSoulsAction(PlayerbotAI * ai, float distance = 10.0f, float delta_angle = M_PI / 8)
: AttackAction(ai, "devourer of souls")
{
this->distance = distance;
this->delta_angle = delta_angle;
}
virtual bool Execute(Event event);
protected:
float distance, delta_angle;
};
#endif

View File

@@ -0,0 +1,25 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONFOSACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONFOSACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "ForgeOfSoulsActions.h"
class WotlkDungeonFoSActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonFoSActionContext()
{
creators["move from bronjahm"] = &WotlkDungeonFoSActionContext::move_from_bronjahm;
creators["attack corrupted soul fragment"] = &WotlkDungeonFoSActionContext::attack_corrupted_soul_fragment;
creators["bronjahm group position"] = &WotlkDungeonFoSActionContext::bronjahm_group_position;
creators["devourer of souls"] = &WotlkDungeonFoSActionContext::devourer_of_souls;
}
private:
static Action* move_from_bronjahm(PlayerbotAI* ai) { return new MoveFromBronjahmAction(ai); }
static Action* attack_corrupted_soul_fragment(PlayerbotAI* ai) { return new AttackCorruptedSoulFragmentAction(ai); }
static Action* bronjahm_group_position(PlayerbotAI* ai) { return new BronjahmGroupPositionAction(ai); }
static Action* devourer_of_souls(PlayerbotAI* ai) { return new DevourerOfSoulsAction(ai); }
};
#endif

View File

@@ -0,0 +1,26 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "ForgeOfSoulsTriggers.h"
class WotlkDungeonFoSTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonFoSTriggerContext()
{
creators["bronjahm position"] = &WotlkDungeonFoSTriggerContext::bronjahm_position;
creators["move from bronjahm"] = &WotlkDungeonFoSTriggerContext::move_from_bronjahm;
creators["switch to soul fragment"] = &WotlkDungeonFoSTriggerContext::switch_to_soul_fragment;
creators["devourer of souls"] = &WotlkDungeonFoSTriggerContext::devourer_of_souls;
}
private:
static Trigger* move_from_bronjahm(PlayerbotAI* ai) { return new MoveFromBronjahmTrigger(ai); }
static Trigger* switch_to_soul_fragment(PlayerbotAI* ai) { return new SwitchToSoulFragment(ai); }
static Trigger* bronjahm_position(PlayerbotAI* ai) { return new BronjahmPositionTrigger(ai); }
static Trigger* devourer_of_souls(PlayerbotAI* ai) { return new DevourerOfSoulsTrigger(ai); }
};
#endif // !_PLAYERBOT_WOTLKDUNGEONFOSTRIGGERCONTEXT_H

View File

@@ -0,0 +1,48 @@
#include "ForgeOfSoulsMultipliers.h"
#include "ForgeOfSoulsActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "ForgeOfSoulsTriggers.h"
#include "ForgeOfSoulsActions.h"
float BronjahmMultiplier::GetValue(Action* action) {
Unit* boss = AI_VALUE2(Unit *, "find target", "bronjahm");
if (!boss)
return 1.0f;
if (dynamic_cast<TankAssistAction*>(action))
return 0.0f;
if (bot->HasAura(SPELL_CORRUPT_SOUL))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<MoveFromBronjahmAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float AttackFragmentMultiplier::GetValue(Action* action)
{
Unit* fragment = nullptr;
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (unit && unit->GetEntry() == NPC_CORRUPTED_SOUL_FRAGMENT)
{
fragment = unit;
break;
}
}
if (fragment && botAI->IsDps(bot) && dynamic_cast<BronjahmGroupPositionAction*>(action))
return 0.0f;
return 1.0f;
}

View File

@@ -0,0 +1,23 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONFOSMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONFOSMULTIPLIERS_H
#include "Multiplier.h"
class BronjahmMultiplier : public Multiplier
{
public:
BronjahmMultiplier(PlayerbotAI* ai) : Multiplier(ai, "bronjahm") {}
public:
virtual float GetValue(Action* action);
};
class AttackFragmentMultiplier : public Multiplier
{
public:
AttackFragmentMultiplier(PlayerbotAI* ai) : Multiplier(ai, "attack fragment") { }
float GetValue(Action* action) override;
};
#endif

View File

@@ -0,0 +1,19 @@
#include "ForgeOfSoulsStrategy.h"
#include "ForgeOfSoulsMultipliers.h"
void WotlkDungeonFoSStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode("move from bronjahm",
{ NextAction("move from bronjahm", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("switch to soul fragment",
{ NextAction("attack corrupted soul fragment", ACTION_RAID + 2) }));
triggers.push_back(new TriggerNode("bronjahm position",
{ NextAction("bronjahm group position", ACTION_RAID + 1) }));
triggers.push_back(new TriggerNode("devourer of souls",
{ NextAction("devourer of souls", ACTION_RAID + 1) }));
}
void WotlkDungeonFoSStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)
{
multipliers.push_back(new BronjahmMultiplier(botAI));
}

View File

@@ -0,0 +1,16 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONFOSSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONFOSSTRATEGY_H
#include "Multiplier.h"
#include "Strategy.h"
class WotlkDungeonFoSStrategy : public Strategy
{
public:
WotlkDungeonFoSStrategy(PlayerbotAI* ai) : Strategy(ai) {}
std::string const getName() override { return "forge of souls"; }
void InitTriggers(std::vector<TriggerNode*> &triggers) override;
void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif // !_PLAYERBOT_WOTLKDUNGEONFOSSTRATEGY_H

View File

@@ -0,0 +1,55 @@
#include "Playerbots.h"
#include "ForgeOfSoulsTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool MoveFromBronjahmTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "bronjahm");
if (!boss)
return false;
if (!boss->FindCurrentSpellBySpellId(SPELL_CORRUPT_SOUL))
return false;
if (!bot->HasAura(SPELL_CORRUPT_SOUL))
return false;
return true;
}
bool SwitchToSoulFragment::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "bronjahm");
if (!boss)
return false;
Unit* corruptedSoul = bot->FindNearestCreature(NPC_CORRUPTED_SOUL_FRAGMENT, 50.0f);
bool activeSoulExists = corruptedSoul && corruptedSoul->IsAlive();
if (!activeSoulExists)
return false;
return true;
}
bool BronjahmPositionTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "bronjahm");
if (!boss)
return false;
if (bot->HasAura(SPELL_CORRUPT_SOUL))
return false;
return true;
}
bool DevourerOfSoulsTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "devourer of souls");
if (!boss)
return false;
return true;
}

View File

@@ -0,0 +1,53 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum ForgeOfSoulsBronjahmIDs
{
// Bronjahm
NPC_CORRUPTED_SOUL_FRAGMENT = 36535,
SPELL_CORRUPT_SOUL = 68839,
SPELL_SOULSTORM_VISUAL = 68870,
SPELL_SOULSTORM_VISUAL2 = 68904,
SPELL_SOULSTORM = 68872,
// Devourer of Souls
SPELL_WAILING_SOULS = 68899,
};
class MoveFromBronjahmTrigger : public Trigger
{
public:
MoveFromBronjahmTrigger(PlayerbotAI* ai) : Trigger(ai, "move from bronjahm") {}
bool IsActive() override;
};
class SwitchToSoulFragment : public Trigger
{
public:
SwitchToSoulFragment(PlayerbotAI* ai) : Trigger(ai, "switch to soul fragment") {}
bool IsActive() override;
};
class BronjahmPositionTrigger : public Trigger
{
public:
BronjahmPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "bronjahm position") {}
bool IsActive() override;
};
class DevourerOfSoulsTrigger : public Trigger
{
public:
DevourerOfSoulsTrigger(PlayerbotAI* ai) : Trigger(ai, "devourer of souls") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,70 @@
#include "Playerbots.h"
#include "GundrakActions.h"
#include "GundrakStrategy.h"
bool AvoidPoisonNovaAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "slad'ran");
if (!boss) { return false; }
float distance = bot->GetExactDist2d(boss->GetPosition());
float radius = 15.0f;
float distanceExtra = 2.0f;
if (distance < radius + distanceExtra)
{
return MoveAway(boss, radius + distanceExtra - distance);
}
return false;
}
bool AttackSnakeWrapAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "slad'ran");
if (!boss) { return false; }
Unit* snakeWrap = nullptr;
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (unit && unit->GetEntry() == NPC_SNAKE_WRAP)
{
Unit* currentTarget = AI_VALUE(Unit*, "current target");
if (!currentTarget || currentTarget->GetEntry() != NPC_SNAKE_WRAP)
{
return Attack(unit);
}
}
}
return false;
}
bool AvoidWhirlingSlashAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "gal'darah");
if (!boss) { return false; }
float distance = bot->GetExactDist2d(boss->GetPosition());
float radius = 5.0f;
float distanceExtra = 2.0f;
if (distance < radius + distanceExtra)
{
if (botAI->IsTank(bot))
{
// The boss chases tank during this, leads to jittery stutter-stepping
// by the tank if we don't pre-move additional range. 2*radius seems ok
return MoveAway(boss, (2.0f * radius) + distanceExtra - distance);
}
// else
return MoveAway(boss, radius + distanceExtra - distance);
}
return false;
}

View File

@@ -0,0 +1,31 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONGDACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONGDACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "GundrakTriggers.h"
class AvoidPoisonNovaAction : public MovementAction
{
public:
AvoidPoisonNovaAction(PlayerbotAI* ai) : MovementAction(ai, "avoid poison nova") {}
bool Execute(Event event) override;
};
class AttackSnakeWrapAction : public AttackAction
{
public:
AttackSnakeWrapAction(PlayerbotAI* ai) : AttackAction(ai, "attack snake wrap") {}
bool Execute(Event event) override;
};
class AvoidWhirlingSlashAction : public MovementAction
{
public:
AvoidWhirlingSlashAction(PlayerbotAI* ai) : MovementAction(ai, "avoid whirling slash") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -0,0 +1,22 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONGDACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONGDACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "GundrakActions.h"
class WotlkDungeonGDActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonGDActionContext() {
creators["avoid poison nova"] = &WotlkDungeonGDActionContext::avoid_poison_nova;
creators["attack snake wrap"] = &WotlkDungeonGDActionContext::attack_snake_wrap;
creators["avoid whirling slash"] = &WotlkDungeonGDActionContext::avoid_whirling_slash;
}
private:
static Action* avoid_poison_nova(PlayerbotAI* ai) { return new AvoidPoisonNovaAction(ai); }
static Action* attack_snake_wrap(PlayerbotAI* ai) { return new AttackSnakeWrapAction(ai); }
static Action* avoid_whirling_slash(PlayerbotAI* ai) { return new AvoidWhirlingSlashAction(ai); }
};
#endif

View File

@@ -0,0 +1,23 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONGDTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONGDTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "GundrakTriggers.h"
class WotlkDungeonGDTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonGDTriggerContext()
{
creators["poison nova"] = &WotlkDungeonGDTriggerContext::poison_nova;
creators["snake wrap"] = &WotlkDungeonGDTriggerContext::snake_wrap;
creators["whirling slash"] = &WotlkDungeonGDTriggerContext::whirling_slash;
}
private:
static Trigger* poison_nova(PlayerbotAI* ai) { return new SladranPoisonNovaTrigger(ai); }
static Trigger* snake_wrap(PlayerbotAI* ai) { return new SladranSnakeWrapTrigger(ai); }
static Trigger* whirling_slash(PlayerbotAI* ai) { return new GaldarahWhirlingSlashTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,62 @@
#include "GundrakMultipliers.h"
#include "GundrakActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "GundrakTriggers.h"
#include "Action.h"
float SladranMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "slad'ran");
if (!boss) { return 1.0f; }
if (boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<AvoidPoisonNovaAction*>(action))
{
return 0.0f;
}
}
if (!botAI->IsDps(bot)) { return 1.0f; }
if (action->getThreatType() == Action::ActionThreatType::Aoe)
{
return 0.0f;
}
Unit* snakeWrap = nullptr;
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (unit && unit->GetEntry() == NPC_SNAKE_WRAP)
{
snakeWrap = unit;
break;
}
}
// Prevent auto-target acquisition during snake wraps
if (snakeWrap && dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
return 1.0f;
}
float GaldarahMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "gal'darah");
if (!boss) { return 1.0f; }
if (boss->HasAura(SPELL_WHIRLING_SLASH))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<AvoidWhirlingSlashAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}

View File

@@ -0,0 +1,24 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONGDMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONGDMULTIPLIERS_H
#include "Multiplier.h"
class SladranMultiplier : public Multiplier
{
public:
SladranMultiplier(PlayerbotAI* ai) : Multiplier(ai, "slad'ran") {}
public:
virtual float GetValue(Action* action);
};
class GaldarahMultiplier : public Multiplier
{
public:
GaldarahMultiplier(PlayerbotAI* ai) : Multiplier(ai, "gal'darah") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,29 @@
#include "GundrakStrategy.h"
#include "GundrakMultipliers.h"
void WotlkDungeonGDStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Moorabi
// Drakkari Colossus
// Slad'ran
// TODO: Might need to add target priority for heroic on the snakes or to burn down boss.
// Will re-test in heroic, decent dps groups should be able to blast him down with no funky strats.
triggers.push_back(new TriggerNode("poison nova",
{ NextAction("avoid poison nova", ACTION_RAID + 5) }));
triggers.push_back(new TriggerNode("snake wrap",
{ NextAction("attack snake wrap", ACTION_RAID + 4) }));
// Gal'darah
triggers.push_back(new TriggerNode("whirling slash",
{ NextAction("avoid whirling slash", ACTION_RAID + 5) }));
// Eck the Ferocious (Heroic only)
}
void WotlkDungeonGDStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new SladranMultiplier(botAI));
multipliers.push_back(new GaldarahMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONGDSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONGDSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonGDStrategy : public Strategy
{
public:
WotlkDungeonGDStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "gundrak"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,37 @@
#include "Playerbots.h"
#include "GundrakTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool SladranPoisonNovaTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "slad'ran");
if (!boss) { return false; }
return bool(boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA));
}
bool SladranSnakeWrapTrigger::IsActive()
{
if (!botAI->IsDps(bot)) { return false; }
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (unit && unit->GetEntry() == NPC_SNAKE_WRAP)
{
return true;
}
}
return false;
}
bool GaldarahWhirlingSlashTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "gal'darah");
return boss && boss->HasAura(SPELL_WHIRLING_SLASH);
}

View File

@@ -0,0 +1,45 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONGDTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONGDTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum GundrakIDs
{
// Slad'ran
SPELL_POISON_NOVA_N = 55081,
SPELL_POISON_NOVA_H = 59842,
NPC_SNAKE_WRAP = 29742,
// Gal'darah
SPELL_WHIRLING_SLASH_N = 55250,
SPELL_WHIRLING_SLASH_H = 59824,
};
#define SPELL_POISON_NOVA DUNGEON_MODE(bot, SPELL_POISON_NOVA_N, SPELL_POISON_NOVA_H)
#define SPELL_WHIRLING_SLASH DUNGEON_MODE(bot, SPELL_WHIRLING_SLASH_N, SPELL_WHIRLING_SLASH_H)
class SladranPoisonNovaTrigger : public Trigger
{
public:
SladranPoisonNovaTrigger(PlayerbotAI* ai) : Trigger(ai, "slad'ran poison nova") {}
bool IsActive() override;
};
class SladranSnakeWrapTrigger : public Trigger
{
public:
SladranSnakeWrapTrigger(PlayerbotAI* ai) : Trigger(ai, "slad'ran snake wrap") {}
bool IsActive() override;
};
class GaldarahWhirlingSlashTrigger : public Trigger
{
public:
GaldarahWhirlingSlashTrigger(PlayerbotAI* ai) : Trigger(ai, "gal'darah whirling slash") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,170 @@
#include "Playerbots.h"
#include "HallsOfLightningActions.h"
#include "HallsOfLightningStrategy.h"
bool BjarngrimTargetAction::Execute(Event event)
{
Unit* target = nullptr;
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector npcs = AI_VALUE(GuidVector, "possible targets");
for (auto& npc : npcs)
{
Unit* unit = botAI->GetUnit(npc);
if (unit && unit->GetEntry() == NPC_STORMFORGED_LIEUTENANT)
{
target = unit;
break;
}
}
Unit* currentTarget = AI_VALUE(Unit*, "current target");
// There are two, we don't want to ping-pong between them if we're attacking one already
if (target && currentTarget && currentTarget->GetEntry() == NPC_STORMFORGED_LIEUTENANT)
{
return false;
}
if (AI_VALUE(Unit*, "current target") == target)
{
return false;
}
return Attack(target);
}
bool AvoidWhirlwindAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "general bjarngrim");
if (!boss) { return false; }
float distance = bot->GetExactDist2d(boss->GetPosition());
float radius = 8.0f;
float distanceExtra = 2.0f;
if (distance < radius + distanceExtra)
{
return MoveAway(boss, radius + distanceExtra - distance);
}
return false;
}
bool VolkhanTargetAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "volkhan");
if (!boss || AI_VALUE(Unit*, "current target") == boss)
{
return false;
}
return Attack(boss);
}
bool StaticOverloadSpreadAction::Execute(Event event)
{
float radius = 8.0f;
float distanceExtra = 2.0f;
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
if (bot->GetGUID() == member)
{
continue;
}
Unit* unit = botAI->GetUnit(member);
if (unit && unit->HasAura(SPELL_STATIC_OVERLOAD)
&& bot->GetExactDist2d(unit) < radius)
{
return MoveAway(unit, radius + distanceExtra - bot->GetExactDist2d(unit));
}
}
return false;
}
bool BallLightningSpreadAction::Execute(Event event)
{
float radius = 6.0f;
float distanceExtra = 1.0f;
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
if (bot->GetGUID() == member)
{
continue;
}
Unit* unit = botAI->GetUnit(member);
if (unit && bot->GetExactDist2d(unit) < radius + distanceExtra)
{
return MoveAway(unit, radius + distanceExtra - bot->GetExactDist2d(unit));
}
}
return false;
}
bool IonarTankPositionAction::isUseful() { return bot->GetExactDist2d(IONAR_TANK_POSITION) > 10.0f; }
bool IonarTankPositionAction::Execute(Event event)
{
return MoveTo(bot->GetMapId(), IONAR_TANK_POSITION.GetPositionX(), IONAR_TANK_POSITION.GetPositionY(), IONAR_TANK_POSITION.GetPositionZ(),
false, false, false, true, MovementPriority::MOVEMENT_COMBAT);
}
bool DispersePositionAction::isUseful() { return bot->GetExactDist2d(DISPERSE_POSITION) > 8.0f; }
bool DispersePositionAction::Execute(Event event)
{
return MoveTo(bot->GetMapId(), DISPERSE_POSITION.GetPositionX(), DISPERSE_POSITION.GetPositionY(), DISPERSE_POSITION.GetPositionZ(),
false, false, false, true, MovementPriority::MOVEMENT_COMBAT);
}
bool LokenStackAction::isUseful()
{
// Minimum hunter range is 5, but values too close to this seem to cause issues..
// Hunter bots will try and melee in between ranged attacks, or just melee entirely at 5 as they are in range.
// 6.5 or 7.0 solves this for this boss.
if (bot->getClass() == CLASS_HUNTER)
{
return AI_VALUE2(float, "distance", "current target") > 6.5f;
}
// else
return AI_VALUE2(float, "distance", "current target") > 2.0f;
}
bool LokenStackAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "loken");
if (!boss) { return false; }
float maxMovement = 10.0f;
if (!boss->HasUnitState(UNIT_STATE_CASTING))
{
if (bot->getClass() == CLASS_HUNTER)
{
return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss) - 6.5f, maxMovement));
}
// else
return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss), maxMovement));
}
return false;
}
bool AvoidLightningNovaAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "loken");
if (!boss) { return false; }
float distance = bot->GetExactDist2d(boss->GetPosition());
float radius = 20.0f;
float distanceExtra = 2.0f;
if (distance < radius + distanceExtra)
{
return MoveAway(boss, radius + distanceExtra - distance);
}
return false;
}

View File

@@ -0,0 +1,79 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOLACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONHOLACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "HallsOfLightningTriggers.h"
const Position IONAR_TANK_POSITION = Position(1078.860f, -261.928f, 61.226f);
const Position DISPERSE_POSITION = Position(1161.152f, -261.584f, 53.223f);
class BjarngrimTargetAction : public AttackAction
{
public:
BjarngrimTargetAction(PlayerbotAI* ai) : AttackAction(ai, "bjarngrim target") {}
bool Execute(Event event) override;
};
class AvoidWhirlwindAction : public MovementAction
{
public:
AvoidWhirlwindAction(PlayerbotAI* ai) : MovementAction(ai, "avoid whirlwind") {}
bool Execute(Event event) override;
};
class VolkhanTargetAction : public AttackAction
{
public:
VolkhanTargetAction(PlayerbotAI* ai) : AttackAction(ai, "volkhan target") {}
bool Execute(Event event) override;
};
class StaticOverloadSpreadAction : public MovementAction
{
public:
StaticOverloadSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "static overload spread") {}
bool Execute(Event event) override;
};
class BallLightningSpreadAction : public MovementAction
{
public:
BallLightningSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "ball lightning spread") {}
bool Execute(Event event) override;
};
class IonarTankPositionAction : public MovementAction
{
public:
IonarTankPositionAction(PlayerbotAI* ai) : MovementAction(ai, "ionar tank position") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class DispersePositionAction : public MovementAction
{
public:
DispersePositionAction(PlayerbotAI* ai) : MovementAction(ai, "disperse position") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class LokenStackAction : public MovementAction
{
public:
LokenStackAction(PlayerbotAI* ai) : MovementAction(ai, "loken stack") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class AvoidLightningNovaAction : public MovementAction
{
public:
AvoidLightningNovaAction(PlayerbotAI* ai) : MovementAction(ai, "avoid lightning nova") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -0,0 +1,34 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOLACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONHOLACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "HallsOfLightningActions.h"
class WotlkDungeonHoLActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonHoLActionContext() {
creators["bjarngrim target"] = &WotlkDungeonHoLActionContext::bjarngrim_target;
creators["avoid whirlwind"] = &WotlkDungeonHoLActionContext::avoid_whirlwind;
creators["volkhan target"] = &WotlkDungeonHoLActionContext::volkhan_target;
creators["static overload spread"] = &WotlkDungeonHoLActionContext::static_overload_spread;
creators["ball lightning spread"] = &WotlkDungeonHoLActionContext::ball_lightning_spread;
creators["ionar tank position"] = &WotlkDungeonHoLActionContext::ionar_tank_position;
creators["disperse position"] = &WotlkDungeonHoLActionContext::disperse_position;
creators["loken stack"] = &WotlkDungeonHoLActionContext::loken_stack;
creators["avoid lightning nova"] = &WotlkDungeonHoLActionContext::avoid_lightning_nova;
}
private:
static Action* bjarngrim_target(PlayerbotAI* ai) { return new BjarngrimTargetAction(ai); }
static Action* avoid_whirlwind(PlayerbotAI* ai) { return new AvoidWhirlwindAction(ai); }
static Action* volkhan_target(PlayerbotAI* ai) { return new VolkhanTargetAction(ai); }
static Action* static_overload_spread(PlayerbotAI* ai) { return new StaticOverloadSpreadAction(ai); }
static Action* ball_lightning_spread(PlayerbotAI* ai) { return new BallLightningSpreadAction(ai); }
static Action* ionar_tank_position(PlayerbotAI* ai) { return new IonarTankPositionAction(ai); }
static Action* disperse_position(PlayerbotAI* ai) { return new DispersePositionAction(ai); }
static Action* loken_stack(PlayerbotAI* ai) { return new LokenStackAction(ai); }
static Action* avoid_lightning_nova(PlayerbotAI* ai) { return new AvoidLightningNovaAction(ai); }
};
#endif

View File

@@ -0,0 +1,35 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "HallsOfLightningTriggers.h"
class WotlkDungeonHoLTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonHoLTriggerContext()
{
creators["stormforged lieutenant"] = &WotlkDungeonHoLTriggerContext::stormforged_lieutenant;
creators["whirlwind"] = &WotlkDungeonHoLTriggerContext::bjarngrim_whirlwind;
creators["volkhan"] = &WotlkDungeonHoLTriggerContext::volkhan;
creators["static overload"] = &WotlkDungeonHoLTriggerContext::static_overload;
creators["ball lightning"] = &WotlkDungeonHoLTriggerContext::ball_lightning;
creators["ionar tank aggro"] = &WotlkDungeonHoLTriggerContext::ionar_tank_aggro;
creators["ionar disperse"] = &WotlkDungeonHoLTriggerContext::ionar_disperse;
creators["loken ranged"] = &WotlkDungeonHoLTriggerContext::loken_ranged;
creators["lightning nova"] = &WotlkDungeonHoLTriggerContext::lightning_nova;
}
private:
static Trigger* stormforged_lieutenant(PlayerbotAI* ai) { return new StormforgedLieutenantTrigger(ai); }
static Trigger* bjarngrim_whirlwind(PlayerbotAI* ai) { return new BjarngrimWhirlwindTrigger(ai); }
static Trigger* volkhan(PlayerbotAI* ai) { return new VolkhanTrigger(ai); }
static Trigger* static_overload(PlayerbotAI* ai) { return new IonarStaticOverloadTrigger(ai); }
static Trigger* ball_lightning(PlayerbotAI* ai) { return new IonarBallLightningTrigger(ai); }
static Trigger* ionar_tank_aggro(PlayerbotAI* ai) { return new IonarTankAggroTrigger(ai); }
static Trigger* ionar_disperse(PlayerbotAI* ai) { return new IonarDisperseTrigger(ai); }
static Trigger* loken_ranged(PlayerbotAI* ai) { return new LokenRangedTrigger(ai); }
static Trigger* lightning_nova(PlayerbotAI* ai) { return new LokenLightningNovaTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,125 @@
#include "HallsOfLightningMultipliers.h"
#include "HallsOfLightningActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "HallsOfLightningTriggers.h"
#include "Action.h"
#include "WarriorActions.h"
float BjarngrimMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "general bjarngrim");
if (!boss || botAI->IsHeal(bot)) { return 1.0f; }
if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND_BJARNGRIM))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<AvoidWhirlwindAction*>(action))
{
return 0.0f;
}
}
// Detect boss adds this way as sometimes they don't get added to threat table on dps bots,
// and some dps just stand at range and don't engage the boss at all as they can't find the adds
// Unit* boss_add = AI_VALUE2(Unit*, "find target", "stormforged lieutenant");
Unit* boss_add = nullptr;
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_STORMFORGED_LIEUTENANT)
{
boss_add = unit;
break;
}
}
if (!boss_add || botAI->IsTank(bot)) { return 1.0f; }
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
if (action->getThreatType() == Action::ActionThreatType::Aoe)
{
return 0.0f;
}
return 1.0f;
}
float VolkhanMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "volkhan");
if (!boss || botAI->IsTank(bot) || botAI->IsHeal(bot)) { return 1.0f; }
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
if (action->getThreatType() == Action::ActionThreatType::Aoe)
{
return 0.0f;
}
return 1.0f;
}
float IonarMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ionar");
if (!boss) { return 1.0f; }
// Check if the boss has dispersed into Sparks (not visible).
if (!bot->CanSeeOrDetect(boss))
{
// Block MovementActions except for specific exceptions.
if (dynamic_cast<MovementAction*>(action)
&& !dynamic_cast<DispersePositionAction*>(action)
&& !dynamic_cast<StaticOverloadSpreadAction*>(action))
{
return 0.0f;
}
}
if (boss->FindCurrentSpellBySpellId(SPELL_DISPERSE))
{
// Explicitly block the CastChargeAction during dispersal.
if (dynamic_cast<CastChargeAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float LokenMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "loken");
if (!boss) { return 1.0f; }
// Prevent FleeAction from being executed.
if (dynamic_cast<FleeAction*>(action)) { return 0.0f; }
// Prevent MovementActions during Lightning Nova unless it's AvoidLightningNovaAction.
if (boss->FindCurrentSpellBySpellId(SPELL_LIGHTNING_NOVA))
{
if (dynamic_cast<MovementAction*>(action)
&& !dynamic_cast<AvoidLightningNovaAction*>(action))
{
return 0.0f;
}
// Specifically prevent Charge during Lightning Nova.
if (dynamic_cast<CastChargeAction*>(action))
{
return 0.0f;
}
}
return 1.0f; // Default multiplier value for other cases.
}

View File

@@ -0,0 +1,42 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOLMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONHOLMULTIPLIERS_H
#include "Multiplier.h"
class BjarngrimMultiplier : public Multiplier
{
public:
BjarngrimMultiplier(PlayerbotAI* ai) : Multiplier(ai, "general bjarngrim") {}
public:
virtual float GetValue(Action* action);
};
class VolkhanMultiplier : public Multiplier
{
public:
VolkhanMultiplier(PlayerbotAI* ai) : Multiplier(ai, "volkhan") {}
public:
virtual float GetValue(Action* action);
};
class IonarMultiplier : public Multiplier
{
public:
IonarMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ionar") {}
public:
virtual float GetValue(Action* action);
};
class LokenMultiplier : public Multiplier
{
public:
LokenMultiplier(PlayerbotAI* ai) : Multiplier(ai, "loken") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,40 @@
#include "HallsOfLightningStrategy.h"
#include "HallsOfLightningMultipliers.h"
void WotlkDungeonHoLStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// General Bjarngrim
triggers.push_back(new TriggerNode("stormforged lieutenant",
{ NextAction("bjarngrim target", ACTION_RAID + 5) }));
triggers.push_back(new TriggerNode("whirlwind",
{ NextAction("avoid whirlwind", ACTION_RAID + 4) }));
// Volkhan
triggers.push_back(new TriggerNode("volkhan",
{ NextAction("volkhan target", ACTION_RAID + 5) }));
// Ionar
triggers.push_back(new TriggerNode("ionar disperse",
{ NextAction("disperse position", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("ionar tank aggro",
{ NextAction("ionar tank position", ACTION_MOVE + 4) }));
triggers.push_back(new TriggerNode("static overload",
{ NextAction("static overload spread", ACTION_MOVE + 3) }));
// TODO: Targeted player can dodge the ball, but a single player soaking it isn't too bad to heal
triggers.push_back(new TriggerNode("ball lightning",
{ NextAction("ball lightning spread", ACTION_MOVE + 2) }));
// Loken
triggers.push_back(new TriggerNode("lightning nova",
{ NextAction("avoid lightning nova", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("loken ranged",
{ NextAction("loken stack", ACTION_MOVE + 4) }));
}
void WotlkDungeonHoLStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new BjarngrimMultiplier(botAI));
multipliers.push_back(new VolkhanMultiplier(botAI));
multipliers.push_back(new IonarMultiplier(botAI));
multipliers.push_back(new LokenMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOLSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONHOLSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonHoLStrategy : public Strategy
{
public:
WotlkDungeonHoLStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "halls of lightning"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,93 @@
#include "Playerbots.h"
#include "HallsOfLightningTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool StormforgedLieutenantTrigger::IsActive()
{
if (!botAI->IsDps(bot)) { return false; }
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (unit && unit->IsInCombat() &&
unit->GetEntry() == NPC_STORMFORGED_LIEUTENANT)
{
return true;
}
}
return false;
}
bool BjarngrimWhirlwindTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "general bjarngrim");
if (!boss) { return false; }
return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND_BJARNGRIM);
}
bool VolkhanTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "volkhan");
return boss && !botAI->IsTank(bot) && !botAI->IsHeal(bot);
}
bool IonarStaticOverloadTrigger::IsActive()
{
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Unit* unit = botAI->GetUnit(member);
if (unit && unit->HasAura(SPELL_STATIC_OVERLOAD))
{
return true;
}
}
return false;
}
bool IonarBallLightningTrigger::IsActive()
{
if (botAI->IsMelee(bot)) { return false; }
Unit* boss = AI_VALUE2(Unit*, "find target", "ionar");
if (!boss) { return false; }
return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_BALL_LIGHTNING);
}
bool IonarTankAggroTrigger::IsActive()
{
if (!botAI->IsTank(bot)) { return false; }
Unit* boss = AI_VALUE2(Unit*, "find target", "ionar");
if (!boss) { return false; }
return AI_VALUE2(bool, "has aggro", "current target");
}
bool IonarDisperseTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ionar");
if (!boss) { return false; }
return !bot->CanSeeOrDetect(boss) || boss->FindCurrentSpellBySpellId(SPELL_DISPERSE);
}
bool LokenRangedTrigger::IsActive()
{
return !botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "loken");
}
bool LokenLightningNovaTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "loken");
if (!boss) { return false; }
return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_LIGHTNING_NOVA);
}

View File

@@ -0,0 +1,95 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum HallsOfLightningIDs
{
// General Bjarngrim
NPC_STORMFORGED_LIEUTENANT = 29240,
SPELL_WHIRLWIND_BJARNGRIM = 52027,
// Ionar
SPELL_STATIC_OVERLOAD_N = 52658,
SPELL_STATIC_OVERLOAD_H = 59795,
SPELL_BALL_LIGHTNING_N = 52780,
SPELL_BALL_LIGHTNING_H = 59800,
SPELL_DISPERSE = 52770,
NPC_SPARK_OF_IONAR = 28926,
// Loken
SPELL_LIGHTNING_NOVA_N = 52960,
SPELL_LIGHTNING_NOVA_H = 59835,
};
#define SPELL_STATIC_OVERLOAD DUNGEON_MODE(bot, SPELL_STATIC_OVERLOAD_N, SPELL_STATIC_OVERLOAD_H)
#define SPELL_BALL_LIGHTNING DUNGEON_MODE(bot, SPELL_BALL_LIGHTNING_N, SPELL_BALL_LIGHTNING_H)
#define SPELL_LIGHTNING_NOVA DUNGEON_MODE(bot, SPELL_LIGHTNING_NOVA_N, SPELL_LIGHTNING_NOVA_H)
class StormforgedLieutenantTrigger : public Trigger
{
public:
StormforgedLieutenantTrigger(PlayerbotAI* ai) : Trigger(ai, "stormforged lieutenant") {}
bool IsActive() override;
};
class BjarngrimWhirlwindTrigger : public Trigger
{
public:
BjarngrimWhirlwindTrigger(PlayerbotAI* ai) : Trigger(ai, "bjarngrim whirlwind") {}
bool IsActive() override;
};
class VolkhanTrigger : public Trigger
{
public:
VolkhanTrigger(PlayerbotAI* ai) : Trigger(ai, "volkhan") {}
bool IsActive() override;
};
class IonarStaticOverloadTrigger : public Trigger
{
public:
IonarStaticOverloadTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar static overload") {}
bool IsActive() override;
};
class IonarBallLightningTrigger : public Trigger
{
public:
IonarBallLightningTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar ball lightning spread") {}
bool IsActive() override;
};
class IonarTankAggroTrigger : public Trigger
{
public:
IonarTankAggroTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar tank aggro") {}
bool IsActive() override;
};
class IonarDisperseTrigger : public Trigger
{
public:
IonarDisperseTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar disperse") {}
bool IsActive() override;
};
class LokenRangedTrigger : public Trigger
{
public:
LokenRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "loken ranged") {}
bool IsActive() override;
};
class LokenLightningNovaTrigger : public Trigger
{
public:
LokenLightningNovaTrigger(PlayerbotAI* ai) : Trigger(ai, "lightning nova") {}
bool IsActive() override;
};
#endif

View File

View File

@@ -0,0 +1,53 @@
#include "Playerbots.h"
#include "HallsOfStoneActions.h"
#include "HallsOfStoneStrategy.h"
bool ShatterSpreadAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "krystallus");
if (!boss) { return false; }
float radius = 40.0f;
Unit* closestMember = nullptr;
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Unit* unit = botAI->GetUnit(member);
if (!unit || bot->GetGUID() == member)
{
continue;
}
if (!closestMember || bot->GetExactDist2d(unit) < bot->GetExactDist2d(closestMember))
{
closestMember = unit;
}
}
if (closestMember && bot->GetExactDist2d(closestMember) < radius)
{
// Move in small increments so course can be corrected, otherwise two bots may
// run in the same direction and not adjust until they reach the destination
// return MoveAway(closestMember, radius - bot->GetExactDist2d(closestMember));
return MoveAway(closestMember, 5.0f);
}
return false;
}
bool AvoidLightningRingAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "sjonnir the ironshaper");
if (!boss) { return false; }
float distance = bot->GetExactDist2d(boss->GetPosition());
float radius = 10.0f;
float distanceExtra = 2.0f;
if (distance < radius + distanceExtra)
{
return MoveAway(boss, radius + distanceExtra - distance);
}
return false;
}

View File

@@ -0,0 +1,24 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOSACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONHOSACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "HallsOfStoneTriggers.h"
class ShatterSpreadAction : public MovementAction
{
public:
ShatterSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "shatter spread") {}
bool Execute(Event event) override;
};
class AvoidLightningRingAction : public MovementAction
{
public:
AvoidLightningRingAction(PlayerbotAI* ai) : MovementAction(ai, "avoid lightning ring") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -0,0 +1,20 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOSACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONHOSACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "HallsOfStoneActions.h"
class WotlkDungeonHoSActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonHoSActionContext() {
creators["shatter spread"] = &WotlkDungeonHoSActionContext::shatter_spread;
creators["avoid lightning ring"] = &WotlkDungeonHoSActionContext::avoid_lightning_ring;
}
private:
static Action* shatter_spread(PlayerbotAI* ai) { return new ShatterSpreadAction(ai); }
static Action* avoid_lightning_ring(PlayerbotAI* ai) { return new AvoidLightningRingAction(ai); }
};
#endif

View File

@@ -0,0 +1,21 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "HallsOfStoneTriggers.h"
class WotlkDungeonHoSTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonHoSTriggerContext()
{
creators["ground slam"] = &WotlkDungeonHoSTriggerContext::ground_slam;
creators["lightning ring"] = &WotlkDungeonHoSTriggerContext::lightning_ring;
}
private:
static Trigger* ground_slam(PlayerbotAI* ai) { return new KrystallusGroundSlamTrigger(ai); }
static Trigger* lightning_ring(PlayerbotAI* ai) { return new SjonnirLightningRingTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,41 @@
#include "HallsOfStoneMultipliers.h"
#include "HallsOfStoneActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "HallsOfStoneTriggers.h"
#include "Action.h"
float KrystallusMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "krystallus");
if (!boss) { return 1.0f; }
// Check both of these... the spell is applied first, debuff later.
// Neither is active for the full duration so we need to trigger off both
if (bot->HasAura(SPELL_GROUND_SLAM) || bot->HasAura(DEBUFF_GROUND_SLAM))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<ShatterSpreadAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float SjonnirMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "sjonnir the ironshaper");
if (!boss) { return 1.0f; }
if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_LIGHTNING_RING))
{
// Problematic since there's a lot of movement on this boss, will prevent players from positioning
// well to deal with adds etc. during the channel period. Takes a bit of work to improve this though
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<AvoidLightningRingAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}

View File

@@ -0,0 +1,24 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOSMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONHOSMULTIPLIERS_H
#include "Multiplier.h"
class KrystallusMultiplier : public Multiplier
{
public:
KrystallusMultiplier(PlayerbotAI* ai) : Multiplier(ai, "krystallus") {}
public:
virtual float GetValue(Action* action);
};
class SjonnirMultiplier : public Multiplier
{
public:
SjonnirMultiplier(PlayerbotAI* ai) : Multiplier(ai, "sjonnir the ironshaper") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,29 @@
#include "HallsOfStoneStrategy.h"
#include "HallsOfStoneMultipliers.h"
void WotlkDungeonHoSStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Maiden of Grief
// TODO: Jump into damage during shock of sorrow?
// Krystallus
// TODO: I think bots need to dismiss pets on this, or they nuke players they are standing close to
triggers.push_back(new TriggerNode("ground slam",
{ NextAction("shatter spread", ACTION_RAID + 5) }));
// Tribunal of Ages
// Seems fine, maybe add focus targeting strat if needed on heroic.
// Main issue is dps will immediately rambo in and sometimes die before tank gets aggro,
// this is mostly an issue with the bot AI as they do it on every fight
// Sjonnir The Ironshaper
// Possibly tank in place in the middle of the room, assign a dps to adds?
triggers.push_back(new TriggerNode("lightning ring",
{ NextAction("avoid lightning ring", ACTION_RAID + 5) }));
}
void WotlkDungeonHoSStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new KrystallusMultiplier(botAI));
multipliers.push_back(new SjonnirMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOSSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONHOSSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonHoSStrategy : public Strategy
{
public:
WotlkDungeonHoSStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "halls of stone"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,22 @@
#include "Playerbots.h"
#include "HallsOfStoneTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool KrystallusGroundSlamTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "krystallus");
if (!boss) { return false; }
// Check both of these... the spell is applied first, debuff later.
// Neither is active for the full duration so we need to trigger off both
return bot->HasAura(SPELL_GROUND_SLAM) || bot->HasAura(DEBUFF_GROUND_SLAM);
}
bool SjonnirLightningRingTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "sjonnir the ironshaper");
if (!boss) { return false; }
return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_LIGHTNING_RING);
}

View File

@@ -0,0 +1,36 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum HallsOfStoneIDs
{
// Krystallus
SPELL_GROUND_SLAM = 50827,
DEBUFF_GROUND_SLAM = 50833,
// Sjonnir The Ironshaper
SPELL_LIGHTNING_RING_N = 50840,
SPELL_LIGHTNING_RING_H = 59848,
};
#define SPELL_LIGHTNING_RING DUNGEON_MODE(bot, SPELL_LIGHTNING_RING_N, SPELL_LIGHTNING_RING_H)
class KrystallusGroundSlamTrigger : public Trigger
{
public:
KrystallusGroundSlamTrigger(PlayerbotAI* ai) : Trigger(ai, "krystallus ground slam") {}
bool IsActive() override;
};
class SjonnirLightningRingTrigger : public Trigger
{
public:
SjonnirLightningRingTrigger(PlayerbotAI* ai) : Trigger(ai, "sjonnir lightning ring") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,172 @@
#include "Playerbots.h"
#include "NexusActions.h"
#include "NexusStrategy.h"
bool MoveFromWhirlwindAction::Execute(Event event)
{
Unit* boss = nullptr;
uint8 faction = bot->GetTeamId();
float targetDist = 10.0f; // Whirlwind has a range of 8, adding a safety buffer
switch (bot->GetMap()->GetDifficulty())
{
case DUNGEON_DIFFICULTY_NORMAL:
if (faction == TEAM_ALLIANCE)
{
boss = AI_VALUE2(Unit*, "find target", "horde commander");
}
else // TEAM_HORDE
{
boss = AI_VALUE2(Unit*, "find target", "alliance commander");
}
break;
case DUNGEON_DIFFICULTY_HEROIC:
if (faction == TEAM_ALLIANCE)
{
boss = AI_VALUE2(Unit*, "find target", "commander kolurg");
}
else // TEAM_HORDE
{
boss = AI_VALUE2(Unit*, "find target", "commander stoutbeard");
}
break;
default:
break;
}
// Ensure boss is valid before accessing its methods
if (!boss)
{
return false;
}
float bossDistance = bot->GetExactDist2d(boss->GetPosition());
// Check if the bot is already at a safe distance
if (bossDistance > targetDist)
{
return false;
}
// Move away from the boss to avoid Whirlwind
return MoveAway(boss, targetDist - bossDistance);
}
bool FirebombSpreadAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra");
float radius = 5.0f;
float targetDist = radius + 1.0f;
if (!boss) { return false; }
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Unit* unit = botAI->GetUnit(member);
if (!unit || bot->GetGUID() == member) { continue; }
if (bot->GetExactDist2d(unit) < targetDist)
{
return MoveAway(unit, targetDist);
}
}
return false;
}
bool TelestraSplitTargetAction::isUseful() { return !botAI->IsHeal(bot); }
bool TelestraSplitTargetAction::Execute(Event event)
{
GuidVector attackers = AI_VALUE(GuidVector, "attackers");
Unit* splitTargets[3] = {nullptr, nullptr, nullptr};
for (auto& attacker : attackers)
{
Unit* unit = botAI->GetUnit(attacker);
if (!unit) { continue; }
switch (unit->GetEntry())
{
// Focus arcane clone first
case NPC_ARCANE_MAGUS:
splitTargets[0] = unit;
break;
// Then the frost clone
case NPC_FROST_MAGUS:
splitTargets[1] = unit;
break;
// Fire clone last
case NPC_FIRE_MAGUS:
splitTargets[2] = unit;
break;
}
}
for (Unit* target : splitTargets)
{
// Attack the first valid split target in the priority list
if (target)
{
if (AI_VALUE(Unit*, "current target") != target)
{
return Attack(target);
}
// Don't continue loop here, the target exists so we don't
// want to move down the prio list. We just don't need to send attack
// command again, just return false and exit the loop that way
return false;
}
}
return false;
}
bool ChaoticRiftTargetAction::isUseful() { return !botAI->IsHeal(bot); }
bool ChaoticRiftTargetAction::Execute(Event event)
{
Unit* chaoticRift = nullptr;
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetName() == "Chaotic Rift")
{
chaoticRift = unit;
break;
}
}
if (!chaoticRift || AI_VALUE(Unit*, "current target") == chaoticRift)
{
return false;
}
return Attack(chaoticRift);
}
bool DodgeSpikesAction::isUseful()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper");
if (!boss) { return false; }
return bot->GetExactDist2d(boss) > 0.5f;
}
bool DodgeSpikesAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper");
if (!boss) { return false; }
return Move(bot->GetAngle(boss), bot->GetExactDist2d(boss) - 0.3f);
}
bool IntenseColdJumpAction::Execute(Event event)
{
// This needs improving but maybe it should be done in the playerbot core.
// Jump doesn't seem to support zero offset (eg. jump on the spot) so need to add a tiny delta.
// This does a tiny bunnyhop that takes a couple of ms, it doesn't do a natural jump.
// Adding extra Z offset causes floating, and appears to scale the jump speed based on Z difference.
// Probably best to revisit once bot movement is improved
return JumpTo(bot->GetMap()->GetId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ() + 0.01f);
// bot->GetMotionMaster()->MoveFall();
}

View File

@@ -0,0 +1,55 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONNEXACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONNEXACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "NexusTriggers.h"
class MoveFromWhirlwindAction : public MovementAction
{
public:
MoveFromWhirlwindAction(PlayerbotAI* ai) : MovementAction(ai, "move from whirlwind") {}
bool Execute(Event event) override;
};
class FirebombSpreadAction : public MovementAction
{
public:
FirebombSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "firebomb spread") {}
bool Execute(Event event) override;
};
class TelestraSplitTargetAction : public AttackAction
{
public:
TelestraSplitTargetAction(PlayerbotAI* ai) : AttackAction(ai, "telestra split target") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class ChaoticRiftTargetAction : public AttackAction
{
public:
ChaoticRiftTargetAction(PlayerbotAI* ai) : AttackAction(ai, "chaotic rift target") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class DodgeSpikesAction : public MovementAction
{
public:
DodgeSpikesAction(PlayerbotAI* ai) : MovementAction(ai, "dodge spikes") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class IntenseColdJumpAction : public MovementAction
{
public:
IntenseColdJumpAction(PlayerbotAI* ai) : MovementAction(ai, "intense cold jump") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -0,0 +1,96 @@
#include "NexusMultipliers.h"
#include "NexusActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "NexusTriggers.h"
float FactionCommanderMultiplier::GetValue(Action* action)
{
Unit* boss = nullptr;
uint8 faction = bot->GetTeamId();
switch (bot->GetMap()->GetDifficulty())
{
case DUNGEON_DIFFICULTY_NORMAL:
if (faction == TEAM_ALLIANCE)
{
boss = AI_VALUE2(Unit*, "find target", "horde commander");
}
else //if (faction == TEAM_HORDE)
{
boss = AI_VALUE2(Unit*, "find target", "alliance commander");
}
break;
case DUNGEON_DIFFICULTY_HEROIC:
if (faction == TEAM_ALLIANCE)
{
boss = AI_VALUE2(Unit*, "find target", "commander kolurg");
}
else //if (faction == TEAM_HORDE)
{
boss = AI_VALUE2(Unit*, "find target", "commander stoutbeard");
}
break;
default:
break;
}
if (boss && boss->HasUnitState(UNIT_STATE_CASTING) &&
boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND))
{
// Prevent movement actions other than flee during a whirlwind, to prevent running back in early.
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<MoveFromWhirlwindAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float TelestraMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra");
if (boss && boss->GetEntry() != NPC_TELESTRA)
{
// boss is split into clones, do not auto acquire target
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float AnomalusMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "anomalus");
if (boss && boss->HasAura(BUFF_RIFT_SHIELD))
{
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float OrmorokMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper");
if (!boss) { return 1.0f; }
// These are used for auto ranged repositioning, need to suppress so ranged dps don't ping-pong
if (dynamic_cast<FleeAction*>(action))
{
return 0.0f;
}
// This boss is annoying and shuffles around a lot. Don't let tank move once fight has started.
// Extra checks are to allow the tank to close distance and engage the boss initially
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<DodgeSpikesAction*>(action)
&& botAI->IsTank(bot) && bot->IsWithinMeleeRange(boss)
&& AI_VALUE2(bool, "facing", "current target"))
{
return 0.0f;
}
return 1.0f;
}

View File

@@ -0,0 +1,42 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONNEXMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONNEXMULTIPLIERS_H
#include "Multiplier.h"
class FactionCommanderMultiplier : public Multiplier
{
public:
FactionCommanderMultiplier(PlayerbotAI* ai) : Multiplier(ai, "faction commander") {}
public:
virtual float GetValue(Action* action);
};
class TelestraMultiplier : public Multiplier
{
public:
TelestraMultiplier(PlayerbotAI* ai) : Multiplier(ai, "grand magus telestra") {}
public:
virtual float GetValue(Action* action);
};
class AnomalusMultiplier : public Multiplier
{
public:
AnomalusMultiplier(PlayerbotAI* ai) : Multiplier(ai, "anomalus") {}
public:
virtual float GetValue(Action* action);
};
class OrmorokMultiplier : public Multiplier
{
public:
OrmorokMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ormorok the tree-shaper") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,28 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONNEXACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONNEXACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "NexusActions.h"
class WotlkDungeonNexActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonNexActionContext() {
creators["move from whirlwind"] = &WotlkDungeonNexActionContext::move_from_whirlwind;
creators["firebomb spread"] = &WotlkDungeonNexActionContext::firebomb_spread;
creators["telestra split target"] = &WotlkDungeonNexActionContext::telestra_split_target;
creators["chaotic rift target"] = &WotlkDungeonNexActionContext::chaotic_rift_target;
creators["dodge spikes"] = &WotlkDungeonNexActionContext::dodge_spikes;
creators["intense cold jump"] = &WotlkDungeonNexActionContext::intense_cold_jump;
}
private:
static Action* move_from_whirlwind(PlayerbotAI* ai) { return new MoveFromWhirlwindAction(ai); }
static Action* firebomb_spread(PlayerbotAI* ai) { return new FirebombSpreadAction(ai); }
static Action* telestra_split_target(PlayerbotAI* ai) { return new TelestraSplitTargetAction(ai); }
static Action* chaotic_rift_target(PlayerbotAI* ai) { return new ChaoticRiftTargetAction(ai); }
static Action* dodge_spikes(PlayerbotAI* ai) { return new DodgeSpikesAction(ai); }
static Action* intense_cold_jump(PlayerbotAI* ai) { return new IntenseColdJumpAction(ai); }
};
#endif

View File

@@ -0,0 +1,33 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "NexusTriggers.h"
class WotlkDungeonNexTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonNexTriggerContext()
{
creators["faction commander whirlwind"] = &WotlkDungeonNexTriggerContext::faction_commander_whirlwind;
creators["telestra firebomb"] = &WotlkDungeonNexTriggerContext::telestra_firebomb;
creators["telestra split phase"] = &WotlkDungeonNexTriggerContext::telestra_split_phase;
creators["chaotic rift"] = &WotlkDungeonNexTriggerContext::chaotic_rift;
creators["ormorok spikes"] = &WotlkDungeonNexTriggerContext::ormorok_spikes;
creators["ormorok stack"] = &WotlkDungeonNexTriggerContext::ormorok_stack;
creators["intense cold"] = &WotlkDungeonNexTriggerContext::intense_cold;
creators["keristrasza positioning"] = &WotlkDungeonNexTriggerContext::keristrasza_positioning;
}
private:
static Trigger* faction_commander_whirlwind(PlayerbotAI* ai) { return new FactionCommanderWhirlwindTrigger(ai); }
static Trigger* telestra_firebomb(PlayerbotAI* ai) { return new TelestraFirebombTrigger(ai); }
static Trigger* telestra_split_phase(PlayerbotAI* ai) { return new TelestraSplitPhaseTrigger(ai); }
static Trigger* chaotic_rift(PlayerbotAI* ai) { return new ChaoticRiftTrigger(ai); }
static Trigger* ormorok_spikes(PlayerbotAI* ai) { return new OrmorokSpikesTrigger(ai); }
static Trigger* ormorok_stack(PlayerbotAI* ai) { return new OrmorokStackTrigger(ai); }
static Trigger* intense_cold(PlayerbotAI* ai) { return new IntenseColdTrigger(ai); }
static Trigger* keristrasza_positioning(PlayerbotAI* ai) { return new KeristraszaPositioningTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,52 @@
#include "NexusStrategy.h"
#include "NexusMultipliers.h"
void WotlkDungeonNexStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Horde Commander (Alliance N)/Commander Kolurg (Alliance H)
// or
// Alliance Commander (Horde N)/Commander Stoutbeard (Horde H)
triggers.push_back(new TriggerNode("faction commander whirlwind",
{ NextAction("move from whirlwind", ACTION_MOVE + 5) }));
// TODO: Handle fear? (tremor totems, fear ward etc.)
// Grand Magus Telestra
triggers.push_back(new TriggerNode("telestra firebomb",
{ NextAction("firebomb spread", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("telestra split phase",
{ NextAction("telestra split target", ACTION_RAID + 1) }));
// TODO: Add priority interrupt on the frost split's Blizzard casts
// Anomalus
triggers.push_back(new TriggerNode("chaotic rift",
{ NextAction("chaotic rift target", ACTION_RAID + 1) }));
// Ormorok the Tree-Shaper
// Tank trigger to stack inside boss. Can also add return action to prevent boss repositioning
// if it becomes too much of a problem. He usually dies before he's up against a wall though
triggers.push_back(new TriggerNode("ormorok spikes",
{ NextAction("dodge spikes", ACTION_MOVE + 5) }));
// Non-tank trigger to stack. Avoiding the spikes at range is.. harder than it seems.
// TODO: This turns hunters into melee marshmallows, have not come up with a better solution yet
triggers.push_back(new TriggerNode("ormorok stack",
{ NextAction("dodge spikes", ACTION_MOVE + 5) }));
// TODO: Add handling for spell reflect... best to spam low level/weak spells but don't want
// to hardcode spells per class, might be difficult to dynamically generate this.
// Will revisit if I find my altbots killing themselves in heroic, just heal through it for now
// Keristrasza
triggers.push_back(new TriggerNode("intense cold",
{ NextAction("intense cold jump", ACTION_MOVE + 5) }));
// Flank dragon positioning
triggers.push_back(new TriggerNode("keristrasza positioning",
{ NextAction("rear flank", ACTION_MOVE + 4) }));
// TODO: Add frost resist aura for paladins?
}
void WotlkDungeonNexStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new FactionCommanderMultiplier(botAI));
multipliers.push_back(new TelestraMultiplier(botAI));
multipliers.push_back(new AnomalusMultiplier(botAI));
multipliers.push_back(new OrmorokMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONNEXSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONNEXSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonNexStrategy : public Strategy
{
public:
WotlkDungeonNexStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "nexus"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,107 @@
#include "Playerbots.h"
#include "NexusTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool FactionCommanderWhirlwindTrigger::IsActive()
{
Unit* boss = nullptr;
uint8 faction = bot->GetTeamId();
switch (bot->GetMap()->GetDifficulty())
{
case DUNGEON_DIFFICULTY_NORMAL:
if (faction == TEAM_ALLIANCE)
{
boss = AI_VALUE2(Unit*, "find target", "horde commander");
}
else //if (faction == TEAM_HORDE)
{
boss = AI_VALUE2(Unit*, "find target", "alliance commander");
}
break;
case DUNGEON_DIFFICULTY_HEROIC:
if (faction == TEAM_ALLIANCE)
{
boss = AI_VALUE2(Unit*, "find target", "commander kolurg");
}
else //if (faction == TEAM_HORDE)
{
boss = AI_VALUE2(Unit*, "find target", "commander stoutbeard");
}
break;
default:
break;
}
if (boss && boss->HasUnitState(UNIT_STATE_CASTING))
{
if (boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND))
{
return true;
}
}
return false;
}
bool TelestraFirebombTrigger::IsActive()
{
if (botAI->IsMelee(bot)) { return false; }
Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra");
// Avoid split phase with the fake Telestra units, only match the true boss id
return boss && boss->GetEntry() == NPC_TELESTRA;
}
bool TelestraSplitPhaseTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra");
// Only match split phase with the fake Telestra units
return boss && boss->GetEntry() != NPC_TELESTRA;
}
bool ChaoticRiftTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "anomalus");
return boss && boss->HasAura(BUFF_RIFT_SHIELD);
}
bool OrmorokSpikesTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper");
if (!boss || !botAI->IsTank(bot)) { return false; }
GuidVector objects = AI_VALUE(GuidVector, "closest game objects");
for (auto i = objects.begin(); i != objects.end(); ++i)
{
GameObject* go = botAI->GetGameObject(*i);
if (go && go->GetEntry() == GO_CRYSTAL_SPIKE)
{
return true;
}
}
return false;
}
bool OrmorokStackTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper");
return (boss && !botAI->IsTank(bot));
}
bool IntenseColdTrigger::IsActive()
{
// Adjust as needed - too much interrupting loses dps time,
// but too many stacks is deadly. Assuming 3-5 is a good number to clear
int stackThreshold = 5;
Unit* boss = AI_VALUE2(Unit*, "find target", "keristrasza");
return boss && botAI->GetAura("intense cold", bot, false, false, stackThreshold);
}
bool KeristraszaPositioningTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "keristrasza");
// Include healers here for now, otherwise they stand in things
return boss && !botAI->IsTank(bot) && !botAI->IsRangedDps(bot);
// return boss && botAI->IsMelee(bot) && !botAI->IsTank(bot);
}

View File

@@ -0,0 +1,89 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum NexusIDs
{
// Faction Commander
NPC_ALLIANCE_COMMANDER = 27949,
NPC_HORDE_COMMANDER = 27947,
NPC_COMMANDER_STOUTBEARD = 26796,
NPC_COMMANDER_KOLURG = 26798,
// SPELL_FRIGHTENING_SHOUT = 19134,
SPELL_WHIRLWIND = 38618,
// Grand Magus Telestra
NPC_TELESTRA = 26731,
NPC_FIRE_MAGUS = 26928,
NPC_FROST_MAGUS = 26930,
NPC_ARCANE_MAGUS = 26929,
// Anomalus
BUFF_RIFT_SHIELD = 47748,
// Ormorok the Tree Shaper
// NPC_CRYSTAL_SPIKE = 27099,
GO_CRYSTAL_SPIKE = 188537,
};
class FactionCommanderWhirlwindTrigger : public Trigger
{
public:
FactionCommanderWhirlwindTrigger(PlayerbotAI* ai) : Trigger(ai, "faction commander whirlwind") {}
bool IsActive() override;
};
class TelestraFirebombTrigger : public Trigger
{
public:
TelestraFirebombTrigger(PlayerbotAI* ai) : Trigger(ai, "telestra firebomb spread") {}
bool IsActive() override;
};
class TelestraSplitPhaseTrigger : public Trigger
{
public:
TelestraSplitPhaseTrigger(PlayerbotAI* ai) : Trigger(ai, "telestra split phase") {}
bool IsActive() override;
};
class ChaoticRiftTrigger : public Trigger
{
public:
ChaoticRiftTrigger(PlayerbotAI* ai) : Trigger(ai, "chaotic rift") {}
bool IsActive() override;
};
class OrmorokSpikesTrigger : public Trigger
{
public:
OrmorokSpikesTrigger(PlayerbotAI* ai) : Trigger(ai, "ormorok spikes") {}
bool IsActive() override;
};
class OrmorokStackTrigger : public Trigger
{
public:
OrmorokStackTrigger(PlayerbotAI* ai) : Trigger(ai, "ormorok stack") {}
bool IsActive() override;
};
class IntenseColdTrigger : public Trigger
{
public:
IntenseColdTrigger(PlayerbotAI* ai) : Trigger(ai, "intense cold") {}
bool IsActive() override;
};
class KeristraszaPositioningTrigger : public Trigger
{
public:
KeristraszaPositioningTrigger(PlayerbotAI* ai) : Trigger(ai, "keristrasza positioning") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,354 @@
#include "Playerbots.h"
#include "OculusActions.h"
#include "OculusStrategy.h"
#include "LastSpellCastValue.h"
bool AvoidUnstableSphereAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "drakos the interrogator");
if (!boss) { return false; }
float radius = 12.0f;
float extraDistance = 1.0f;
Unit* closestSphere = nullptr;
GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs");
for (auto& npc : npcs)
{
Unit* unit = botAI->GetUnit(npc);
if (unit && unit->GetEntry() == NPC_UNSTABLE_SPHERE && !unit->isMoving())
{
if (!closestSphere || bot->GetExactDist2d(unit) < bot->GetExactDist2d(closestSphere))
{
closestSphere = unit;
}
}
}
if (closestSphere && bot->GetExactDist2d(closestSphere) < radius + extraDistance)
{
return MoveAway(closestSphere, fmin(3.0f, bot->GetExactDist2d(closestSphere) - radius + extraDistance));
}
return false;
}
bool MountDrakeAction::isPossible() { return bot->GetMapId() == OCULUS_MAP_ID; }
bool MountDrakeAction::Execute(Event event)
{
std::map<int32, int32> drakeAssignments;
// Composition can be adjusted - both 3/1/1 and 2/2/1 are good default comps
// {Amber, Emerald, Ruby}
std::vector<uint8> composition = {2, 2, 1};
// std::vector<uint8> composition = {3, 1, 1};
int32 myIndex = botAI->GetGroupSlotIndex(bot);
Player* master = botAI->GetMaster();
if (!master) { return false; }
Unit* vehicle = master->GetVehicleBase();
if (!vehicle) { return false; }
// Subtract the player's chosen mount type from the composition so player can play whichever they prefer
switch (vehicle->GetEntry())
{
case NPC_AMBER_DRAKE:
composition[0]--;
break;
case NPC_EMERALD_DRAKE:
composition[1]--;
break;
case NPC_RUBY_DRAKE:
composition[2]--;
break;
}
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Player* player = botAI->GetPlayer(member);
if (!player->GetSession()->IsBot()) { continue; }
for (int i = 0; i < composition.size(); i++)
{
if (composition[i] > 0)
{
drakeAssignments[botAI->GetGroupSlotIndex(player)] = DRAKE_ITEMS[i];
composition[i]--;
break;
}
}
}
// Correct/update the drake items in inventories incase assignments have changed
for (uint32 itemId : DRAKE_ITEMS)
{
Item* item = bot->GetItemByEntry(itemId);
if (!item) { continue; }
if (itemId == drakeAssignments[myIndex])
{
// Use our assigned drake
return UseItemAuto(item);
}
// Else assigned drake is different, destroy old drake
uint32 count = 1;
bot->DestroyItemCount(item, count, true);
break;
}
// Bot does not have the correct drake item
bot->AddItem(drakeAssignments[myIndex], 1);
return false;
}
bool DismountDrakeAction::Execute(Event event)
{
if (bot->GetVehicle())
{
bot->ExitVehicle();
return true;
}
return false;
}
bool OccFlyDrakeAction::Execute(Event event)
{
Player* master = botAI->GetMaster();
if (!master) { return false; }
Unit* masterVehicle = master->GetVehicleBase();
Unit* vehicleBase = bot->GetVehicleBase();
if (!vehicleBase || !masterVehicle) { return false; }
MotionMaster* mm = vehicleBase->GetMotionMaster();
Unit* boss = AI_VALUE2(Unit*, "find target", "ley-guardian eregos");
if (boss && !boss->HasAura(SPELL_PLANAR_SHIFT))
{
// Handle as boss encounter instead of formation flight
mm->Clear(false);
float distance = vehicleBase->GetExactDist(boss);
float range = 55.0f; // Drake range is 60yd
if (distance > range)
{
mm->MoveForwards(boss, range - distance);
vehicleBase->SendMovementFlagUpdate();
return true;
}
vehicleBase->SetFacingToObject(boss);
mm->MoveIdle();
vehicleBase->SendMovementFlagUpdate();
return false;
}
if (vehicleBase->GetExactDist(masterVehicle) > 20.0f)
{
// 3/4 of a circle, with frontal cone 90 deg unobstructed
float angle = botAI->GetGroupSlotIndex(bot) * (2*M_PI - M_PI_2)/5 + M_PI_2;
vehicleBase->SetCanFly(true);
mm->MoveFollow(masterVehicle, 15.0f, angle);
vehicleBase->SendMovementFlagUpdate();
return true;
}
return false;
}
bool OccDrakeAttackAction::Execute(Event event)
{
vehicleBase = bot->GetVehicleBase();
if (!vehicleBase) { return false; }
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
{
GuidVector npcs = AI_VALUE(GuidVector, "possible targets");
for (auto& npc : npcs)
{
Unit* unit = botAI->GetUnit(npc);
if (!unit || !unit->IsInCombat()) { continue; }
target = unit;
break;
}
}
// Check this again to see if a target was assigned
if (!target) { return false; }
switch (vehicleBase->GetEntry())
{
case NPC_AMBER_DRAKE:
return AmberDrakeAction(target);
case NPC_EMERALD_DRAKE:
return EmeraldDrakeAction(target);
case NPC_RUBY_DRAKE:
return RubyDrakeAction(target);
default:
break;
}
return false;
}
bool OccDrakeAttackAction::CastDrakeSpellAction(Unit* target, uint32 spellId, uint32 cooldown)
{
if (botAI->CanCastVehicleSpell(spellId, target))
if (botAI->CastVehicleSpell(spellId, target))
{
vehicleBase->AddSpellCooldown(spellId, 0, cooldown);
return true;
}
return false;
}
bool OccDrakeAttackAction::AmberDrakeAction(Unit* target)
{
Aura* shockCharges = target->GetAura(SPELL_SHOCK_CHARGE, vehicleBase->GetGUID());
if (shockCharges && shockCharges->GetStackAmount() > 8)
{
// At 9 charges, better to detonate and re-channel rather than stacking the last charge due to gcd
// If stacking Amber drakes, may need to drop this even lower as the charges stack so fast
return CastDrakeSpellAction(target, SPELL_SHOCK_LANCE, 0);
}
// Deal with enrage after shock charges, as Stop Time adds 5 charges and they may get wasted
if (target->HasAura(SPELL_ENRAGED_ASSAULT) &&
!target->HasAura(SPELL_STOP_TIME) &&
!vehicleBase->HasSpellCooldown(SPELL_STOP_TIME))
{
return CastDrakeSpellAction(target, SPELL_STOP_TIME, 60000);
}
if (!vehicleBase->FindCurrentSpellBySpellId(SPELL_TEMPORAL_RIFT))
{
return CastDrakeSpellAction(target, SPELL_TEMPORAL_RIFT, 0);
}
return false;
}
bool OccDrakeAttackAction::EmeraldDrakeAction(Unit* target)
{
Aura* poisonStacks = target->GetAura(SPELL_LEECHING_POISON, vehicleBase->GetGUID());
if (!poisonStacks || (poisonStacks->GetStackAmount() < 3 ||
poisonStacks->GetDuration() < 4000))
{
return CastDrakeSpellAction(target, SPELL_LEECHING_POISON, 0);
}
if (!vehicleBase->HasSpellCooldown(SPELL_TOUCH_THE_NIGHTMARE) &&
(!target->HasAura(SPELL_TOUCH_THE_NIGHTMARE) || vehicleBase->HealthAbovePct(90)))
{
return CastDrakeSpellAction(target, SPELL_TOUCH_THE_NIGHTMARE, 10000);
}
Unit* healingTarget = nullptr;
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Unit* unit = botAI->GetUnit(member);
if (!unit || bot->GetGUID() == member)
{
continue;
}
Unit* drake = unit->GetVehicleBase();
if (!drake || drake->IsFullHealth()) { continue; }
if (!healingTarget || drake->GetHealthPct() < healingTarget->GetHealthPct() - 15.0f)
{
healingTarget = drake;
}
}
Spell* currentSpell = vehicleBase->FindCurrentSpellBySpellId(SPELL_DREAM_FUNNEL);
if (healingTarget)
{
if (!currentSpell || currentSpell->m_targets.GetUnitTarget() != healingTarget)
{
float distance = vehicleBase->GetExactDist(healingTarget);
float range = 55.0f;
if (distance > range)
{
MotionMaster* mm = vehicleBase->GetMotionMaster();
mm->Clear(false);
mm->MoveForwards(healingTarget, distance - range - 10.0f);
vehicleBase->SendMovementFlagUpdate();
return false;
}
return CastDrakeSpellAction(healingTarget, SPELL_DREAM_FUNNEL, 0);
}
}
// Fill GCDs with Leeching Poison to refresh timer, rather than idling
if (!currentSpell)
{
return CastDrakeSpellAction(target, SPELL_LEECHING_POISON, 0);
}
return false;
}
bool OccDrakeAttackAction::RubyDrakeAction(Unit* target)
{
Aura* evasiveCharges = vehicleBase->GetAura(SPELL_EVASIVE_CHARGES);
Aura* evasiveManeuvers = vehicleBase->GetAura(SPELL_EVASIVE_MANEUVERS);
if (evasiveCharges)
{
if (evasiveManeuvers &&
!vehicleBase->HasSpellCooldown(SPELL_MARTYR) &&
evasiveManeuvers->GetDuration() > 10000 &&
evasiveCharges->GetStackAmount() >= 5)
{
return CastDrakeSpellAction(vehicleBase, SPELL_MARTYR, 10000);
}
if (!vehicleBase->HasSpellCooldown(SPELL_EVASIVE_MANEUVERS) &&
evasiveCharges->GetStackAmount() >= 10)
{
return CastDrakeSpellAction(vehicleBase, SPELL_EVASIVE_MANEUVERS, 5000);
}
}
return CastDrakeSpellAction(target, SPELL_SEARING_WRATH, 0);
}
bool AvoidArcaneExplosionAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "mage-lord urom");
if (!boss) { return false; }
const Position* closestPos = nullptr;
for (auto& position : uromSafePositions)
{
if (!closestPos || bot->GetExactDist(position) < bot->GetExactDist(closestPos))
{
closestPos = &position;
}
}
if (!closestPos) { return false; }
return MoveNear(bot->GetMapId(), closestPos->GetPositionX(), closestPos->GetPositionY(), closestPos->GetPositionZ(), 2.0f, MovementPriority::MOVEMENT_COMBAT);
}
bool TimeBombSpreadAction::Execute(Event event)
{
float radius = 10.0f;
float distanceExtra = 2.0f;
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
if (bot->GetGUID() == member)
{
continue;
}
Unit* unit = botAI->GetUnit(member);
if (unit && bot->GetExactDist2d(unit) < radius)
{
return MoveAway(unit, radius + distanceExtra - bot->GetExactDist2d(unit));
}
}
return false;
}

View File

@@ -0,0 +1,76 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOCCACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONOCCACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "OculusTriggers.h"
#include "UseItemAction.h"
#include "GenericSpellActions.h"
const Position uromSafePositions[3] =
{
Position(1138.88f, 1052.22f, 508.36f),
Position(1084.62f, 1079.71f, 508.36f),
Position(1087.42f, 1020.132f, 508.36f)
};
class AvoidUnstableSphereAction : public MovementAction
{
public:
AvoidUnstableSphereAction(PlayerbotAI* ai) : MovementAction(ai, "avoid unstable sphere") {}
bool Execute(Event event) override;
};
class MountDrakeAction : public UseItemAction
{
public:
MountDrakeAction(PlayerbotAI* ai) : UseItemAction(ai, "mount drake") {}
bool Execute(Event event) override;
bool isPossible() override;
};
class DismountDrakeAction : public Action
{
public:
DismountDrakeAction(PlayerbotAI* ai) : Action(ai, "dismount drake") {}
bool Execute(Event event) override;
};
class OccFlyDrakeAction : public MovementAction
{
public:
OccFlyDrakeAction(PlayerbotAI* ai) : MovementAction(ai, "occ fly drake") {}
bool Execute(Event event) override;
};
class OccDrakeAttackAction : public Action
{
public:
OccDrakeAttackAction(PlayerbotAI* botAI) : Action(botAI, "occ drake attack") {}
bool Execute(Event event) override;
protected:
Unit* vehicleBase;
bool CastDrakeSpellAction(Unit* target, uint32 spellId, uint32 cooldown);
bool AmberDrakeAction(Unit* target);
bool EmeraldDrakeAction(Unit* target);
bool RubyDrakeAction(Unit* target);
};
class AvoidArcaneExplosionAction : public MovementAction
{
public:
AvoidArcaneExplosionAction(PlayerbotAI* ai) : MovementAction(ai, "avoid arcane explosion") {}
bool Execute(Event event) override;
};
class TimeBombSpreadAction : public MovementAction
{
public:
TimeBombSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "time bomb spread") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -0,0 +1,111 @@
#include "OculusMultipliers.h"
#include "OculusActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "OculusTriggers.h"
#include "FollowActions.h"
#include "ReachTargetActions.h"
float MountingDrakeMultiplier::GetValue(Action* action)
{
// P.I.T.A bug where the bots will somehow interrupt their item spell use,
// even though the 0.5 sec cast goes off, it puts the drake essence on 15 sec cd
// and no drake comes down.
// It seems like this is due to moving/other actions being processed during the 0.5 secs.
// If we suppress everything, they seem to mount properly. A bit of a ham-fisted solution but it works
Player* master = botAI->GetMaster();
if (!master) { return 1.0f; }
if (bot->GetMapId() != OCULUS_MAP_ID || !master->GetVehicleBase() || bot->GetVehicleBase()) { return 1.0f; }
if (!dynamic_cast<MountDrakeAction*>(action))
{
return 0.0f;
}
return 1.0f;
}
float OccFlyingMultiplier::GetValue(Action* action)
{
if (bot->GetMapId() != OCULUS_MAP_ID || !bot->GetVehicleBase()) { return 1.0f; }
// Suppresses FollowAction as well as some attack-based movements
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<OccFlyDrakeAction*>(action))
{
return 0.0f;
}
return 1.0f;
}
float UromMultiplier::GetValue(Action* action)
{
if (GetPhaseByCurrentPosition(bot) < 3)
{
Unit* target = action->GetTarget();
if (target && target->GetEntry() == NPC_MAGE_LORD_UROM)
{
return 0.0f;
}
}
Unit* boss = AI_VALUE2(Unit*, "find target", "mage-lord urom");
if (!boss) { return 1.0f; }
// REAL BOSS FIGHT
if (boss->HasUnitState(UNIT_STATE_CASTING) &&
boss->FindCurrentSpellBySpellId(SPELL_EMPOWERED_ARCANE_EXPLOSION))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<AvoidArcaneExplosionAction*>(action))
{
return 0.0f;
}
}
// Don't bother avoiding Frostbomb for melee
if (botAI->IsMelee(bot))
{
if (dynamic_cast<AvoidAoeAction*>(action))
{
return 0.0f;
}
}
if (bot->HasAura(SPELL_TIME_BOMB))
{
if (dynamic_cast<MovementAction*>(action) && !dynamic_cast<TimeBombSpreadAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
uint8 UromMultiplier::GetPhaseByCurrentPosition(Unit* unit)
{
// Distance to return a positive match for spawn platforms, tweak slightly if needed/
// Make sure this doesn't get too large and reach the central ring as well
float distance = 60.0f;
for (uint8 i = 0; i < 3; ++i)
{
if (unit->GetDistance(uromCoords[i][0], uromCoords[i][1], uromCoords[i][2]) < distance)
{
return i;
}
}
return 3;
}
float EregosMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ley-guardian eregos");
if (!boss) { return 1.0f; }
if (boss->HasAura(SPELL_PLANAR_SHIFT && dynamic_cast<OccDrakeAttackAction*>(action)))
{
return 0.0f;
}
return 1.0f;
}

View File

@@ -0,0 +1,53 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOCCMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONOCCMULTIPLIERS_H
#include "Multiplier.h"
#include "Unit.h"
const float uromCoords[4][4] =
{ // Platform coordinates
{1177.47f, 937.722f, 527.405f, 2.21657f},
{968.66f, 1042.53f, 527.32f, 0.077f},
{1164.02f, 1170.85f, 527.321f, 3.66f},
{1118.31f, 1080.377f, 508.361f, 4.25f} // Inner ring, actual boss fight
};
class MountingDrakeMultiplier : public Multiplier
{
public:
MountingDrakeMultiplier(PlayerbotAI* ai) : Multiplier(ai, "mounting drake") {}
public:
virtual float GetValue(Action* action);
};
class OccFlyingMultiplier : public Multiplier
{
public:
OccFlyingMultiplier(PlayerbotAI* ai) : Multiplier(ai, "occ flying drake") {}
public:
virtual float GetValue(Action* action);
};
class UromMultiplier : public Multiplier
{
public:
UromMultiplier(PlayerbotAI* ai) : Multiplier(ai, "mage-lord urom") {}
public:
virtual float GetValue(Action* action);
protected:
uint8 GetPhaseByCurrentPosition(Unit* boss);
};
class EregosMultiplier : public Multiplier
{
public:
EregosMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ley-guardian eregos") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,30 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOCCACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONOCCACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "OculusActions.h"
class WotlkDungeonOccActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonOccActionContext() {
creators["avoid unstable sphere"] = &WotlkDungeonOccActionContext::avoid_unstable_sphere;
creators["mount drake"] = &WotlkDungeonOccActionContext::mount_drake;
creators["dismount drake"] = &WotlkDungeonOccActionContext::dismount_drake;
creators["occ fly drake"] = &WotlkDungeonOccActionContext::occ_fly_drake;
creators["occ drake attack"] = &WotlkDungeonOccActionContext::occ_drake_attack;
creators["avoid arcane explosion"] = &WotlkDungeonOccActionContext::avoid_arcane_explosion;
creators["time bomb spread"] = &WotlkDungeonOccActionContext::time_bomb_spread;
}
private:
static Action* avoid_unstable_sphere(PlayerbotAI* ai) { return new AvoidUnstableSphereAction(ai); }
static Action* mount_drake(PlayerbotAI* ai) { return new MountDrakeAction(ai); }
static Action* dismount_drake(PlayerbotAI* ai) { return new DismountDrakeAction(ai); }
static Action* occ_fly_drake(PlayerbotAI* ai) { return new OccFlyDrakeAction(ai); }
static Action* occ_drake_attack(PlayerbotAI* ai) { return new OccDrakeAttackAction(ai); }
static Action* avoid_arcane_explosion(PlayerbotAI* ai) { return new AvoidArcaneExplosionAction(ai); }
static Action* time_bomb_spread(PlayerbotAI* ai) { return new TimeBombSpreadAction(ai); }
};
#endif

View File

@@ -0,0 +1,33 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "OculusTriggers.h"
class WotlkDungeonOccTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonOccTriggerContext()
{
creators["unstable sphere"] = &WotlkDungeonOccTriggerContext::unstable_sphere;
creators["drake mount"] = &WotlkDungeonOccTriggerContext::drake_mount;
creators["drake dismount"] = &WotlkDungeonOccTriggerContext::drake_dismount;
creators["group flying"] = &WotlkDungeonOccTriggerContext::group_flying;
creators["drake combat"] = &WotlkDungeonOccTriggerContext::drake_combat;
creators["varos cloudstrider"] = &WotlkDungeonOccTriggerContext::varos_cloudstrider;
creators["arcane explosion"] = &WotlkDungeonOccTriggerContext::arcane_explosion;
creators["time bomb"] = &WotlkDungeonOccTriggerContext::time_bomb;
}
private:
static Trigger* unstable_sphere(PlayerbotAI* ai) { return new DrakosUnstableSphereTrigger(ai); }
static Trigger* drake_mount(PlayerbotAI* ai) { return new DrakeMountTrigger(ai); }
static Trigger* drake_dismount(PlayerbotAI* ai) { return new DrakeDismountTrigger(ai); }
static Trigger* group_flying(PlayerbotAI* ai) { return new GroupFlyingTrigger(ai); }
static Trigger* drake_combat(PlayerbotAI* ai) { return new DrakeCombatTrigger(ai); }
static Trigger* varos_cloudstrider(PlayerbotAI* ai) { return new VarosCloudstriderTrigger(ai); }
static Trigger* arcane_explosion(PlayerbotAI* ai) { return new UromArcaneExplosionTrigger(ai); }
static Trigger* time_bomb(PlayerbotAI* ai) { return new UromTimeBombTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,41 @@
#include "OculusStrategy.h"
#include "OculusMultipliers.h"
void WotlkDungeonOccStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Drakos the Interrogator
// TODO: May need work, TBA.
triggers.push_back(new TriggerNode("unstable sphere",
{ NextAction("avoid unstable sphere", ACTION_MOVE + 5) }));
// DRAKES
triggers.push_back(new TriggerNode("drake mount",
{ NextAction("mount drake", ACTION_RAID + 5) }));
triggers.push_back(new TriggerNode("drake dismount",
{ NextAction("dismount drake", ACTION_RAID + 5) }));
triggers.push_back(new TriggerNode("group flying",
{ NextAction("occ fly drake", ACTION_NORMAL + 1) }));
triggers.push_back(new TriggerNode("drake combat",
{ NextAction("occ drake attack", ACTION_NORMAL + 5) }));
// Varos Cloudstrider
// Seems to be no way to identify the marked cores, may need to hook boss AI..
// triggers.push_back(new TriggerNode("varos cloudstrider",
// { NextAction("avoid energize cores", ACTION_RAID + 5) }));
// Mage-Lord Urom
triggers.push_back(new TriggerNode("arcane explosion",
{ NextAction("avoid arcane explosion", ACTION_MOVE + 5) }));
triggers.push_back(new TriggerNode("time bomb",
{ NextAction("time bomb spread", ACTION_MOVE + 4) }));
// Ley-Guardian Eregos
}
void WotlkDungeonOccStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new MountingDrakeMultiplier(botAI));
multipliers.push_back(new OccFlyingMultiplier(botAI));
multipliers.push_back(new UromMultiplier(botAI));
multipliers.push_back(new EregosMultiplier(botAI));
}

View File

@@ -0,0 +1,17 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOCCSTRATEGY_H
#define _PLAYERBOT_WOTLKDUNGEONOCCSTRATEGY_H
#include "Multiplier.h"
#include "AiObjectContext.h"
#include "Strategy.h"
class WotlkDungeonOccStrategy : public Strategy
{
public:
WotlkDungeonOccStrategy(PlayerbotAI* ai) : Strategy(ai) {}
virtual std::string const getName() override { return "oculus"; }
virtual void InitTriggers(std::vector<TriggerNode*> &triggers) override;
virtual void InitMultipliers(std::vector<Multiplier*> &multipliers) override;
};
#endif

View File

@@ -0,0 +1,75 @@
#include "Playerbots.h"
#include "OculusTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
#include "Unit.h"
bool DrakosUnstableSphereTrigger::IsActive()
{
// Doesn't seem to be much point trying to get melee to dodge this,
// they get hit anyway and it just causes a lot of running around and chaos
// if (botAI->IsMelee(bot)) { return false; }
if (botAI->IsTank(bot)) { return false; }
GuidVector targets = AI_VALUE(GuidVector, "nearest hostile npcs");
for (auto& target : targets)
{
Unit* unit = botAI->GetUnit(target);
if (unit && unit->GetEntry() == NPC_UNSTABLE_SPHERE)
{
return true;
}
}
return false;
}
bool DrakeMountTrigger::IsActive()
{
Player* master = botAI->GetMaster();
if (!master) { return false; }
return master->GetVehicleBase() && !bot->GetVehicleBase();
}
bool DrakeDismountTrigger::IsActive()
{
Player* master = botAI->GetMaster();
if (!master) { return false; }
return !master->GetVehicleBase() && bot->GetVehicleBase();
}
bool GroupFlyingTrigger::IsActive()
{
Player* master = botAI->GetMaster();
if (!master) { return false; }
return master->GetVehicleBase() && bot->GetVehicleBase();
}
bool DrakeCombatTrigger::IsActive()
{
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
return !targets.empty();
}
bool VarosCloudstriderTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "varos cloudstrider");
if (!boss) { return false; }
return true;
}
bool UromArcaneExplosionTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "mage-lord urom");
if (!boss) { return false; }
return bool(boss->FindCurrentSpellBySpellId(SPELL_EMPOWERED_ARCANE_EXPLOSION));
}
bool UromTimeBombTrigger::IsActive()
{
return bot->HasAura(SPELL_TIME_BOMB);
}

View File

@@ -0,0 +1,129 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum OculusIDs
{
// Drakos the Interrogator
NPC_UNSTABLE_SPHERE = 28166,
SPELL_UNSTABLE_SPHERE_PASSIVE = 50756,
SPELL_UNSTABLE_SPHERE_PULSE = 50757,
SPELL_UNSTABLE_SPHERE_TIMER = 50758,
// Drakes
NPC_AMBER_DRAKE = 27755,
NPC_EMERALD_DRAKE = 27692,
NPC_RUBY_DRAKE = 27756,
ITEM_AMBER_ESSENCE = 37859,
ITEM_EMERALD_ESSENCE = 37815,
ITEM_RUBY_ESSENCE = 37860,
SPELL_AMBER_ESSENCE = 49461,
SPELL_EMERALD_ESSENCE = 49345,
SPELL_RUBY_ESSENCE = 49462,
// Abilities:
// Amber
SPELL_SHOCK_LANCE = 49840,
SPELL_SHOCK_CHARGE = 49836,
SPELL_STOP_TIME = 49838,
SPELL_TEMPORAL_RIFT = 49592,
// Emerald
SPELL_LEECHING_POISON = 50328,
SPELL_TOUCH_THE_NIGHTMARE = 50341,
SPELL_DREAM_FUNNEL = 50344,
// Ruby
SPELL_SEARING_WRATH = 50232,
SPELL_EVASIVE_MANEUVERS = 50240,
SPELL_EVASIVE_CHARGES = 50241,
SPELL_MARTYR = 50253,
// Varos Cloudstrider
NPC_CENTRIFUGE_CORE = 28183,
// Mage-Lord Urom
NPC_MAGE_LORD_UROM = 27655,
SPELL_TIME_BOMB_N = 51121,
SPELL_TIME_BOMB_H = 59376,
SPELL_EMPOWERED_ARCANE_EXPLOSION_N = 51110,
SPELL_EMPOWERED_ARCANE_EXPLOSION_H = 59377,
// Ley-Guardian Eregos
SPELL_ENRAGED_ASSAULT = 51170,
SPELL_PLANAR_SHIFT = 51162,
};
#define SPELL_EMPOWERED_ARCANE_EXPLOSION DUNGEON_MODE(bot, SPELL_EMPOWERED_ARCANE_EXPLOSION_N, SPELL_EMPOWERED_ARCANE_EXPLOSION_H)
#define SPELL_TIME_BOMB DUNGEON_MODE(bot, SPELL_TIME_BOMB_N, SPELL_TIME_BOMB_H)
const std::vector<uint32> DRAKE_ITEMS = {ITEM_AMBER_ESSENCE, ITEM_EMERALD_ESSENCE, ITEM_RUBY_ESSENCE};
const std::vector<uint32> DRAKE_SPELLS = {SPELL_AMBER_ESSENCE, SPELL_EMERALD_ESSENCE, SPELL_RUBY_ESSENCE};
const uint32 OCULUS_MAP_ID = 578;
// const float uromCoords[4][4] =
// {
// {1177.47f, 937.722f, 527.405f, 2.21657f},
// {968.66f, 1042.53f, 527.32f, 0.077f},
// {1164.02f, 1170.85f, 527.321f, 3.66f},
// {1118.31f, 1080.377f, 508.361f, 4.25f} // Inner ring, actual boss fight
// };
class DrakosUnstableSphereTrigger : public Trigger
{
public:
DrakosUnstableSphereTrigger(PlayerbotAI* ai) : Trigger(ai, "drakos unstable sphere") {}
bool IsActive() override;
};
class DrakeMountTrigger : public Trigger
{
public:
DrakeMountTrigger(PlayerbotAI* ai) : Trigger(ai, "drake mount") {}
bool IsActive() override;
};
class DrakeDismountTrigger : public Trigger
{
public:
DrakeDismountTrigger(PlayerbotAI* ai) : Trigger(ai, "drake dismount") {}
bool IsActive() override;
};
class GroupFlyingTrigger : public Trigger
{
public:
GroupFlyingTrigger(PlayerbotAI* ai) : Trigger(ai, "drake fly") {}
bool IsActive() override;
};
class DrakeCombatTrigger : public Trigger
{
public:
DrakeCombatTrigger(PlayerbotAI* ai) : Trigger(ai, "drake combat") {}
bool IsActive() override;
};
class VarosCloudstriderTrigger : public Trigger
{
public:
VarosCloudstriderTrigger(PlayerbotAI* ai) : Trigger(ai, "varos cloudstrider") {}
bool IsActive() override;
};
class UromArcaneExplosionTrigger : public Trigger
{
public:
UromArcaneExplosionTrigger(PlayerbotAI* ai) : Trigger(ai, "urom arcane explosion") {}
bool IsActive() override;
};
class UromTimeBombTrigger : public Trigger
{
public:
UromTimeBombTrigger(PlayerbotAI* ai) : Trigger(ai, "urom time bomb") {}
bool IsActive() override;
};
#endif

View File

@@ -0,0 +1,87 @@
#include "Playerbots.h"
#include "OldKingdomActions.h"
#include "OldKingdomStrategy.h"
bool AttackNadoxGuardianAction::Execute(Event event)
{
Unit* target = AI_VALUE2(Unit*, "find target", "ahn'kahar guardian");
if (!target || AI_VALUE(Unit*, "current target") == target)
{
return false;
}
return Attack(target);
}
bool AttackJedogaVolunteerAction::Execute(Event event)
{
Unit* target = nullptr;
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (!unit) // Null check for safety
{
continue; // Skip null or invalid units
}
if (unit && unit->GetEntry() == NPC_TWILIGHT_VOLUNTEER)
{
target = unit;
break;
}
}
if (!target || AI_VALUE(Unit*, "current target") == target)
{
return false;
}
return Attack(target);
}
bool AvoidShadowCrashAction::Execute(Event event)
{
// Could check all enemy units in range as it's possible to pull multiple of these mobs.
// They should really be killed 1 by 1, multipulls are messy so we just handle singles for now
Unit* unit = AI_VALUE2(Unit*, "find target", "forgotten one");
if (!unit) { return false; }
Unit* victim = nullptr;
float radius = 10.0f;
float targetDist = radius + 2.0f;
// Actively move if targeted by a shadow crash.
// Spell check not needed, they don't have any other non-instant casts
if (unit->HasUnitState(UNIT_STATE_CASTING)) // && unit->FindCurrentSpellBySpellId(SPELL_SHADOW_CRASH))
{
// This doesn't seem to avoid casts very well, perhaps because this isn't checked while allies are casting.
// TODO: Revisit if this is an issue in heroics, otherwise ignore shadow crashes for the most part.
victim = botAI->GetUnit(unit->GetTarget());
float distance = bot->GetExactDist2d(victim->GetPosition());
if (victim && distance < radius)
{
return MoveAway(victim, targetDist - distance);
}
}
// Otherwise ranged members passively spread, to avoid AoE overlap
if (botAI->IsMelee(bot)) { return false; }
GuidVector members = AI_VALUE(GuidVector, "group members");
for (auto& member : members)
{
Unit* unit = botAI->GetUnit(member);
if (!unit || bot->GetGUID() == member)
{
continue;
}
float currentDist = bot->GetExactDist2d(botAI->GetUnit(member));
if (currentDist < radius)
{
return MoveAway(botAI->GetUnit(member), targetDist - currentDist);
}
}
return false;
}

View File

@@ -0,0 +1,31 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOKACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONOKACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "OldKingdomTriggers.h"
class AttackNadoxGuardianAction : public AttackAction
{
public:
AttackNadoxGuardianAction(PlayerbotAI* ai) : AttackAction(ai, "attack nadox guardian") {}
bool Execute(Event event) override;
};
class AttackJedogaVolunteerAction : public AttackAction
{
public:
AttackJedogaVolunteerAction(PlayerbotAI* ai) : AttackAction(ai, "attack jedoga volunteer") {}
bool Execute(Event event) override;
};
class AvoidShadowCrashAction : public MovementAction
{
public:
AvoidShadowCrashAction(PlayerbotAI* ai) : MovementAction(ai, "avoid shadow crash") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -0,0 +1,68 @@
#include "OldKingdomMultipliers.h"
#include "OldKingdomActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "OldKingdomTriggers.h"
#include "Action.h"
float ElderNadoxMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "elder nadox");
if (!boss) { return 1.0f; }
Unit* guardian = AI_VALUE2(Unit*, "find target", "ahn'kahar guardian");
if (guardian)
{
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float JedogaShadowseekerMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "jedoga shadowseeker");
if (!boss) { return 1.0f; }
Unit* volunteer = nullptr;
// Target is not findable from threat table using AI_VALUE2(),
// therefore need to search manually for the unit name
GuidVector targets = AI_VALUE(GuidVector, "possible targets no los");
for (auto i = targets.begin(); i != targets.end(); ++i)
{
Unit* unit = botAI->GetUnit(*i);
if (unit && unit->GetEntry() == NPC_TWILIGHT_VOLUNTEER)
{
volunteer = unit;
break;
}
}
if (volunteer)
{
if (dynamic_cast<DpsAssistAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}
float ForgottenOneMultiplier::GetValue(Action* action)
{
Unit* unit = AI_VALUE2(Unit*, "find target", "forgotten one");
if (!unit) { return 1.0f; }
if (bot->isMoving())
{
if (dynamic_cast<MovementAction*>(action))
{
return 0.0f;
}
}
return 1.0f;
}

View File

@@ -0,0 +1,33 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOKMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONOKMULTIPLIERS_H
#include "Multiplier.h"
class ElderNadoxMultiplier : public Multiplier
{
public:
ElderNadoxMultiplier(PlayerbotAI* ai) : Multiplier(ai, "elder nadox") {}
public:
virtual float GetValue(Action* action);
};
class JedogaShadowseekerMultiplier : public Multiplier
{
public:
JedogaShadowseekerMultiplier(PlayerbotAI* ai) : Multiplier(ai, "jedoga shadowseeker") {}
public:
virtual float GetValue(Action* action);
};
class ForgottenOneMultiplier : public Multiplier
{
public:
ForgottenOneMultiplier(PlayerbotAI* ai) : Multiplier(ai, "forgotten one") {}
public:
virtual float GetValue(Action* action);
};
#endif

View File

@@ -0,0 +1,22 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOKACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONOKACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "OldKingdomActions.h"
class WotlkDungeonOKActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonOKActionContext() {
creators["attack nadox guardian"] = &WotlkDungeonOKActionContext::attack_nadox_guardian;
creators["attack jedoga volunteer"] = &WotlkDungeonOKActionContext::attack_jedoga_volunteer;
creators["avoid shadow crash"] = &WotlkDungeonOKActionContext::avoid_shadow_crash;
}
private:
static Action* attack_nadox_guardian(PlayerbotAI* ai) { return new AttackNadoxGuardianAction(ai); }
static Action* attack_jedoga_volunteer(PlayerbotAI* ai) { return new AttackJedogaVolunteerAction(ai); }
static Action* avoid_shadow_crash(PlayerbotAI* ai) { return new AvoidShadowCrashAction(ai); }
};
#endif

View File

@@ -0,0 +1,23 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONOKTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONOKTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "OldKingdomTriggers.h"
class WotlkDungeonOKTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonOKTriggerContext()
{
creators["nadox guardian"] = &WotlkDungeonOKTriggerContext::nadox_guardian;
creators["jedoga volunteer"] = &WotlkDungeonOKTriggerContext::jedoga_volunteer;
creators["shadow crash"] = &WotlkDungeonOKTriggerContext::shadow_crash;
}
private:
static Trigger* nadox_guardian(PlayerbotAI* ai) { return new NadoxGuardianTrigger(ai); }
static Trigger* jedoga_volunteer(PlayerbotAI* ai) { return new JedogaVolunteerTrigger(ai); }
static Trigger* shadow_crash(PlayerbotAI* ai) { return new ShadowCrashTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,35 @@
#include "OldKingdomStrategy.h"
#include "OldKingdomMultipliers.h"
void WotlkDungeonOKStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{
// Elder Nadox
triggers.push_back(new TriggerNode("nadox guardian",
{ NextAction("attack nadox guardian", ACTION_RAID + 5) }));
// Prince Taldaram
// Flame Orb spawns in melee, doesn't have a clear direction until it starts moving.
// Maybe not worth trying to avoid and just heal through. Only consideration is not to have ranged
// players anywhere near melee when it spawns
// Jedoga Shadowseeker
triggers.push_back(new TriggerNode("jedoga volunteer",
{ NextAction("attack jedoga volunteer", ACTION_RAID + 5) }));
// Herald Volazj
// Trash mobs before him have a big telegraphed shadow crash spell,
// this can be avoided and is intended to be dodged
triggers.push_back(new TriggerNode("shadow crash",
{ NextAction("avoid shadow crash", ACTION_MOVE + 5) }));
// Volazj is not implemented properly in AC, insanity phase does nothing.
// Amanitar (Heroic Only)
// TODO: once I get to heroics
}
void WotlkDungeonOKStrategy::InitMultipliers(std::vector<Multiplier*> &multipliers)
{
multipliers.push_back(new ElderNadoxMultiplier(botAI));
multipliers.push_back(new JedogaShadowseekerMultiplier(botAI));
multipliers.push_back(new ForgottenOneMultiplier(botAI));
}

Some files were not shown because too many files have changed in this diff Show More