mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.cpp
Crow 7eff13a9f8
Implement remaining racials* + minor modifications to racials strategy (#2456)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->

*The title is a lie because I did not implement Cannibalize. I don't
think that one is ever going to be worth the processing cost to find
nearby corpses.

Anyway:
- Implemented Stoneform and PoisonDiseaseBleedTrigger.
- Implemented Escape Artist and MovementImpairedTrigger. The trigger
excludes Stealth and Prowl, like with Hand of Freedom. I realize Gnomes
cannot normally be Druids, but I have left Prowl in, in case somebody
uses a mod to make it happen. I could easily be persuaded to remove the
Prowl exclusion, though.
- Implemented Rogue version of Arcane Torrent, used based on the low
energy trigger.
- Implemented DK version of Arcane Torrent; I don't know shit about DKs,
and it doesn't look like there is any runic-energy-related trigger, so I
just made this a generic boost trigger. That means it's used on cooldown
while the boost strategy is active and the encounter trips the balance
that causes boost trigger to fire. It's not great, but it's better than
not using the ability at all.
- All racials, plus Lifeblood (which is also under the racials
strategy), are gated behind HasSpell() checks. This stops bots from
evaluating racials they don't have and reduces log spam.
- Removed the ActionNodeFactory for racials, which was used only for
Lifeblood with Gift of the Naaru alternative. I split those abilities
into their own TriggerNodes, behind their own spell checks.
- Increased threshold for Lifeblood and Gift of the Naaru use to medium
health instead of low health. These are not strong heals, plus they are
HoTs, so bots may as well get more use out of them. Gift of the Naaru
can be used on allies as well, but I did not implement it (because of
the effort and because I don't think it's worth the processing cost
anyway for such an insignificant heal).
- Removed spell checks from EMFH and WoTF IsPossible() since they are
checked before the trigger is evaluated.
- Removed IsUseful() overrides from EMFH and WoTF since the checks are
already in the triggers. If we should have the checks done twice anyway,
LMK.

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.

## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->

I tested:
- EMFH with Fear
- Escape Artist with Entangling Roots
- Stoneform with Rupture
- Arcane Torrent (Rogue version)

I did not test the DK version of Arcane Torrent. I did not check all
possible conditions for the CC breaks, but it can be done most easily by
self-botting and using the .aura GM command. The racials strategy is a
default combat strategy only so for testing at least you may want to add
nc +racials to make it easier.

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [ ] No, not at all
    - - [x] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)

New triggers always add more processing, but this stuff is
insignificant.

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

Racials is a default combat strategy so new racials/changes to existing
racials modifies default behavior.

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



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-06-12 14:57:49 -07:00

754 lines
20 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "GenericTriggers.h"
#include <string>
#include "GenericBuffUtils.h"
#include "CreatureAI.h"
#include "ItemVisitors.h"
#include "LastSpellCastValue.h"
#include "ObjectGuid.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "PositionValue.h"
#include "SharedDefines.h"
#include "TemporarySummon.h"
#include "ThreatManager.h"
#include "Timer.h"
#include "PlayerbotAI.h"
#include "Player.h"
#include "Corpse.h"
bool LowManaTrigger::IsActive()
{
return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.lowMana;
}
bool MediumManaTrigger::IsActive()
{
return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.mediumMana;
}
bool LowEnergyTrigger::IsActive()
{
return AI_VALUE2(uint8, "energy", "self target") < threshold;
}
bool NoPetTrigger::IsActive()
{
return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() &&
!bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target");
}
bool HasPetTrigger::IsActive()
{
return AI_VALUE(Unit*, "pet target") && !AI_VALUE2(bool, "mounted", "self target");
}
bool PetAttackTrigger::IsActive()
{
Guardian* pet = bot->GetGuardianPet();
if (!pet)
return false;
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack())
return false;
if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat())
return false;
return true;
}
bool HighManaTrigger::IsActive()
{
return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana;
}
bool AlmostFullManaTrigger::IsActive()
{
return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") > 85;
}
bool EnoughManaTrigger::IsActive()
{
return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana;
}
bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; }
bool EnergyAvailable::IsActive() { return AI_VALUE2(uint8, "energy", "self target") >= amount; }
bool ComboPointsAvailableTrigger::IsActive() { return AI_VALUE2(uint8, "combo", "current target") >= amount; }
bool ComboPointsNotFullTrigger::IsActive() { return AI_VALUE2(uint8, "combo", "current target") < amount; }
bool TargetWithComboPointsLowerHealTrigger::IsActive()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target || !target->IsAlive() || !target->IsInWorld())
return false;
return ComboPointsAvailableTrigger::IsActive() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime;
}
bool LoseAggroTrigger::IsActive() { return !AI_VALUE2(bool, "has aggro", "current target"); }
bool HasAggroTrigger::IsActive() { return AI_VALUE2(bool, "has aggro", "current target"); }
bool PanicTrigger::IsActive()
{
return AI_VALUE2(uint8, "health", "self target") < sPlayerbotAIConfig.criticalHealth &&
(!AI_VALUE2(bool, "has mana", "self target") ||
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.lowMana);
}
bool OutNumberedTrigger::IsActive()
{
if (bot->GetMap() && (bot->GetMap()->IsDungeon() || bot->GetMap()->IsRaid()))
return false;
if (bot->GetGroup() && bot->GetGroup()->isRaidGroup())
return false;
int32 botLevel = bot->GetLevel();
uint32 friendPower = 200;
uint32 foePower = 0;
for (auto& attacker : botAI->GetAiObjectContext()->GetValue<GuidVector>("attackers")->Get())
{
Creature* creature = botAI->GetCreature(attacker);
if (!creature)
continue;
int32 dLevel = creature->GetLevel() - botLevel;
if (dLevel > -10)
foePower = std::max(100 + 10 * dLevel, dLevel * 200);
}
if (!foePower)
return false;
for (auto& helper : botAI->GetAiObjectContext()->GetValue<GuidVector>("nearest friendly players")->Get())
{
Unit* player = botAI->GetUnit(helper);
if (!player || player == bot)
continue;
int32 dLevel = player->GetLevel() - botLevel;
if (dLevel > -10 && bot->GetDistance(player) < 10.0f)
friendPower += std::max(200 + 20 * dLevel, dLevel * 200);
}
return friendPower < foePower;
}
bool BuffTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target)
return false;
Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration);
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
return false;
}
Value<Unit*>* BuffOnPartyTrigger::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura", ai::buff::MakeAuraQualifierForBuff(spell));
}
bool BuffOnPartyTrigger::IsActive()
{
Unit* target = GetTarget();
if (ai::buff::ShouldDeferPartyBuffEvaluationForRecentLogin(bot, target, spell))
return false;
return BuffTrigger::IsActive();
}
bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); }
Value<Unit*>* DebuffOnAttackerTrigger::GetTargetValue()
{
return context->GetValue<Unit*>("attacker without aura", spell);
}
Value<Unit*>* DebuffOnMeleeAttackerTrigger::GetTargetValue()
{
return context->GetValue<Unit*>("melee attacker without aura", spell);
}
bool NoAttackersTrigger::IsActive()
{
return !AI_VALUE(Unit*, "current target") && AI_VALUE(uint8, "my attacker count") > 0;
}
bool InvalidTargetTrigger::IsActive() { return AI_VALUE2(bool, "invalid target", "current target"); }
bool NoTargetTrigger::IsActive() { return !AI_VALUE(Unit*, "current target"); }
bool MyAttackerCountTrigger::IsActive()
{
return AI_VALUE2(bool, "combat", "self target") && AI_VALUE(uint8, "my attacker count") >= amount;
}
bool MediumThreatTrigger::IsActive()
{
if (!AI_VALUE(Unit*, "main tank"))
return false;
return MyAttackerCountTrigger::IsActive();
}
bool LowTankThreatTrigger::IsActive()
{
Unit* mainTank = AI_VALUE(Unit*, "main tank");
if (!mainTank)
return false;
Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target)
return false;
ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mainTank);
return tankThreat == 0.0f || threat > tankThreat * 0.5f;
}
bool AoeTrigger::IsActive()
{
Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target)
return false;
GuidVector attackers = context->GetValue<GuidVector>("attackers")->Get();
int attackers_count = 0;
for (ObjectGuid const guid : attackers)
{
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive())
continue;
if (unit->GetDistance(current_target->GetPosition()) <= range)
attackers_count++;
}
return attackers_count >= amount;
}
bool NoFoodTrigger::IsActive()
{
bool isRandomBot = sRandomPlayerbotMgr.IsRandomBot(bot);
if (isRandomBot && botAI->HasCheat(BotCheatMask::food))
return false;
return AI_VALUE2(std::vector<Item*>, "inventory items", "conjured food").empty();
}
bool NoDrinkTrigger::IsActive()
{
bool isRandomBot = sRandomPlayerbotMgr.IsRandomBot(bot);
if (isRandomBot && botAI->HasCheat(BotCheatMask::food))
return false;
return AI_VALUE2(std::vector<Item*>, "inventory items", "conjured water").empty();
}
bool TargetInSightTrigger::IsActive() { return AI_VALUE(Unit*, "grind target"); }
bool DebuffTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
return false;
return BuffTrigger::IsActive() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
}
bool DebuffOnBossTrigger::IsActive()
{
if (!DebuffTrigger::IsActive())
return false;
Creature* creature = GetTarget()->ToCreature();
return creature && (creature->IsDungeonBoss() || creature->isWorldBoss());
}
bool SpellTrigger::IsActive() { return GetTarget(); }
bool SpellCanBeCastTrigger::IsActive()
{
Unit* target = GetTarget();
return target && botAI->CanCastSpell(spell, target);
}
bool SpellNoCooldownTrigger::IsActive()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", name);
if (!spellId)
return false;
return !bot->HasSpellCooldown(spellId);
}
bool SpellCooldownTrigger::IsActive()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", name);
if (!spellId)
return false;
return bot->HasSpellCooldown(spellId);
}
RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability)
: Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) {}
bool RandomTrigger::IsActive()
{
if (getMSTime() - lastCheck < sPlayerbotAIConfig.repeatDelay)
return false;
lastCheck = getMSTime();
int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier);
if (k < 1)
k = 1;
return (rand() % k) == 0;
}
bool AndTrigger::IsActive() { return ls && rs && ls->IsActive() && rs->IsActive(); }
std::string const AndTrigger::getName()
{
std::string name(ls->getName());
name = name + " and ";
name = name + rs->getName();
return name;
}
bool TwoTriggers::IsActive()
{
if (name1.empty() || name2.empty())
return false;
Trigger* trigger1 = botAI->GetAiObjectContext()->GetTrigger(name1);
Trigger* trigger2 = botAI->GetAiObjectContext()->GetTrigger(name2);
if (!trigger1 || !trigger2)
return false;
return trigger1->IsActive() && trigger2->IsActive();
}
std::string const TwoTriggers::getName()
{
std::string name;
name = name1 + " and " + name2;
return name;
}
bool BoostTrigger::IsActive()
{
if (!BuffTrigger::IsActive())
return false;
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->ToPlayer())
return true;
return AI_VALUE(uint8, "balance") <= balance;
}
bool GenericBoostTrigger::IsActive()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->ToPlayer())
return true;
return AI_VALUE(uint8, "balance") <= balance;
}
bool HealerShouldAttackTrigger::IsActive()
{
if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1)
return true;
if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth)
return false;
if (bot->GetAura(33891)) // Tree of Life
{
LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get();
if (lastSpell.timer + 5 > time(nullptr))
return false;
}
int manaThreshold;
int balance = AI_VALUE(uint8, "balance");
if (balance <= 50)
manaThreshold = 85;
else if (balance <= 100)
manaThreshold = sPlayerbotAIConfig.highMana;
else
manaThreshold = sPlayerbotAIConfig.mediumMana;
if (AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") < manaThreshold)
return false;
return true;
}
bool ItemCountTrigger::IsActive() { return AI_VALUE2(uint32, "item count", item) < count; }
bool InterruptSpellTrigger::IsActive()
{
return SpellTrigger::IsActive() && botAI->IsInterruptableSpellCasting(GetTarget(), getName());
}
bool DeflectSpellTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID())
return false;
uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get();
if (!spellid)
return false;
SpellInfo const* deflectSpell = sSpellMgr->GetSpellInfo(spellid);
if (!deflectSpell)
return false;
// warrior deflects all
if (spell == "spell reflection")
return true;
// human priest feedback
if (spell == "feedback")
return true;
SpellSchoolMask deflectSchool = SpellSchoolMask(deflectSpell->Effects[EFFECT_0].MiscValue);
SpellSchoolMask attackSchool = SPELL_SCHOOL_MASK_NONE;
if (Spell* spell = target->GetCurrentSpell(CURRENT_GENERIC_SPELL))
{
if (SpellInfo const* tarSpellInfo = spell->GetSpellInfo())
{
attackSchool = tarSpellInfo->GetSchoolMask();
if (deflectSchool == attackSchool)
return true;
}
}
return false;
}
bool AttackerCountTrigger::IsActive() { return AI_VALUE(uint8, "attacker count") >= amount; }
bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); }
bool LossOfControlTrigger::IsActive()
{
return bot->HasAuraType(SPELL_AURA_MOD_STUN) ||
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM);
}
bool FearCharmSleepTrigger::IsActive()
{
return bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM) ||
bot->HasAuraType(SPELL_AURA_AOE_CHARM) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP);
}
bool FearSleepSapTrigger::IsActive()
{
return bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED);
}
bool PoisonDiseaseBleedTrigger::IsActive()
{
return botAI->HasAuraToDispel(bot, DISPEL_POISON) ||
botAI->HasAuraToDispel(bot, DISPEL_DISEASE) ||
bot->HasAuraWithMechanic(1 << MECHANIC_BLEED);
}
bool MovementImpairedTrigger::IsActive()
{
return botAI->IsMovementImpaired(bot) &&
!botAI->HasAnyAuraOf(bot, "stealth", "prowl", nullptr);
}
bool HasAuraStackTrigger::IsActive()
{
return botAI->GetAura(getName(), GetTarget(), false, true, stack);
}
bool TimerTrigger::IsActive()
{
time_t now = time(nullptr);
if (now != lastCheck)
{
lastCheck = now;
return true;
}
return false;
}
bool TimerBGTrigger::IsActive()
{
time_t now = time(nullptr);
if (now - lastCheck >= 60)
{
lastCheck = now;
return true;
}
return false;
}
bool HasNoAuraTrigger::IsActive() { return !botAI->HasAura(getName(), GetTarget()); }
bool TankAssistTrigger::IsActive()
{
if (!AI_VALUE(uint8, "attacker count"))
return false;
Unit* currentTarget = AI_VALUE(Unit*, "current target");
if (!currentTarget)
return true;
Unit* tankTarget = AI_VALUE(Unit*, "tank target");
if (!tankTarget || currentTarget == tankTarget)
return false;
return AI_VALUE2(bool, "has aggro", "current target");
}
bool IsBehindTargetTrigger::IsActive()
{
Unit* target = AI_VALUE(Unit*, "current target");
return target && AI_VALUE2(bool, "behind", "current target");
}
bool IsNotBehindTargetTrigger::IsActive()
{
if (botAI->HasStrategy("stay", botAI->GetState()))
return false;
Unit* target = AI_VALUE(Unit*, "current target");
return target && !AI_VALUE2(bool, "behind", "current target");
}
bool IsNotFacingTargetTrigger::IsActive()
{
if (botAI->HasStrategy("stay", botAI->GetState()))
return false;
return !AI_VALUE2(bool, "facing", "current target");
}
bool HasCcTargetTrigger::IsActive()
{
return AI_VALUE2(Unit*, "cc target", getName()) && !AI_VALUE2(Unit*, "current cc target", getName());
}
bool NoMovementTrigger::IsActive() { return !AI_VALUE2(bool, "moving", "self target"); }
bool NoPossibleTargetsTrigger::IsActive()
{
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
return !targets.size();
}
bool PossibleAddsTrigger::IsActive()
{
return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target");
}
bool NotDpsTargetActiveTrigger::IsActive()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->IsAlive())
{
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
if (target == enemy)
return false;
}
Unit* dps = AI_VALUE(Unit*, "dps target");
return dps && target != dps;
}
bool NotDpsAoeTargetActiveTrigger::IsActive()
{
Unit* dps = AI_VALUE(Unit*, "dps aoe target");
Unit* target = AI_VALUE(Unit*, "current target");
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
if (target && target == enemy && target->IsAlive())
return false;
return dps && target != dps;
}
bool IsSwimmingTrigger::IsActive() { return AI_VALUE2(bool, "swimming", "self target"); }
bool HasNearestAddsTrigger::IsActive()
{
GuidVector targets = AI_VALUE(GuidVector, "nearest adds");
return targets.size();
}
bool HasItemForSpellTrigger::IsActive()
{
std::string const spell = getName();
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
return spellId && AI_VALUE2(Item*, "item for spell", spellId);
}
bool TargetChangedTrigger::IsActive()
{
Unit* oldTarget = context->GetValue<Unit*>("old target")->Get();
Unit* target = context->GetValue<Unit*>("current target")->Get();
return target && oldTarget != target;
}
Value<Unit*>* InterruptEnemyHealerTrigger::GetTargetValue()
{
return context->GetValue<Unit*>("enemy healer target", spell);
}
bool RandomBotUpdateTrigger::IsActive()
{
return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update");
}
bool NoNonBotPlayersAroundTrigger::IsActive()
{
return !botAI->HasPlayerNearby();
/*if (!bot->InBattleground())
return AI_VALUE(GuidVector, "nearest non bot players").empty();
return false;
*/
}
bool NewPlayerNearbyTrigger::IsActive() { return AI_VALUE(ObjectGuid, "new player nearby"); }
bool CollisionTrigger::IsActive() { return AI_VALUE2(bool, "collision", "self target"); }
bool ReturnToStayPositionTrigger::IsActive()
{
PositionInfo stayPosition = AI_VALUE(PositionMap&, "position")["stay"];
if (stayPosition.isSet())
{
const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z);
return distance > sPlayerbotAIConfig.followDistance;
}
return false;
}
bool GiveItemTrigger::IsActive()
{
return AI_VALUE2(Unit*, "party member without item", item) && AI_VALUE2(uint32, "item count", item);
}
bool GiveFoodTrigger::IsActive()
{
return AI_VALUE(Unit*, "party member without food") && AI_VALUE2(uint32, "item count", item);
}
bool GiveWaterTrigger::IsActive()
{
return AI_VALUE(Unit*, "party member without water") && AI_VALUE2(uint32, "item count", item);
}
Value<Unit*>* SnareTargetTrigger::GetTargetValue() { return context->GetValue<Unit*>("snare target", spell); }
bool StayTimeTrigger::IsActive()
{
time_t stayTime = AI_VALUE(time_t, "stay time");
time_t now = time(nullptr);
return delay && stayTime && now > stayTime + 2 * delay / 1000;
}
bool IsMountedTrigger::IsActive() { return AI_VALUE2(bool, "mounted", "self target"); }
bool CorpseNearTrigger::IsActive()
{
return bot->GetCorpse() && bot->GetCorpse()->IsWithinDistInMap(bot, CORPSE_RECLAIM_RADIUS, true);
}
bool IsFallingTrigger::IsActive() { return bot->HasUnitMovementFlag(MOVEMENTFLAG_FALLING); }
bool IsFallingFarTrigger::IsActive() { return bot->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR); }
bool HasAreaDebuffTrigger::IsActive() { return AI_VALUE2(bool, "has area debuff", "self target"); }
Value<Unit*>* BuffOnMainTankTrigger::GetTargetValue() { return context->GetValue<Unit*>("main tank", spell); }
bool AmmoCountTrigger::IsActive()
{
if (bot->GetUInt32Value(PLAYER_AMMO_ID) != 0)
return ItemCountTrigger::IsActive(); // Ammo already equipped
if (botAI->FindAmmo())
return true; // Found ammo in inventory but not equipped
return ItemCountTrigger::IsActive();
}
bool NewPetTrigger::IsActive()
{
ObjectGuid currentPetGuid = ObjectGuid::Empty;
if (Pet* pet = bot->GetPet())
currentPetGuid = pet->GetGUID();
else if (Guardian* guardian = bot->GetGuardianPet())
currentPetGuid = guardian->GetGUID();
if (currentPetGuid != lastPetGuid)
{
triggered = false;
lastPetGuid = currentPetGuid;
}
if (currentPetGuid != ObjectGuid::Empty && !triggered)
{
triggered = true;
return true;
}
return false;
}