PoS Strategies (#1283)

PoS Strategies
This commit is contained in:
Noscopezz
2025-05-11 00:24:09 +02:00
committed by GitHub
parent dc703cc897
commit 3881940f33
16 changed files with 571 additions and 10 deletions

View File

@@ -75,6 +75,7 @@ AiObjectContext::AiObjectContext(PlayerbotAI* botAI) : PlayerbotAIAware(botAI)
actionContexts.Add(new WotlkDungeonUPActionContext());
actionContexts.Add(new WotlkDungeonCoSActionContext());
actionContexts.Add(new WotlkDungeonFoSActionContext());
actionContexts.Add(new WotlkDungeonPoSActionContext());
actionContexts.Add(new WotlkDungeonToCActionContext());
triggerContexts.Add(new TriggerContext());
@@ -102,7 +103,8 @@ AiObjectContext::AiObjectContext(PlayerbotAI* botAI) : PlayerbotAIAware(botAI)
triggerContexts.Add(new WotlkDungeonOccTriggerContext());
triggerContexts.Add(new WotlkDungeonUPTriggerContext());
triggerContexts.Add(new WotlkDungeonCoSTriggerContext());
triggerContexts.Add(new WotlkDungeonFosTriggerContext());
triggerContexts.Add(new WotlkDungeonFoSTriggerContext());
triggerContexts.Add(new WotlkDungeonPoSTriggerContext());
triggerContexts.Add(new WotlkDungeonToCTriggerContext());
valueContexts.Add(new ValueContext());

View File

@@ -15,6 +15,7 @@
#include "wotlk/utgardepinnacle/UtgardePinnacleStrategy.h"
#include "wotlk/cullingofstratholme/CullingOfStratholmeStrategy.h"
#include "wotlk/forgeofsouls/ForgeOfSoulsStrategy.h"
#include "wotlk/pitofsaron/PitOfSaronStrategy.h"
#include "wotlk/trialofthechampion/TrialOfTheChampionStrategy.h"
/*
@@ -79,10 +80,11 @@ class DungeonStrategyContext : public NamedObjectContext<Strategy>
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); }
static Strategy* wotlk_pos(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); }
};

View File

@@ -14,8 +14,8 @@
#include "utgardepinnacle/UtgardePinnacleActionContext.h"
#include "cullingofstratholme/CullingOfStratholmeActionContext.h"
#include "forgeofsouls/ForgeOfSoulsActionContext.h"
#include "pitofsaron/PitOfSaronActionContext.h"
#include "trialofthechampion/TrialOfTheChampionActionContext.h"
// #include "hallsofreflection/HallsOfReflectionActionContext.h"
// #include "pitofsaron/PitOfSaronActionContext.h"
#endif

View File

@@ -14,8 +14,9 @@
#include "utgardepinnacle/UtgardePinnacleTriggerContext.h"
#include "cullingofstratholme/CullingOfStratholmeTriggerContext.h"
#include "forgeofsouls/ForgeOfSoulsTriggerContext.h"
#include "pitofsaron/PitOfSaronTriggerContext.h"
#include "trialofthechampion/TrialOfTheChampionTriggerContext.h"
// #include "hallsofreflection/HallsOfReflectionTriggerContext.h"
// #include "pitofsaron/PitOfSaronTriggerContext.h"
#endif

View File

@@ -5,15 +5,15 @@
#include "AiObjectContext.h"
#include "ForgeOfSoulsTriggers.h"
class WotlkDungeonFosTriggerContext : public NamedObjectContext<Trigger>
class WotlkDungeonFoSTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonFosTriggerContext()
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;
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:

View File

@@ -0,0 +1,21 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONPOSACTIONCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONPOSACTIONCONTEXT_H
#include "Action.h"
#include "NamedObjectContext.h"
#include "PitOfSaronActions.h"
class WotlkDungeonPoSActionContext : public NamedObjectContext<Action>
{
public:
WotlkDungeonPoSActionContext()
{
creators["ick and krick"] = &WotlkDungeonPoSActionContext::ick_and_krick;
creators["tyrannus"] = &WotlkDungeonPoSActionContext::tyrannus;
}
private:
static Action* ick_and_krick(PlayerbotAI* ai) { return new IckAndKrickAction(ai); }
static Action* tyrannus(PlayerbotAI* ai) { return new TyrannusAction(ai); }
};
#endif

View File

@@ -0,0 +1,315 @@
#include "Playerbots.h"
#include "PitOfSaronActions.h"
#include "PitOfSaronStrategy.h"
#include "SharedDefines.h"
bool IckAndKrickAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "Ick");
if (!boss)
return false;
std::vector<Unit*> orbs;
bool orb = false;
// First gather all orbs
GuidVector npcs1 = AI_VALUE(GuidVector, "nearest hostile npcs");
for (auto& npc : npcs1)
{
Unit* unit = botAI->GetUnit(npc);
if (unit && unit->IsAlive() && unit->HasAura(SPELL_EXPLODING_ORB_VISUAL))
{
orb = true;
break;
}
}
bool pursuit = bot->HasAura(SPELL_PURSUIT) || (!botAI->IsTank(bot) && boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_PURSUIT));
bool poisonNova = boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA_POS) || boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA_POS_HC));
bool explosiveBarrage = orb || boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_ICK) || boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_KRICK));
bool isTank = botAI->IsTank(bot);
if (pursuit && Pursuit(pursuit, boss))
return true;
if (poisonNova && PoisonNova(poisonNova, boss))
return true;
if (explosiveBarrage && ExplosiveBarrage(explosiveBarrage, boss))
return true;
if (isTank && !pursuit && !poisonNova && !explosiveBarrage)
{
if (TankPosition(boss))
return true;
}
return false;
}
bool IckAndKrickAction::TankPosition(Unit* boss)
{
if (botAI->HasAggro(boss))
{
float distance = bot->GetExactDist2d(ICKANDKRICK_TANK_POSITION.GetPositionX(), ICKANDKRICK_TANK_POSITION.GetPositionY());
if (distance > 7.0f)
{
// Calculate direction vector
float dirX = ICKANDKRICK_TANK_POSITION.GetPositionX() - bot->GetPositionX();
float dirY = ICKANDKRICK_TANK_POSITION.GetPositionY() - bot->GetPositionY();
float length = sqrt(dirX * dirX + dirY * dirY);
dirX /= length;
dirY /= length;
// Move in increments of 3.0f
float moveX = bot->GetPositionX() + dirX * 3.0f;
float moveY = bot->GetPositionY() + dirY * 3.0f;
return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT);
}
}
return false;
}
bool IckAndKrickAction::Pursuit(bool pursuit, Unit* boss)
{
// Only execute this action when pursuit is active and for non-tank players
if (!pursuit || botAI->IsTank(bot))
return false;
// Get the tank position as a reference point
Position tankPosition = ICKANDKRICK_TANK_POSITION;
// Calculate distance from boss
float distanceToBoss = bot->GetExactDist2d(boss);
// If boss is close, move in a circular pattern
if (distanceToBoss < 20.0f)
{
// Current bot position
float x = bot->GetPositionX();
float y = bot->GetPositionY();
// Calculate vector from tank to bot
float dx = x - tankPosition.GetPositionX();
float dy = y - tankPosition.GetPositionY();
// Normalize the vector
float distance = std::sqrt(dx * dx + dy * dy);
if (distance > 0)
{
dx /= distance;
dy /= distance;
}
// Rotate the vector by 90 degrees to create circular movement
// (x,y) rotated 90 degrees becomes (-y,x)
float rotatedX = -dy;
float rotatedY = dx;
// Create a target position along the circular path
// We want to maintain a specific radius from the tank
float targetRadius = 20.0f; // 20 feet radius circle around tank
Position targetPos;
targetPos.Relocate(tankPosition.GetPositionX() + rotatedX * targetRadius,
tankPosition.GetPositionY() + rotatedY * targetRadius, bot->GetPositionZ());
// Move to the calculated circular position
botAI->Reset();
return MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), bot->GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_COMBAT);
}
return false;
}
bool IckAndKrickAction::PoisonNova(bool poisonNova, Unit* boss)
{
if (bot->GetExactDist2d(boss) < 20.0f && poisonNova)
return MoveAway(boss, 11.0f);
else if (poisonNova)
return true;
return false;
}
bool IckAndKrickAction::ExplosiveBarrage(bool explosiveBarrage, Unit* boss)
{
std::vector<Unit*> orbs;
Unit* closestOrb = nullptr;
float closestDistance = std::numeric_limits<float>::max();
// First gather all orbs
GuidVector npcs1 = AI_VALUE(GuidVector, "nearest hostile npcs");
for (auto& npc : npcs1)
{
Unit* unit = botAI->GetUnit(npc);
if (unit && unit->IsAlive() && unit->HasAura(SPELL_EXPLODING_ORB_VISUAL))
{
orbs.push_back(unit);
float dist = bot->GetDistance(unit);
if (dist < closestDistance)
{
closestDistance = dist;
closestOrb = unit;
}
}
}
if (!orbs.empty() && closestOrb && bot->GetExactDist2d(closestOrb) < 7.0f)
{
// Get player positions to avoid moving toward them
std::vector<Position> playerPositions;
Group* group = bot->GetGroup();
if (group)
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
if (Player* member = ref->GetSource())
{
if (member != bot && member->IsAlive() && bot->GetMap() == member->GetMap() &&
bot->GetExactDist2d(member) < 20.0f)
{
playerPositions.push_back(member->GetPosition());
}
}
}
}
// Calculate several potential escape positions
const int NUM_ANGLES = 16; // Check 16 different directions
const float SAFE_DISTANCE = 7.0f;
const float MAX_BOSS_DISTANCE = 30.0f; // Maximum allowed distance from boss
const float ANGLE_INCREMENT = 2 * M_PI / NUM_ANGLES;
Position bestPos;
bool foundValidPos = false;
float bestScore = -1.0f;
for (int i = 0; i < NUM_ANGLES; i++)
{
float angle = i * ANGLE_INCREMENT;
Position potentialPos;
potentialPos.m_positionX = bot->GetPositionX() + SAFE_DISTANCE * cos(angle);
potentialPos.m_positionY = bot->GetPositionY() + SAFE_DISTANCE * sin(angle);
potentialPos.m_positionZ = bot->GetPositionZ();
// Check if position is valid (not in a wall)
if (!bot->IsWithinLOS(potentialPos.m_positionX, potentialPos.m_positionY, potentialPos.m_positionZ))
continue;
// Check if position is within maximum allowed distance from boss
if (boss && sqrt(pow(potentialPos.m_positionX - boss->GetPositionX(), 2) +
pow(potentialPos.m_positionY - boss->GetPositionY(), 2)) > MAX_BOSS_DISTANCE)
continue;
// Score this position based on:
// 1. Distance from all orbs (farther is better)
// 2. Distance from other players (farther is better)
// 3. Distance from boss (closer is better, but still safe)
float score = 0.0f;
// Check distance from all orbs
float minOrbDist = std::numeric_limits<float>::max();
for (Unit* orb : orbs)
{
float orbDist = sqrt(pow(potentialPos.m_positionX - orb->GetPositionX(), 2) +
pow(potentialPos.m_positionY - orb->GetPositionY(), 2));
minOrbDist = std::min(minOrbDist, orbDist);
}
score += minOrbDist * 2.0f; // Weight orb distance more heavily
// Check distance from other players
for (const Position& playerPos : playerPositions)
{
float playerDist = sqrt(pow(potentialPos.m_positionX - playerPos.m_positionX, 2) +
pow(potentialPos.m_positionY - playerPos.m_positionY, 2));
score += std::min(playerDist, 10.0f); // Cap player distance contribution
}
// Factor in proximity to boss (closer is better, as long as we're safe from orbs)
if (boss)
{
float bossDist = sqrt(pow(potentialPos.m_positionX - boss->GetPositionX(), 2) +
pow(potentialPos.m_positionY - boss->GetPositionY(), 2));
// Add points for being closer to boss (inverse relationship)
// but only if we're safely away from orbs
if (minOrbDist > SAFE_DISTANCE)
{
score += (MAX_BOSS_DISTANCE - bossDist) * 0.5f;
}
}
// If this is the best position so far, remember it
if (score > bestScore)
{
bestScore = score;
bestPos = potentialPos;
foundValidPos = true;
}
}
// If we found a valid position, move there
if (foundValidPos)
{
botAI->Reset();
// Use MoveTo instead of FleePosition since we already calculated the exact position
return MoveTo(bot->GetMapId(), bestPos.m_positionX, bestPos.m_positionY, bot->GetPositionZ(), false, false,
false, true, MovementPriority::MOVEMENT_COMBAT);
}
else
{
return FleePosition(closestOrb->GetPosition(), 7.0f, 250U);
}
}
return false;
}
bool TyrannusAction::Execute(Event event)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "scourgelord tyrannus");
if (!boss)
return false;
bool rangedSpread = false;
if (botAI->IsRanged(bot) && boss->HealthBelowPct(99))
rangedSpread = true;
if (rangedSpread && RangedSpread(rangedSpread))
return true;
return false;
}
bool TyrannusAction::RangedSpread(bool rangedSpread)
{
float radius = 10.0f;
float moveIncrement = 3.0f;
GuidVector members = AI_VALUE(GuidVector, "group members");
if (botAI->IsRanged(bot) && rangedSpread)
{
// Ranged: spread from other members
for (auto& member : members)
{
Unit* unit = botAI->GetUnit(member);
if (!unit || !unit->IsAlive() || unit == bot)
continue;
float dist = bot->GetExactDist2d(unit);
if (dist < radius)
{
float moveDistance = std::min(moveIncrement, radius - dist + 1.0f);
return FleePosition(unit->GetPosition(), moveDistance, 250U);
}
}
}
return false;
}

View File

@@ -0,0 +1,32 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONPOSACTIONS_H
#define _PLAYERBOT_WOTLKDUNGEONPOSACTIONS_H
#include "Action.h"
#include "AttackAction.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "PitOfSaronTriggers.h"
const Position ICKANDKRICK_TANK_POSITION = Position(816.8508f, 102.331505f, 509.1586f);
class IckAndKrickAction : public AttackAction
{
public:
IckAndKrickAction(PlayerbotAI* ai) : AttackAction(ai, "ick and krick") {}
bool Execute(Event event) override;
bool TankPosition(Unit* boss);
bool Pursuit(bool pursuit, Unit* boss);
bool PoisonNova(bool poisonNova, Unit* boss);
bool ExplosiveBarrage(bool explosiveBarrage, Unit* boss);
};
class TyrannusAction : public AttackAction
{
public:
TyrannusAction(PlayerbotAI* ai) : AttackAction(ai, "tyrannus") {}
bool Execute(Event event) override;
bool RangedSpread(bool rangedSpread);
};
#endif

View File

@@ -0,0 +1,39 @@
#include "PitOfSaronMultipliers.h"
#include "PitOfSaronActions.h"
#include "GenericSpellActions.h"
#include "ChooseTargetActions.h"
#include "MovementActions.h"
#include "PitOfSaronTriggers.h"
float IckAndKrickMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "ick");
if (!boss)
return 1.0f;
// Allow the IckAndKrickAction to run
if (dynamic_cast<IckAndKrickAction*>(action))
return 1.0f;
if (boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA_POS) || boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA_POS_HC)) && bot->GetExactDist2d(boss) < 20.0f)
return 0.0f; // Cancel all other actions when we need to handle Poison Nova
if (bot->GetExactDist2d(boss) < 15.0f && bot->HasAura(SPELL_PURSUIT) && !botAI->IsTank(bot))
return 0.0f; // Cancel all other actions when we need to handle Pursuit
if (!botAI->IsHeal(bot) && boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_ICK) || boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_KRICK)))
return 0.0f; // Cancel all other actions when we need to handle Explosive Barrage
return 1.0f;
}
float GarfrostMultiplier::GetValue(Action* action)
{
Unit* boss = AI_VALUE2(Unit*, "find target", "garfrost");
if (!boss)
return 1.0f;
return 1.0f;
}

View File

@@ -0,0 +1,24 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONPOSMULTIPLIERS_H
#define _PLAYERBOT_WOTLKDUNGEONPOSMULTIPLIERS_H
#include "Multiplier.h"
class IckAndKrickMultiplier : public Multiplier
{
public:
IckAndKrickMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ick and krick") {}
public:
virtual float GetValue(Action* action);
};
class GarfrostMultiplier : public Multiplier
{
public:
GarfrostMultiplier(PlayerbotAI* ai) : Multiplier(ai, "garfrost") { }
float GetValue(Action* action) override;
};
#endif

View File

@@ -0,0 +1,17 @@
#include "PitOfSaronStrategy.h"
#include "PitOfSaronMultipliers.h"
void WotlkDungeonPoSStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode("ick and krick",
NextAction::array(0, new NextAction("ick and krick", ACTION_RAID + 5), nullptr)));
triggers.push_back(new TriggerNode("tyrannus",
NextAction::array(0, new NextAction("tyrannus", ACTION_RAID + 5), nullptr)));
}
void WotlkDungeonPoSStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)
{
multipliers.push_back(new IckAndKrickMultiplier(botAI));
//multipliers.push_back(new AttackFragmentMultiplier(botAI));
}

View File

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

View File

@@ -0,0 +1,22 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERCONTEXT_H
#define _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERCONTEXT_H
#include "NamedObjectContext.h"
#include "AiObjectContext.h"
#include "PitOfSaronTriggers.h"
class WotlkDungeonPoSTriggerContext : public NamedObjectContext<Trigger>
{
public:
WotlkDungeonPoSTriggerContext()
{
creators["ick and krick"] = &WotlkDungeonPoSTriggerContext::ick_and_krick;
creators["tyrannus"] = &WotlkDungeonPoSTriggerContext::tyrannus;
}
private:
static Trigger* ick_and_krick(PlayerbotAI* ai) { return new IckAndKrickTrigger(ai); }
static Trigger* tyrannus(PlayerbotAI* ai) { return new TyrannusTrigger(ai); }
};
#endif

View File

@@ -0,0 +1,22 @@
#include "Playerbots.h"
#include "PitOfSaronTriggers.h"
#include "AiObject.h"
#include "AiObjectContext.h"
bool IckAndKrickTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "Ick");
if (!boss)
return false;
return true;
}
bool TyrannusTrigger::IsActive()
{
Unit* boss = AI_VALUE2(Unit*, "find target", "scourgelord tyrannus");
if (!boss)
return false;
return true;
}

View File

@@ -0,0 +1,48 @@
#ifndef _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERS_H
#define _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERS_H
#include "Trigger.h"
#include "PlayerbotAIConfig.h"
#include "GenericTriggers.h"
#include "DungeonStrategyUtils.h"
enum PitOfSaronIDs
{
//NPCs
NPC_GARFROST = 36494,
NPC_KRICK = 36477,
NPC_ICK = 36476,
NPC_TYRANNUS = 36658,
NPC_RIMEFANG = 36661,
//Spells
SPELL_PURSUIT = 68987,
SPELL_POISON_NOVA_POS = 68989,
SPELL_POISON_NOVA_POS_HC = 70434,
SPELL_EXPLOSIVE_BARRAGE_KRICK = 69012,
SPELL_EXPLOSIVE_BARRAGE_ICK = 69263,
SPELL_EXPLOSIVE_BARRAGE_SUMMON = 69015,
SPELL_EXPLODING_ORB_VISUAL = 69017,
SPELL_MARK_OF_RIMEFANG = 69275,
RIMEFANG_SPELL_HOARFROST = 69246,
RIMEFANG_SPELL_HOARFROST_HC = 69245,
RIMEFANG_SPELL_HOARFROST_HC2 = 69645,
};
class IckAndKrickTrigger : public Trigger
{
public:
IckAndKrickTrigger(PlayerbotAI* ai) : Trigger(ai, "ick and krick") {}
bool IsActive() override;
};
class TyrannusTrigger : public Trigger
{
public:
TyrannusTrigger(PlayerbotAI* ai) : Trigger(ai, "tyrannus") {}
bool IsActive() override;
};
#endif