Compare commits

..

53 Commits

Author SHA1 Message Date
bash
400c563e3d refactor(Core/Movement): Drop redundant bot filter setters at PathGenerator sites 2026-05-29 20:04:39 +02:00
bash
7d26a5b81e refactor(Core/Movement): Rename SetAreaCost calls to SetNavTerrainCost 2026-05-29 15:45:48 +02:00
bash
80ea99c2f9 fix(Core/Movement): Apply bot filter setters at all PathGenerator construction sites 2026-05-29 15:45:48 +02:00
bash
a352905de2 fix(Core/Travel): Apply NAV_WATER cost bias on regen PathGenerator 2026-05-29 15:45:48 +02:00
bash
68e876a9e2 fix(Core/Travel): Exclude NAV_GROUND_STEEP on regen PathGenerator 2026-05-29 15:45:47 +02:00
bash
15aa83f340 fix(Core/Travel): Hoist portal/transport cheat above 2-point reject 2026-05-29 15:45:47 +02:00
bash
a625b0522a fix(Core/Travel): Match cmangos buildPath stitching, drop 75y guard 2026-05-29 15:45:47 +02:00
bash
e3eb060106 fix(Core/Travel): Preserve walk paths from taxi-path overwrite 2026-05-29 15:45:47 +02:00
bash
c6c3079f26 chore(Core/Travel): Warn admins to shutdown after generatenode 2026-05-29 15:45:47 +02:00
bash
41c3c517e5 fix(Core/Travel): Skip 5y dedup when loading nodes from DB 2026-05-29 15:45:47 +02:00
bash
461253b39e chore(DB/Travel): Temporarily disable Aldrassil ramp anchors 2026-05-29 15:45:47 +02:00
bash
cce4b19cb2 fix(Core/Travel): Drop 2-point check, keep last-segment teleport guard 2026-05-29 15:45:47 +02:00
bash
f94e4087aa fix(Core/Travel): Reject paths with >75y final-segment teleport jumps 2026-05-29 15:45:47 +02:00
bash
8cfa286a17 fix(Core/Travel): Reject 2-point BuildShortcut paths between non-adjacent nodes 2026-05-29 15:45:47 +02:00
bash
3c8659a6e4 chore(Core/Travel): Bump 2-point shortcut threshold to 75y 2026-05-29 15:45:47 +02:00
bash
e8b4461d33 fix(Core/Travel): Reject 2-point BuildShortcut teleports in chained probe 2026-05-29 15:45:47 +02:00
bash
c04ee595de Revert non-progress chained-probe detection (broke valid paths) 2026-05-29 15:45:47 +02:00
bash
da0377766a fix(Core/Travel): Loosen chained-probe non-progress threshold 2026-05-29 15:45:47 +02:00
bash
11f1eda3e0 fix(Core/Travel): Bail chained probe on non-progress oscillation 2026-05-29 15:45:47 +02:00
bash
8bc988d731 fix(Core/Travel): Chunk all saveNodeStore phases (deletes, nodes, links) 2026-05-29 15:45:47 +02:00
bash
f0ec70c3ea fix(Core/Travel): Chunk saveNodeStore path inserts to avoid mega-tx 2026-05-29 15:45:47 +02:00
bash
795384d1f8 feat(DB/Travel): Add Aldrassil ramp travelnode anchors 2026-05-29 15:45:47 +02:00
bash
d841b21250 chore(Core/Debug): Compact debug-move whisper format 2026-05-29 15:45:47 +02:00
bash
d4699aff6f feat(Core/Travel): Sparse-segment clip in LaunchWalkSpline 2026-05-29 15:45:47 +02:00
bash
56d69bf075 feat(Core/RPG): Prefix-trim and sparse-segment clip on path dispatch 2026-05-29 15:45:47 +02:00
bash
c5ab22345e feat(Core/RPG): Port cmangos 8-angle LOS+navmesh-snap to MoveWorldObjectTo 2026-05-29 15:45:47 +02:00
bash
af82b4c296 chore(Core/RPG): Loosen Z-mismatch threshold from 5y to 10y 2026-05-29 15:45:47 +02:00
bash
2cc8e46571 fix(Core/RPG): Reject mmap paths whose endpoint Z misses dest 2026-05-29 15:45:47 +02:00
bash
2ecbd8d48e fix(Core/RPG): Reject mmap paths that LOS-fail any segment 2026-05-29 15:45:47 +02:00
bash
a9654d4ff3 feat(Core/RPG): Switch POI when current cluster is empty 2026-05-29 15:45:47 +02:00
bash
ce9406d401 fix(Core/RPG): Stop next to quest objects instead of on top of them 2026-05-29 15:45:47 +02:00
bash
f82904db87 chore: Drop bot movement console logs 2026-05-29 15:45:47 +02:00
bash
52b7120453 chore: Tighten comments in travel and movement code 2026-05-29 15:45:46 +02:00
bash
afab6fd814 chore(Core/Travel): Drop cmangos reference in RefineWalkPoints comment 2026-05-29 15:45:46 +02:00
bash
5ccf9c9799 fix(Core/RPG): LOS check on MoveRandomNear samples to avoid tree tunneling 2026-05-29 15:45:46 +02:00
bash
a322e54b0e Revert "fix(Core/Travel): LOS check before trusting raw cmangos waypoints" 2026-05-29 15:45:46 +02:00
bash
bdd386714b fix(Core/Travel): LOS gate on empty-probe single-waypoint fallback 2026-05-29 15:45:46 +02:00
bash
e3ec6d01c6 fix(Core/Travel): LOS check before trusting raw cmangos waypoints 2026-05-29 15:45:46 +02:00
bash
8776868ae5 chore(Core/Travel): Revert travelnode threshold to 50y 2026-05-29 15:45:46 +02:00
bash
424df402e6 chore(Core/Travel): Bump travelnode threshold to 75y 2026-05-29 15:45:46 +02:00
bash
33a4e4b4b2 fix(Core/Travel): Trust travelnode waypoints when AC mmap rejects segments 2026-05-29 15:45:46 +02:00
bash
5c11a831b9 feat(Core/Travel): Hardcode 50y travelnode threshold 2026-05-29 15:45:46 +02:00
bash
7de2d9ce3e core filter isnt working yet 2026-05-29 15:45:46 +02:00
bash
f420e36599 refactor(Core/Travel): Drop redundant NAV_GROUND_STEEP excludes (core handles via IsBot) 2026-05-29 15:45:46 +02:00
bash
955f61b1ff fix(Core/Travel): Exclude NAV_GROUND_STEEP at all bot PathGenerator sites 2026-05-29 15:45:46 +02:00
bash
2e5fcd08bb feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos 2026-05-29 15:45:46 +02:00
bash
749670fc30 feat(Core/Travel): Cap bots at 50° via NAV_GROUND_STEEP exclude 2026-05-29 15:45:46 +02:00
bash
4a2ead82f9 feat(Core/Debug): Trace movement entry points and visualize travel nodes 2026-05-29 15:45:46 +02:00
bash
e5dacf8bed feat(Core/RPG): MoveFarTo flow, quest-pursuit at POI, MoveRandomNear retries 2026-05-29 15:45:46 +02:00
bash
c7175d67c2 feat(Core/Travel): Travel-node graph routing for long-distance pathing 2026-05-29 15:45:46 +02:00
bash
75493b5f89 feat(Core/Loot): Quest GO loot, bag-make-room, item-pursuit 2026-05-29 15:45:46 +02:00
bash
f5745bd923 chore(Tools): Add mmap/vmap client-data extraction script 2026-05-29 15:45:45 +02:00
bash
018d5e5933 feat(DB/Travel): Import cmangos travel-node graph 2026-05-29 15:45:45 +02:00
48 changed files with 1405 additions and 2762 deletions

View File

@ -22,6 +22,7 @@
# THRESHOLDS
# QUESTS
# COMBAT
# GREATER BUFFS STRATEGIES
# CHEATS
# SPELLS
# FLIGHTPATH
@ -473,23 +474,6 @@ AiPlayerbot.AutoSaveMana = 1
# Default: 60 (60%)
AiPlayerbot.SaveManaThreshold = 60
# Enable Paladin bots to use greater blessings, with the blessing used being based on the
# number of Paladins in the raid/group and the spec of the recipient. Priorities for each
# spec are hardcoded in GreaterBlessingActions.h.
# 0 = disabled
# 1 = enabled in raid groups only
# 2 = enabled in all groups
# Default: 1 (raid only)
AiPlayerbot.AutoGreaterBlessings = 1
# Enable bots to use group reagent buffs: Gift of the Wild, Arcane Brilliance,
# Prayer of Fortitude, Prayer of Spirit, and Prayer of Shadow Protection.
# 0 = disabled
# 1 = enabled in raid groups only
# 2 = enabled in all groups
# Default: 2 (all groups)
AiPlayerbot.AutoPartyBuffs = 2
# Bots can flee from enemies
AiPlayerbot.FleeingEnabled = 1
@ -498,6 +482,24 @@ AiPlayerbot.FleeingEnabled = 1
#
####################################################################################################
####################################################################################################
# GREATER BUFFS STRATEGIES
#
#
# Min group size to use Greater buffs (Paladin, Mage, Druid)
# Default: 3
AiPlayerbot.MinBotsForGreaterBuff = 3
# Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff
# Default: 30
AiPlayerbot.RPWarningCooldown = 30
#
#
#
####################################################################################################
####################################################################################################
# CHEATS
#

View File

@ -95,8 +95,9 @@ bool FollowAction::Execute(Event /*event*/)
bool const movingAllowed = IsMovingAllowed();
bool const dupMove = IsDuplicateMove(destX, destY, destZ);
bool const waiting = IsWaitingForLastMove(priority);
if (movingAllowed && !dupMove)
if (movingAllowed && !dupMove && !waiting)
{
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);

View File

@ -24,7 +24,9 @@ using ai::buff::MakeAuraQualifierForBuff;
using ai::spell::HasSpellOrCategoryCooldown;
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell)
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) {}
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell)
{
}
bool CastSpellAction::Execute(Event /*event*/)
{
@ -51,12 +53,18 @@ bool CastSpellAction::Execute(Event /*event*/)
wstrToLower(wnamepart);
if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
if (!Utf8FitTo(spell, wnamepart))
continue;
if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue;
uint32 itemId = spellInfo->Effects[0].ItemType;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK)
if (!proto)
continue;
if (bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue;
if (spellInfo->Id > castId)
@ -84,7 +92,10 @@ bool CastSpellAction::isUseful()
}
Unit* spellTarget = GetTarget();
if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
if (!spellTarget)
return false;
if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
return false;
// float combatReach = bot->GetCombatReach() + target->GetCombatReach();
@ -132,7 +143,10 @@ CastMeleeSpellAction::CastMeleeSpellAction(
bool CastMeleeSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !bot->IsWithinMeleeRange(target))
if (!target)
return false;
if (!bot->IsWithinMeleeRange(target))
return false;
return CastSpellAction::isUseful();
@ -148,7 +162,10 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(
bool CastMeleeDebuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !bot->IsWithinMeleeRange(target))
if (!target)
return false;
if (!bot->IsWithinMeleeRange(target))
return false;
return CastDebuffSpellAction::isUseful();
@ -158,55 +175,14 @@ bool CastAuraSpellAction::isUseful()
{
if (!GetTarget() || !CastSpellAction::isUseful())
return false;
Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration);
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
if (!aura)
return true;
return false;
}
bool CastBuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !CastSpellAction::isUseful())
return false;
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
return !aura || (beforeDuration && aura->GetDuration() < beforeDuration);
}
bool CastBuffSpellAction::Execute(Event /*event*/)
{
return botAI->CastSpell(spell, GetTarget());
}
bool GroupBuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !CastSpellAction::isUseful())
return false;
if (ai::buff::IsGroupVariantEnabled(bot, spell))
{
std::string const groupVariant = ai::buff::GroupVariantFor(spell);
if (!groupVariant.empty() && botAI->HasAura(groupVariant, target, false, isOwner, -1, checkDuration))
return false;
}
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
if (beforeDuration && aura->GetDuration() < beforeDuration)
return true;
return false;
}
bool GroupBuffSpellAction::Execute(Event /*event*/)
{
std::string const castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, spell);
return botAI->CastSpell(castName, GetTarget());
}
CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(
PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
@ -272,16 +248,25 @@ Value<Unit*>* CurePartyMemberAction::GetTargetValue()
return context->GetValue<Unit*>("party member to dispel", dispelType);
}
// Make Bots Paladin, druid, mage use the greater buff rank spell
// TODO Priest doen't verify il he have components
Value<Unit*>* BuffOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member without aura", spell);
}
Value<Unit*>* GroupBuffOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
}
bool BuffOnPartyAction::Execute(Event /*event*/)
{
std::string castName = spell; // default = mono
auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(
bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP);
return botAI->CastSpell(castName, GetTarget());
}
// End greater buff fix
CastShootAction::CastShootAction(
PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
{
@ -380,32 +365,50 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
bool CastEveryManForHimselfAction::isPossible()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
if (!spellId)
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
}
bool CastEveryManForHimselfAction::isUseful()
{
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))
&& CastSpellAction::isUseful();
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM))
&& CastSpellAction::isUseful();
}
bool CastWillOfTheForsakenAction::isPossible()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
if (!spellId)
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
}
bool CastWillOfTheForsakenAction::isUseful()
{
return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM) ||
bot->HasAuraType(SPELL_AURA_AOE_CHARM) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP))
&& CastSpellAction::isUseful();
bot->HasAuraType(SPELL_AURA_MOD_CHARM) ||
bot->HasAuraType(SPELL_AURA_AOE_CHARM) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP))
&& CastSpellAction::isUseful();
}
bool UseTrinketAction::Execute(Event /*event*/)
@ -424,7 +427,10 @@ bool UseTrinketAction::Execute(Event /*event*/)
bool UseTrinketAction::UseTrinket(Item* item)
{
if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true))
if (bot->CanUseItem(item) != EQUIP_ERR_OK)
return false;
if (bot->IsNonMeleeSpellCast(true))
return false;
uint8 bagIndex = item->GetBagSlot();
@ -471,13 +477,14 @@ bool UseTrinketAction::UseTrinket(Item* item)
if (spellProcFlag != 0) return false;
if (!botAI->CanCastSpell(spellId, bot, false))
{
return false;
}
break;
}
}
if (!spellId)
return false;
WorldPacket packet(CMSG_USE_ITEM);
packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags;
@ -493,8 +500,9 @@ bool CastDebuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false;
}
return CastAuraSpellAction::isUseful() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
}

View File

@ -69,7 +69,9 @@ class CastDebuffSpellAction : public CastAuraSpellAction
{
public:
CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f)
: CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) {}
: CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime)
{
}
bool isUseful() override;
private:
@ -88,7 +90,9 @@ class CastDebuffSpellOnAttackerAction : public CastDebuffSpellAction
public:
CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
float needLifeTime = 8.0f)
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {}
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime)
{
}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; }
@ -100,7 +104,9 @@ class CastDebuffSpellOnMeleeAttackerAction : public CastDebuffSpellAction
public:
CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
float needLifeTime = 8.0f)
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {}
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime)
{
}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; }
@ -113,19 +119,6 @@ public:
CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0);
std::string const GetTargetName() override { return "self target"; }
bool isUseful() override;
bool Execute(Event event) override;
};
class GroupBuffSpellAction : public CastBuffSpellAction
{
public:
GroupBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false,
uint32 beforeDuration = 0)
: CastBuffSpellAction(botAI, spell, checkIsOwner, beforeDuration) {}
bool isUseful() override;
bool Execute(Event event) override;
};
class CastEnchantItemMainHandAction : public CastSpellAction
@ -158,6 +151,8 @@ public:
// Yunfan: Mana efficiency tell the bot how to save mana. The higher the better.
HealingManaEfficiency manaEfficiency;
uint8 estAmount;
// protected:
};
class CastAoeHealSpellAction : public CastHealingSpellAction
@ -197,7 +192,9 @@ class HealPartyMemberAction : public CastHealingSpellAction, public PartyMemberA
public:
HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f,
HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true)
: CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) {}
: CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell)
{
}
std::string const GetTargetName() override { return "party member to heal"; }
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
@ -222,7 +219,9 @@ class CurePartyMemberAction : public CastSpellAction, public PartyMemberActionNa
{
public:
CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType)
: CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) {}
: CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType)
{
}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
@ -231,25 +230,18 @@ protected:
uint32 dispelType;
};
// Make Bots Paladin, druid, mage use the greater buff rank spell
class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport
{
public:
BuffOnPartyAction(PlayerbotAI* botAI, std::string const spell)
: CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
};
class GroupBuffOnPartyAction : public GroupBuffSpellAction, public PartyMemberActionNameSupport
{
public:
GroupBuffOnPartyAction(PlayerbotAI* botAI, std::string const spell)
: GroupBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
: CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) { }
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
};
// End Fix
class CastShootAction : public CastSpellAction
{
@ -331,7 +323,6 @@ class UseTrinketAction : public Action
public:
UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {}
bool Execute(Event event) override;
protected:
bool UseTrinket(Item* trinket);
};
@ -470,11 +461,12 @@ class BuffOnMainTankAction : public CastBuffSpellAction, public MainTankActionNa
{
public:
BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false)
: CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) {}
: CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell)
{
}
public:
virtual Value<Unit*>* GetTargetValue();
virtual std::string const getName() { return MainTankActionNameSupport::getName(); }
};
#endif

View File

@ -215,6 +215,9 @@ bool MovementAction::JumpTo(uint32 mapId, float x, float y, float z, MovementPri
if (IsDuplicateMove(x, y, z))
return false;
if (IsWaitingForLastMove(priority))
return false;
float speed = bot->GetSpeed(MOVE_RUN);
MotionMaster& mm = *bot->GetMotionMaster();
mm.Clear();
@ -322,6 +325,10 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
{
return false;
}
if (IsWaitingForLastMove(priority))
{
return false;
}
bool generatePath = !bot->IsFlying() && !bot->isSwimming();
bool disableMoveSplinePath =
@ -1046,6 +1053,20 @@ bool MovementAction::IsDuplicateMove(float x, float y, float z)
return true;
}
bool MovementAction::IsWaitingForLastMove(MovementPriority priority)
{
LastMovement& lastMove = *context->GetValue<LastMovement&>("last movement");
if (priority > lastMove.priority)
return false;
// heuristic 5s
if (lastMove.lastdelayTime + lastMove.msTime > getMSTime())
return true;
return false;
}
bool MovementAction::IsMovingAllowed()
{
return botAI->CanMove();
@ -1402,7 +1423,9 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
bool MovementAction::ChaseTo(WorldObject* obj, float distance)
{
if (!IsMovingAllowed())
{
return false;
}
if (obj)
EmitDebugMove("ChaseTo", "chase", obj->GetPositionX(), obj->GetPositionY(), obj->GetPositionZ());
@ -1412,6 +1435,8 @@ bool MovementAction::ChaseTo(WorldObject* obj, float distance)
VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot);
if (!seat || !seat->CanControl())
return false;
// vehicle->GetMotionMaster()->Clear();
vehicle->GetBase()->GetMotionMaster()->MoveChase((Unit*)obj, 30.0f);
return true;
}
@ -1427,34 +1452,10 @@ bool MovementAction::ChaseTo(WorldObject* obj, float distance)
botAI->InterruptSpell();
}
// Try a chained mmap probe first — for targets behind obstacles
// this routes the bot around terrain instead of straight-charging
// into a wall. Falls back to engine MoveChase for short/clear
// chases where target tracking matters more than path routing.
float const targetDist = bot->GetExactDist(obj);
if (targetDist > distance + 3.0f)
{
float const angle = obj->GetAngle(bot);
float x = obj->GetPositionX();
float y = obj->GetPositionY();
float z = obj->GetPositionZ();
obj->GetNearPoint(bot, x, y, z, bot->GetObjectSize(), distance, angle);
PathGenerator path(bot);
path.CalculatePath(x, y, z, false);
PathType type = path.GetPathType();
if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_SHORTCUT)) &&
path.GetPath().size() >= 2)
{
Movement::PointsArray points = path.GetPath();
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
WaitForReach(targetDist - distance);
return true;
}
}
// bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveChase((Unit*)obj, distance);
// TODO shouldnt this use "last movement" value?
WaitForReach(bot->GetExactDist2d(obj) - distance);
return true;
}
@ -3063,6 +3064,51 @@ bool MoveAwayFromPlayerWithDebuffAction::isPossible()
return bot->CanFreeMove();
}
bool MovementAction::CheckSplineProgress(TravelPlan& state)
{
if (!state.splineActive)
return false;
// walkPoints may have been cleared by a map transfer or external reset
// while the spline was still flagged active; bail out safely.
if (state.walkPoints.empty())
{
state.splineActive = false;
return false;
}
if (bot->movespline->Finalized())
{
G3D::Vector3 const& endPt = state.walkPoints.back();
float distToEnd = bot->GetExactDist(endPt.x, endPt.y, endPt.z);
if (distToEnd < 10.0f)
{
state.splineActive = false;
state.walkPoints.clear();
return true; // Arrived
}
// Spline finalized short of target — interrupted (combat/knockback/etc).
// Caller will re-launch.
state.splineActive = false;
return false;
}
// Stuck detection
if (state.splineStartTime &&
GetMSTimeDiffToNow(state.splineStartTime) > state.expectedDuration * 2 + (30 * IN_MILLISECONDS))
{
G3D::Vector3 const& endPt = state.walkPoints.back();
botAI->TeleportTo(WorldLocation(bot->GetMapId(), endPt.x, endPt.y, endPt.z));
state.splineActive = false;
state.walkPoints.clear();
return true;
}
return false; // Still moving
}
bool MovementAction::LaunchWalkSpline(TravelPlan& state)
{
if (state.walkPoints.size() < 2)
@ -3144,10 +3190,14 @@ bool MovementAction::LaunchWalkSpline(TravelPlan& state)
bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN);
state.splineStartTime = getMSTime();
state.splineActive = true;
G3D::Vector3 const& last = state.walkPoints.back();
// Mirror what MoveTo does after dispatching a spline so the
// lastPath cache below picks up the in-flight waypoint chain.
// Update LastMovement so MoveFarTo's spline-active early-out
// knows about this in-flight walk and won't recompute the path
// mid-spline. Mirror what MoveTo does after dispatching a spline.
{
float delay = static_cast<float>(state.expectedDuration);
delay = std::min(delay, static_cast<float>(sPlayerbotAIConfig.maxWaitForMove));
@ -3273,6 +3323,19 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
// an executor-ran-this-tick label here would whisper every tick
// while the plan is active.
// Handle active spline
if (state.splineActive)
{
if (!CheckSplineProgress(state))
{
if (state.splineActive)
return true; // Still moving
else
LaunchWalkSpline(state); // Interrupted, re-launch
}
return true;
}
if (state.stepIdx >= state.steps.size())
{
state.Reset();
@ -3284,29 +3347,89 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
switch (pt.type)
{
case PathNodeType::NODE_PREPATH:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.stepIdx++;
return true;
}
float const botX = bot->GetPositionX();
float const botY = bot->GetPositionY();
float const botZ = bot->GetPositionZ();
// Walk forward through the route while distance keeps shrinking.
// Once it starts growing we're past the closest waypoint — break.
size_t bestIdx = state.stepIdx + 1;
float bestDistSq = FLT_MAX;
for (size_t i = state.stepIdx + 1; i < state.steps.size(); ++i)
{
const PathNodePoint& cand = state.steps[i];
if (cand.type != PathNodeType::NODE_PATH &&
cand.type != PathNodeType::NODE_NODE)
break; // stop at portal/transport/etc — can't walk past
float const dx = cand.point.GetPositionX() - botX;
float const dy = cand.point.GetPositionY() - botY;
float const dz = cand.point.GetPositionZ() - botZ;
float const dSq = dx * dx + dy * dy + dz * dz;
if (dSq >= bestDistSq)
break; // moving away — closest waypoint already found
bestDistSq = dSq;
bestIdx = i;
}
constexpr float ARRIVAL_DIST = 5.0f;
WorldPosition const& target = state.steps[bestIdx].point;
float const distToTarget = bot->GetExactDist(
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
if (distToTarget < ARRIVAL_DIST)
{
state.stepIdx = bestIdx;
return true;
}
// Validate the path before MoveTo. PathGenerator can
// return NORMAL | NOT_USING_PATH when start or end poly
// is invalid (BuildShortcut → 2-point straight line).
// PointMovementGenerator would then dispatch the bot
// straight through any geometry between bot and target.
// The default accept mask (NORMAL | INCOMPLETE) rejects
// NOT_USING_PATH, so abort the plan and let MoveFarTo
// re-derive instead of walking a known-bad shortcut.
PathResult validate = GeneratePath(
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
DEFAULT_PATH_ACCEPT_MASK, false);
if (!validate.reachable)
{
EmitDebugMove("TravelPlan", "prepath-unreachable",
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
state.Reset();
return false;
}
return MoveTo(target.GetMapId(),
target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(),
false, false, false, true /*exact_waypoint*/);
}
case PathNodeType::NODE_PATH:
case PathNodeType::NODE_NODE:
{
// Batch consecutive walkable points (PREPATH, PATH, NODE) into
// one spline. With per-tick re-resolve the plan starts at
// stepIdx=0 each tick, so we must dispatch a real spline (not
// a single-waypoint MoveTo) — otherwise the executor's
// "near closest waypoint" heuristic returns true without
// moving and the bot never advances.
//
// Capped at 20 points per dispatch as a cheap upper bound on
// per-tick work. The engine plays the spline; next tick
// re-resolves from the bot's new position and dispatches the
// next batch.
// Batch consecutive walk points into one spline. Capped at
// 20 points per dispatch as a cheap upper bound on per-tick
// work; stepIdx advances exactly in step with what's
// dispatched, so the next tick picks up from the cutoff.
static constexpr uint32 MAX_SPLINE_POINTS = 20;
state.walkPoints.clear();
while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS)
{
const PathNodePoint& wp = state.steps[state.stepIdx];
if (wp.type != PathNodeType::NODE_PREPATH &&
wp.type != PathNodeType::NODE_PATH &&
wp.type != PathNodeType::NODE_NODE)
break; // stop at portal/transport/etc — can't walk past
if (wp.type != PathNodeType::NODE_PATH && wp.type != PathNodeType::NODE_NODE)
break;
state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(),
wp.point.GetPositionY(), wp.point.GetPositionZ()));
state.stepIdx++;
@ -3365,102 +3488,32 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
return true;
}
case PathNodeType::NODE_AREA_TRIGGER:
case PathNodeType::NODE_PORTAL:
{
// Pair: trigger (pointIdx) + dest (pointIdx+1).
// Bot walks into the area trigger volume; server teleports
// on entry. Bot may need quest/key prereqs to actually cross.
// Pair: source (pointIdx) + dest (pointIdx+1)
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& trigger = state.steps[state.stepIdx];
const PathNodePoint& src = state.steps[state.stepIdx];
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
// Already on destination map — trigger fired, advance.
// Already on destination map?
if (bot->GetMapId() == dst.point.GetMapId())
{
state.stepIdx += 2;
return true;
}
// Walk to the trigger position; collision with the trigger
// volume teleports us.
float dist = bot->GetExactDist(trigger.point.GetPositionX(),
trigger.point.GetPositionY(),
trigger.point.GetPositionZ());
// Walk to portal source
float dist = bot->GetExactDist(src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
if (dist > INTERACTION_DISTANCE)
return MoveTo(trigger.point.GetMapId(),
trigger.point.GetPositionX(),
trigger.point.GetPositionY(),
trigger.point.GetPositionZ());
return MoveTo(src.point.GetMapId(), src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ());
// At trigger but didn't teleport — likely missing quest/key.
// Abort; the do-quest yield-to-grind multiplier or next
// POI pick can reroute.
state.Reset();
return false;
}
case PathNodeType::NODE_STATIC_PORTAL:
{
// Pair: portal-GO position (pointIdx) + dest (pointIdx+1).
// Bot walks within interact range of the portal GameObject
// and sends CMSG_GAMEOBJ_USE to trigger its teleport spell.
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& portal = state.steps[state.stepIdx];
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
if (bot->GetMapId() == dst.point.GetMapId())
{
state.stepIdx += 2;
return true;
}
// Walk to portal GO position
float dist = bot->GetExactDist(portal.point.GetPositionX(),
portal.point.GetPositionY(),
portal.point.GetPositionZ());
if (dist > INTERACTION_DISTANCE)
return MoveTo(portal.point.GetMapId(),
portal.point.GetPositionX(),
portal.point.GetPositionY(),
portal.point.GetPositionZ());
// In range — find the portal GameObject and interact
if (!portal.entry)
{
state.Reset();
return false;
}
if (bot->IsMounted())
bot->Dismount();
botAI->RemoveShapeshift();
GuidVector nearGOs = AI_VALUE(GuidVector, "nearest game objects");
for (ObjectGuid const& guid : nearGOs)
{
GameObject* go = botAI->GetGameObject(guid);
if (!go || go->GetEntry() != portal.entry)
continue;
if (!bot->GetGameObjectIfCanInteractWith(guid, GAMEOBJECT_TYPE_SPELLCASTER))
continue;
WorldPacket packet(CMSG_GAMEOBJ_USE);
packet << guid;
bot->GetSession()->QueuePacket(new WorldPacket(packet));
return true;
}
// GO not found nearby — abort and let next tick try again
// At portal but didn't cross — natural collision missed.
// Abort the plan; stuck-recovery in MoveFarTo will decide
// whether to retry or teleport the bot.
state.Reset();
return false;
}
@ -3584,6 +3637,24 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
return true;
}
case PathNodeType::NODE_TELEPORT:
{
// Teleport-spell node (e.g. mage portals). Not implemented
// — abort the plan instead of silently teleporting the
// bot. The plan executor regards this node as terminal.
state.Reset();
return false;
}
case PathNodeType::NODE_FLYING_MOUNT:
{
// Flying-mount node not implemented — abort. The graph
// generator produces these but their execution is
// server-specific; we treat them as unreachable rather
// than papering over with a teleport.
state.Reset();
return false;
}
default:
{
LOG_ERROR("playerbots",

View File

@ -72,6 +72,7 @@ protected:
void SetNextMovementDelay(float delayMillis);
bool IsMovingAllowed(WorldObject* target);
bool IsDuplicateMove(float x, float y, float z);
bool IsWaitingForLastMove(MovementPriority priority);
bool IsMovingAllowed();
bool Flee(Unit* target);
void ClearIdleState();
@ -103,6 +104,7 @@ protected:
private:
bool LaunchWalkSpline(TravelPlan& state);
bool CheckSplineProgress(TravelPlan& state);
bool MoveToSpline(TravelPlan& state, WorldPosition target);
// Per-segment mmap refinement of a travel-node-graph walk batch.
// The graph stores offline-baked coords whose straight-line

View File

@ -7,7 +7,6 @@
#include <string>
#include "GenericBuffUtils.h"
#include "CreatureAI.h"
#include "ItemVisitors.h"
#include "LastSpellCastValue.h"
@ -42,50 +41,52 @@ bool LowEnergyTrigger::IsActive()
bool NoPetTrigger::IsActive()
{
return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() &&
!bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target");
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");
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;
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;
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;
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; }
@ -100,8 +101,9 @@ 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;
}
@ -162,27 +164,19 @@ bool BuffTrigger::IsActive()
Unit* target = GetTarget();
if (!target)
return false;
if (!SpellTrigger::IsActive())
return false;
Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration);
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
if (!aura)
return true;
if (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();
return context->GetValue<Unit*>("party member without aura", spell);
}
bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); }
@ -215,14 +209,13 @@ 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)
Unit* mt = AI_VALUE(Unit*, "main tank");
if (!mt)
return false;
Unit* current_target = AI_VALUE(Unit*, "current target");
@ -231,7 +224,7 @@ bool LowTankThreatTrigger::IsActive()
ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mainTank);
float tankThreat = mgr.GetThreat(mt);
return tankThreat == 0.0f || threat > tankThreat * 0.5f;
}
@ -239,8 +232,9 @@ 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)
@ -248,9 +242,10 @@ bool AoeTrigger::IsActive()
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive())
continue;
if (unit->GetDistance(current_target->GetPosition()) <= range)
{
attackers_count++;
}
}
return attackers_count >= amount;
}
@ -279,19 +274,20 @@ 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;
}
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());
}
Creature* c = GetTarget()->ToCreature();
return c && ((c->IsDungeonBoss()) || (c->isWorldBoss()));
}
bool SpellTrigger::IsActive() { return GetTarget(); }
@ -321,7 +317,9 @@ bool SpellCooldownTrigger::IsActive()
}
RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability)
: Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) {}
: Trigger(botAI, name), probability(probability), lastCheck(getMSTime())
{
}
bool RandomTrigger::IsActive()
{
@ -332,7 +330,6 @@ bool RandomTrigger::IsActive()
int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier);
if (k < 1)
k = 1;
return (rand() % k) == 0;
}
@ -371,11 +368,9 @@ 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;
}
@ -384,19 +379,20 @@ 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()
{
// nobody can help me
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
// special check for resto druid (dont remove tree of life frequently)
if (bot->GetAura(33891))
{
LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get();
if (lastSpell.timer + 5 > time(nullptr))
@ -405,6 +401,7 @@ bool HealerShouldAttackTrigger::IsActive()
int manaThreshold;
int balance = AI_VALUE(uint8, "balance");
// higher threshold in higher pressure
if (balance <= 50)
manaThreshold = 85;
else if (balance <= 100)
@ -428,7 +425,13 @@ bool InterruptSpellTrigger::IsActive()
bool DeflectSpellTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID())
if (!target)
return false;
if (!target->IsNonMeleeSpellCast(true))
return false;
if (target->GetTarget() != bot->GetGUID())
return false;
uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get();
@ -459,7 +462,6 @@ bool DeflectSpellTrigger::IsActive()
return true;
}
}
return false;
}
@ -493,16 +495,17 @@ bool FearSleepSapTrigger::IsActive()
bool HasAuraStackTrigger::IsActive()
{
return botAI->GetAura(getName(), GetTarget(), false, true, stack);
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);
// sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "HasAuraStackTrigger::IsActive %s %d", getName(), aura ?
// aura->GetStackAmount() : -1);
return aura;
}
bool TimerTrigger::IsActive()
{
time_t now = time(nullptr);
if (now != lastCheck)
if (time(nullptr) != lastCheck)
{
lastCheck = now;
lastCheck = time(nullptr);
return true;
}
@ -549,8 +552,9 @@ bool IsBehindTargetTrigger::IsActive()
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");
}
@ -558,8 +562,9 @@ bool IsNotBehindTargetTrigger::IsActive()
bool IsNotFacingTargetTrigger::IsActive()
{
if (botAI->HasStrategy("stay", botAI->GetState()))
{
return false;
}
return !AI_VALUE2(bool, "facing", "current target");
}
@ -576,14 +581,12 @@ bool NoPossibleTargetsTrigger::IsActive()
return !targets.size();
}
bool PossibleAddsTrigger::IsActive()
{
return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target");
}
bool PossibleAddsTrigger::IsActive() { return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); }
bool NotDpsTargetActiveTrigger::IsActive()
{
Unit* target = AI_VALUE(Unit*, "current target");
// do not switch if enemy target
if (target && target->IsAlive())
{
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
@ -601,6 +604,7 @@ bool NotDpsAoeTargetActiveTrigger::IsActive()
Unit* target = AI_VALUE(Unit*, "current target");
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
// do not switch if enemy target
if (target && target == enemy && target->IsAlive())
return false;
@ -634,10 +638,7 @@ 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 RandomBotUpdateTrigger::IsActive() { return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); }
bool NoNonBotPlayersAroundTrigger::IsActive()
{
@ -717,24 +718,43 @@ bool AmmoCountTrigger::IsActive()
bool NewPetTrigger::IsActive()
{
// Get the bot player object from the AI
Player* bot = botAI->GetBot();
if (!bot)
return false;
// Try to get the current pet; initialize guardian and GUID to null/empty
Pet* pet = bot->GetPet();
Guardian* guardian = nullptr;
ObjectGuid currentPetGuid = ObjectGuid::Empty;
if (Pet* pet = bot->GetPet())
// If bot has a pet, get its GUID
if (pet)
{
currentPetGuid = pet->GetGUID();
else if (Guardian* guardian = bot->GetGuardianPet())
currentPetGuid = guardian->GetGUID();
}
else
{
// If no pet, try to get a guardian pet and its GUID
guardian = bot->GetGuardianPet();
if (guardian)
currentPetGuid = guardian->GetGUID();
}
// If the current pet or guardian GUID has changed (including becoming empty), reset the trigger state
if (currentPetGuid != lastPetGuid)
{
triggered = false;
lastPetGuid = currentPetGuid;
}
// If there's a valid current pet/guardian (non-empty GUID) and we haven't triggered yet, activate trigger
if (currentPetGuid != ObjectGuid::Empty && !triggered)
{
triggered = true;
return true;
}
// Otherwise, do not activate
return false;
}

View File

@ -20,7 +20,9 @@ class StatAvailable : public Trigger
{
public:
StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available")
: Trigger(botAI, name), amount(amount) {}
: Trigger(botAI, name), amount(amount)
{
}
protected:
int32 amount;
@ -116,8 +118,8 @@ public:
class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger
{
public:
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* botAI, int32 combo_point = 5, float lifeTime = 8.0f)
: ComboPointsAvailableTrigger(botAI, combo_point), lifeTime(lifeTime)
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* ai, int32 combo_point = 5, float lifeTime = 8.0f)
: ComboPointsAvailableTrigger(ai, combo_point), lifeTime(lifeTime)
{
}
bool IsActive() override;
@ -194,6 +196,7 @@ public:
bool IsActive() override;
};
// TODO: check other targets
class InterruptSpellTrigger : public SpellTrigger
{
public:
@ -214,7 +217,9 @@ class AttackerCountTrigger : public Trigger
{
public:
AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance)
: Trigger(botAI), amount(amount), distance(distance) {}
: Trigger(botAI), amount(amount), distance(distance)
{
}
bool IsActive() override;
std::string const getName() override { return "attacker count"; }
@ -264,7 +269,9 @@ class AoeTrigger : public AttackerCountTrigger
{
public:
AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f)
: AttackerCountTrigger(botAI, amount), range(range) {}
: AttackerCountTrigger(botAI, amount), range(range)
{
}
bool IsActive() override;
std::string const getName() override { return "aoe"; }
@ -310,8 +317,7 @@ public:
class BuffTrigger : public SpellTrigger
{
public:
BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1,
bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0)
BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0)
: SpellTrigger(botAI, spell, checkInterval)
{
this->checkIsOwner = checkIsOwner;
@ -333,10 +339,11 @@ class BuffOnPartyTrigger : public BuffTrigger
{
public:
BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
: BuffTrigger(botAI, spell, checkInterval) {}
: BuffTrigger(botAI, spell, checkInterval)
{
}
Value<Unit*>* GetTargetValue() override;
bool IsActive() override;
std::string const getName() override { return spell + " on party"; }
};
@ -386,7 +393,9 @@ class DebuffTrigger : public BuffTrigger
public:
DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false,
float needLifeTime = 8.0f, uint32 beforeDuration = 0)
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) {}
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime)
{
}
std::string const GetTargetName() override { return "current target"; }
bool IsActive() override;
@ -399,7 +408,9 @@ class DebuffOnBossTrigger : public DebuffTrigger
{
public:
DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false)
: DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
: DebuffTrigger(botAI, spell, checkInterval, checkIsOwner)
{
}
bool IsActive() override;
};
@ -408,7 +419,9 @@ class DebuffOnAttackerTrigger : public DebuffTrigger
public:
DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
float needLifeTime = 8.0f)
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime)
{
}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; }
@ -419,7 +432,9 @@ class DebuffOnMeleeAttackerTrigger : public DebuffTrigger
public:
DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
float needLifeTime = 8.0f)
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime)
{
}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; }
@ -429,7 +444,9 @@ class BoostTrigger : public BuffTrigger
{
public:
BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f)
: BuffTrigger(botAI, spell, 1), balance(balance) {}
: BuffTrigger(botAI, spell, 1), balance(balance)
{
}
bool IsActive() override;
@ -441,7 +458,9 @@ class GenericBoostTrigger : public Trigger
{
public:
GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f)
: Trigger(botAI, "generic boost", 1), balance(balance) {}
: Trigger(botAI, "generic boost", 1), balance(balance)
{
}
bool IsActive() override;
@ -453,7 +472,9 @@ class HealerShouldAttackTrigger : public Trigger
{
public:
HealerShouldAttackTrigger(PlayerbotAI* botAI)
: Trigger(botAI, "healer should attack", 1) {}
: Trigger(botAI, "healer should attack", 1)
{
}
bool IsActive() override;
};
@ -559,7 +580,7 @@ public:
class HasPetTrigger : public Trigger
{
public:
HasPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "has pet", 5 * 1000) {}
HasPetTrigger(PlayerbotAI* ai) : Trigger(ai, "has pet", 5 * 1000) {}
virtual bool IsActive() override;
};
@ -567,7 +588,7 @@ public:
class PetAttackTrigger : public Trigger
{
public:
PetAttackTrigger(PlayerbotAI* botAI) : Trigger(botAI, "pet attack") {}
PetAttackTrigger(PlayerbotAI* ai) : Trigger(ai, "pet attack") {}
virtual bool IsActive() override;
};
@ -576,7 +597,9 @@ class ItemCountTrigger : public Trigger
{
public:
ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000)
: Trigger(botAI, item, interval), item(item), count(count) {}
: Trigger(botAI, item, interval), item(item), count(count)
{
}
bool IsActive() override;
std::string const getName() override { return "item count"; }
@ -590,7 +613,9 @@ class AmmoCountTrigger : public ItemCountTrigger
{
public:
AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000)
: ItemCountTrigger(botAI, item, count, interval) {}
: ItemCountTrigger(botAI, item, count, interval)
{
}
bool IsActive() override;
};
@ -598,7 +623,9 @@ class HasAuraTrigger : public Trigger
{
public:
HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
: Trigger(botAI, spell, checkInterval) {}
: Trigger(botAI, spell, checkInterval)
{
}
std::string const GetTargetName() override { return "self target"; }
bool IsActive() override;
@ -607,8 +634,10 @@ public:
class HasAuraStackTrigger : public Trigger
{
public:
HasAuraStackTrigger(PlayerbotAI* botAI, std::string spell, int stack, int checkInterval = 1)
: Trigger(botAI, spell, checkInterval), stack(stack) {}
HasAuraStackTrigger(PlayerbotAI* ai, std::string spell, int stack, int checkInterval = 1)
: Trigger(ai, spell, checkInterval), stack(stack)
{
}
std::string const GetTargetName() override { return "self target"; }
bool IsActive() override;
@ -829,7 +858,9 @@ class StayTimeTrigger : public Trigger
{
public:
StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name)
: Trigger(botAI, name, 5 * 1000), delay(delay) {}
: Trigger(botAI, name, 5 * 1000), delay(delay)
{
}
bool IsActive() override;
@ -846,7 +877,7 @@ public:
class ReturnToStayPositionTrigger : public Trigger
{
public:
ReturnToStayPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to stay position", 2) {}
ReturnToStayPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "return to stay position", 2) {}
virtual bool IsActive() override;
};
@ -861,7 +892,9 @@ class GiveItemTrigger : public Trigger
{
public:
GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item)
: Trigger(botAI, name, 2 * 1000), item(item) {}
: Trigger(botAI, name, 2 * 1000), item(item)
{
}
bool IsActive() override;
@ -929,7 +962,9 @@ class BuffOnMainTankTrigger : public BuffTrigger
{
public:
BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1)
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner)
{
}
public:
virtual Value<Unit*>* GetTargetValue();
@ -938,7 +973,7 @@ public:
class SelfResurrectTrigger : public Trigger
{
public:
SelfResurrectTrigger(PlayerbotAI* botAI) : Trigger(botAI, "can self resurrect") {}
SelfResurrectTrigger(PlayerbotAI* ai) : Trigger(ai, "can self resurrect") {}
bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); }
};
@ -946,7 +981,7 @@ public:
class NewPetTrigger : public Trigger
{
public:
NewPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {}
NewPetTrigger(PlayerbotAI* ai) : Trigger(ai, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {}
bool IsActive() override;

View File

@ -11,20 +11,21 @@
bool LootAvailableTrigger::IsActive()
{
// Strategy is non-combat-only — the engine state separation is the
// safety net. Don't gate on hostiles-in-sight: that locked out
// looting in zones with continuous respawns (e.g. cave farms).
// If a new enemy aggros mid-loot the combat engine takes over, loot
// resumes on the next non-combat window.
if (!AI_VALUE(bool, "has available loot"))
return false;
// "stay" strategy is restrictive: only loot if corpse is at our feet.
bool distanceCheck = false;
if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT))
return ServerFacade::instance().IsDistanceLessOrEqualThan(
AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
{
distanceCheck =
ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), CONTACT_DISTANCE);
}
else
{
distanceCheck = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"),
INTERACTION_DISTANCE - 2.0f);
}
return true;
// if loot target if empty, always pass distance check
return AI_VALUE(bool, "has available loot") &&
(distanceCheck || AI_VALUE(GuidVector, "all targets").empty());
}
bool FarFromCurrentLootTrigger::IsActive()

View File

@ -4,89 +4,23 @@
*/
#include "GenericBuffUtils.h"
#include "AiObjectContext.h"
#include "GameTime.h"
#include "Group.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include <map>
#include "Player.h"
#include "Group.h"
#include "SpellMgr.h"
#include "Unit.h"
#include "Chat.h"
#include "PlayerbotAI.h"
#include "ServerFacade.h"
#include "AiObjectContext.h"
#include "Value.h"
#include "Config.h"
#include "PlayerbotTextMgr.h"
namespace ai::buff
{
namespace
{
// Prevents bots from immediately casting already-present buffs upon logging in
constexpr uint32 POST_LOGIN_BUFF_GRACE_MS = 5 * IN_MILLISECONDS;
bool IsWithinPostLoginBuffGrace(Player* player)
{
if (!player)
return false;
return getMSTimeDiff(
player->GetInGameTime(), GameTime::GetGameTimeMS().count()) < POST_LOGIN_BUFF_GRACE_MS;
}
}
static bool HasEnoughSameMapMissingPlayersForGroupVariant(
Player* bot, PlayerbotAI* botAI, std::string const& baseName,
std::string const& groupName, uint32 requiredCount = 3)
{
Group* group = bot->GetGroup();
if (!group)
return false;
uint32 missingCount = 0;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member || !member->IsInWorld() || !member->IsAlive() ||
member->GetMap() != bot->GetMap())
{
continue;
}
if (botAI->HasAura(baseName, member) || botAI->HasAura(groupName, member))
continue;
if (++missingCount >= requiredCount)
return true;
}
return false;
}
static bool IsEligibleGroupForPartyBuffs(Group const* group)
{
if (!group)
return false;
switch (sPlayerbotAIConfig.autoPartyBuffs)
{
case AutoPartyBuffMode::RAID_ONLY:
return group->isRaidGroup();
case AutoPartyBuffMode::GROUP_OR_RAID:
return true;
case AutoPartyBuffMode::DISABLED:
return false;
}
return false;
}
bool IsGroupVariantEnabled(Player* bot, std::string const& name)
{
if (!IsEligibleGroupForPartyBuffs(bot->GetGroup()))
return false;
return !GroupVariantFor(name).empty();
}
std::string MakeAuraQualifierForBuff(std::string const& name)
{
// Paladin
@ -100,89 +34,27 @@ namespace ai::buff
if (name == "arcane intellect") return "arcane intellect,arcane brilliance";
// Priest
if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude";
if (name == "divine spirit") return "divine spirit,prayer of spirit";
if (name == "shadow protection") return "shadow protection,prayer of shadow protection";
return name;
}
std::string GroupVariantFor(std::string const& name)
{
// Paladin
if (name == "blessing of kings") return "greater blessing of kings";
if (name == "blessing of might") return "greater blessing of might";
if (name == "blessing of wisdom") return "greater blessing of wisdom";
if (name == "blessing of sanctuary") return "greater blessing of sanctuary";
// Druid
if (name == "mark of the wild") return "gift of the wild";
// Mage
if (name == "arcane intellect") return "arcane brilliance";
// Priest
if (name == "power word: fortitude") return "prayer of fortitude";
if (name == "divine spirit") return "prayer of spirit";
if (name == "shadow protection") return "prayer of shadow protection";
// Paladin blessings are intentionally not included here because they are
// coordinated by the auto greater blessing system instead.
return std::string();
}
bool NeedsPostLoginBuffGrace(std::string const& name)
{
static char const* const trackedBuffs[] = {
"mark of the wild",
"arcane intellect",
"power word: fortitude",
"prayer of fortitude",
"divine spirit",
"prayer of spirit",
"shadow protection",
"prayer of shadow protection",
"blessing of kings",
"blessing of might",
"blessing of wisdom",
"blessing of sanctuary"
};
for (char const* trackedBuff : trackedBuffs)
{
if (name.find(trackedBuff) != std::string::npos)
return true;
}
return false;
}
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
Player* bot, Unit* target, std::string const& spell)
{
if (!NeedsPostLoginBuffGrace(spell))
return false;
if (IsWithinPostLoginBuffGrace(bot))
return true;
Player* playerTarget = target ? target->ToPlayer() : nullptr;
return IsWithinPostLoginBuffGrace(playerTarget);
}
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot)
{
if (IsWithinPostLoginBuffGrace(bot))
return true;
Group* group = bot->GetGroup();
if (!group)
return false;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member || !member->IsInWorld())
continue;
if (IsWithinPostLoginBuffGrace(member))
return true;
}
return false;
}
bool HasRequiredReagents(Player* bot, uint32 spellId)
{
if (!spellId)
@ -200,33 +72,75 @@ namespace ai::buff
return false;
}
}
// No reagent required
return true;
}
return false;
}
std::string UpgradeToGroupIfAppropriate(
Player* bot, PlayerbotAI* botAI, std::string const& baseName)
Player* bot,
PlayerbotAI* botAI,
std::string const& baseName,
bool announceOnMissing,
std::function<void(std::string const&)> announce)
{
if (!IsGroupVariantEnabled(bot, baseName))
return baseName;
std::string castName = baseName;
Group* g = bot->GetGroup();
if (!g || g->GetMembersCount() < static_cast<uint32>(sPlayerbotAIConfig.minBotsForGreaterBuff))
return castName; // Group too small: stay in solo mode
std::string const groupName = GroupVariantFor(baseName);
if (groupName.empty())
return baseName;
// Prefer singles until at least three living, in-world group members on the bot's map
// are missing both the single-target buff and its group variant.
if (!HasEnoughSameMapMissingPlayersForGroupVariant(bot, botAI, baseName, groupName))
return baseName;
uint32 const groupSpellId = botAI->GetAiObjectContext()
if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty())
{
uint32 const groupVariantSpellId = botAI->GetAiObjectContext()
->GetValue<uint32>("spell id", groupName)->Get();
if (groupSpellId && HasRequiredReagents(bot, groupSpellId))
return groupName;
// We check usefulness on the **basic** buff (not the greater version),
// because "spell cast useful" may return false for the greater variant.
bool const usefulBase = botAI->GetAiObjectContext()
->GetValue<bool>("spell cast useful", baseName)->Get();
return baseName;
if (groupVariantSpellId && HasRequiredReagents(bot, groupVariantSpellId))
{
// Learned + reagents OK -> switch to greater
return groupName;
}
// Missing reagents -> announce if (a) greater is known, (b) base buff is useful,
// (c) announce was requested, (d) a callback is provided.
if (announceOnMissing && groupVariantSpellId && usefulBase && announce)
{
static std::map<std::pair<uint32, std::string>, time_t> s_lastWarn; // par bot & par buff
time_t now = std::time(nullptr);
uint32 botLow = static_cast<uint32>(bot->GetGUID().GetCounter());
time_t& last = s_lastWarn[ std::make_pair(botLow, groupName) ];
if (!last || now - last >= sPlayerbotAIConfig.rpWarningCooldown) // Configurable anti-spam
{
// DB Key choice in regard of the buff
std::string key;
if (groupName.find("greater blessing") != std::string::npos)
key = "rp_missing_reagent_greater_blessing";
else if (groupName == "gift of the wild")
key = "rp_missing_reagent_gift_of_the_wild";
else if (groupName == "arcane brilliance")
key = "rp_missing_reagent_arcane_brilliance";
else
key = "rp_missing_reagent_generic";
// Placeholders
std::map<std::string, std::string> placeholders;
placeholders["%group_spell"] = groupName;
placeholders["%base_spell"] = baseName;
std::string announceText = PlayerbotTextMgr::instance().GetBotTextOrDefault(key,
"Out of components for %group_spell. Using %base_spell!", placeholders);
announce(announceText);
last = now;
}
}
}
return castName;
}
}

View File

@ -6,40 +6,63 @@
#pragma once
#include <string>
#include <functional>
#include "Common.h"
#include "Group.h"
#include "Chat.h"
#include "Language.h"
class Player;
class PlayerbotAI;
class Unit;
namespace ai::buff
{
bool IsGroupVariantEnabled(Player* bot, std::string const& name);
// Build an aura qualifier "single + greater" to avoid double-buffing
std::string MakeAuraQualifierForBuff(std::string const& name);
// Returns the group spell name for a given single-target buff.
// If no group equivalent exists, returns "".
std::string GroupVariantFor(std::string const& name);
bool NeedsPostLoginBuffGrace(std::string const& name);
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
Player* bot,
Unit* target,
std::string const& spell);
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot);
// Checks if the bot has the required reagents to cast a spell (by its spellId).
// Returns false if the spellId is invalid.
bool HasRequiredReagents(Player* bot, uint32 spellId);
// Applies the "switch to group buff" policy if: the bot is in a group of size x+,
// the group variant is known/useful, and reagents are available. Otherwise, returns baseName.
// If announceOnMissing == true and reagents are missing, calls the 'announce' callback
// (if provided) to notify the party/raid.
std::string UpgradeToGroupIfAppropriate(
Player* bot,
PlayerbotAI* botAI,
std::string const& baseName);
std::string const& baseName,
bool announceOnMissing = false,
std::function<void(std::string const&)> announce = {}
);
}
namespace ai::spell
{
bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId);
}
namespace ai::chat {
inline std::function<void(std::string const&)> MakeGroupAnnouncer(Player* me)
{
return [me](std::string const& msg)
{
if (Group* g = me->GetGroup())
{
WorldPacket data;
ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY;
ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str());
g->BroadcastPacket(&data, true, -1, me->GetGUID());
}
else
{
me->Say(msg, LANG_UNIVERSAL);
}
};
}
}

View File

@ -87,16 +87,16 @@ public:
bool isUseful() override;
};
class CastMarkOfTheWildAction : public GroupBuffSpellAction
class CastMarkOfTheWildAction : public CastBuffSpellAction
{
public:
CastMarkOfTheWildAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "mark of the wild") {}
CastMarkOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mark of the wild") {}
};
class CastMarkOfTheWildOnPartyAction : public GroupBuffOnPartyAction
class CastMarkOfTheWildOnPartyAction : public BuffOnPartyAction
{
public:
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "mark of the wild") {}
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "mark of the wild") {}
};
class CastSurvivalInstinctsAction : public CastBuffSpellAction

View File

@ -9,6 +9,11 @@
#include "Playerbots.h"
#include "ServerFacade.h"
bool MarkOfTheWildOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
}
bool MarkOfTheWildTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());

View File

@ -23,13 +23,15 @@ class PlayerbotAI;
class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger
{
public:
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 4 * 2000) {}
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 2 * 2000) {}
bool IsActive() override;
};
class MarkOfTheWildTrigger : public BuffTrigger
{
public:
MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 4 * 2000) {}
MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 2 * 2000) {}
bool IsActive() override;
};

View File

@ -40,16 +40,16 @@ public:
CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {}
};
class CastArcaneIntellectAction : public GroupBuffSpellAction
class CastArcaneIntellectAction : public CastBuffSpellAction
{
public:
CastArcaneIntellectAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "arcane intellect") {}
CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {}
};
class CastArcaneIntellectOnPartyAction : public GroupBuffOnPartyAction
class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction
{
public:
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "arcane intellect") {}
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {}
};
class CastFocusMagicOnPartyAction : public CastSpellAction

View File

@ -31,6 +31,11 @@ bool NoManaGemTrigger::IsActive()
return true;
}
bool ArcaneIntellectOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
}
bool ArcaneIntellectTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());

View File

@ -19,13 +19,14 @@ class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger
{
public:
ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "arcane intellect", 4 * 2000) {}
: BuffOnPartyTrigger(botAI, "arcane intellect", 2 * 2000) {}
bool IsActive() override;
};
class ArcaneIntellectTrigger : public BuffTrigger
{
public:
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 4 * 2000) {}
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 2 * 2000) {}
bool IsActive() override;
};

View File

@ -7,100 +7,24 @@
#include "AiFactory.h"
#include "Event.h"
#include "GenericBuffUtils.h"
#include "PaladinGreaterBlessingAction.h"
#include "PaladinHelper.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "SharedDefines.h"
#include "../../../../../src/server/scripts/Spells/spell_generic.cpp"
#include "Ai/Base/Util/GenericBuffUtils.h"
#include "Group.h"
#include "ObjectAccessor.h"
static bool IsBlessingTargetCandidate(Player* bot, Player* player)
using ai::buff::MakeAuraQualifierForBuff;
// Helper : detect tank role on the target (player bot or not) return true if spec is tank or if the bot have tank strategies (bear/tank/tank face).
static inline bool IsTankRole(Player* p)
{
if (!player || !player->IsAlive() || player->GetMapId() != bot->GetMapId())
return false;
if (player->IsGameMaster())
return false;
return bot->GetDistance(player) < sPlayerbotAIConfig.spellDistance * 2 &&
bot->IsWithinLOS(player->GetPositionX(), player->GetPositionY(),
player->GetPositionZ());
}
static bool HasBlessingAura(
PlayerbotAI* botAI, Unit* target, std::initializer_list<char const*> auraNames)
{
for (char const* auraName : auraNames)
{
if (botAI->HasAura(auraName, target))
return true;
}
return false;
}
static bool IsGreaterBlessingMode(Player* bot)
{
return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup());
}
template <typename Predicate>
static Unit* FindBlessingTarget(
Player* bot, PlayerbotAI* botAI, Predicate&& predicate)
{
std::vector<Player*> masters;
std::vector<Player*> healers;
std::vector<Player*> tanks;
std::vector<Player*> others;
Player* master = botAI->GetMaster();
auto addPlayer = [&](Player* player)
{
if (!IsBlessingTargetCandidate(bot, player))
return;
if (player == master)
masters.push_back(player);
else if (botAI->IsHeal(player))
healers.push_back(player);
else if (botAI->IsTank(player))
tanks.push_back(player);
else
others.push_back(player);
};
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
addPlayer(ref->GetSource());
}
else
{
addPlayer(bot);
}
std::vector<std::vector<Player*>*> orderedLists = {
&masters, &healers, &tanks, &others };
for (std::vector<Player*>* players : orderedLists)
{
for (Player* player : *players)
{
if (predicate(player))
return player;
}
}
return nullptr;
}
static inline bool IsTankRole(Player* player)
{
if (!player)
return false;
if (player->HasTankSpec())
if (!p) return false;
if (p->HasTankSpec())
return true;
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(player))
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p))
{
if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("tank", BOT_STATE_COMBAT) ||
@ -110,36 +34,33 @@ static inline bool IsTankRole(Player* player)
otherAI->HasStrategy("bear", BOT_STATE_COMBAT))
return true;
}
return false;
}
// Added for solo paladin patch : determine if he's the only paladin on party
static inline bool IsOnlyPaladinInGroup(Player* bot)
{
if (!bot)
return false;
Group* group = bot->GetGroup();
if (!group)
return true;
uint32 paladins = 0u;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
if (!bot) return false;
Group* g = bot->GetGroup();
if (!g) return true; // solo
uint32 pals = 0u;
for (GroupReference* r = g->GetFirstMember(); r; r = r->next())
{
Player* player = ref->GetSource();
if (!player || !player->IsInWorld()) continue;
if (player->getClass() == CLASS_PALADIN) ++paladins;
Player* p = r->GetSource();
if (!p || !p->IsInWorld()) continue;
if (p->getClass() == CLASS_PALADIN) ++pals;
}
return paladins == 1u;
return pals == 1u;
}
inline std::string const GetActualBlessingOfMight(Unit* target)
{
if (!target->ToPlayer())
{
return "blessing of might";
}
uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass())
{
case CLASS_MAGE:
@ -149,15 +70,21 @@ inline std::string const GetActualBlessingOfMight(Unit* target)
break;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION)
{
return "blessing of wisdom";
}
break;
case CLASS_DRUID:
if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE)
{
return "blessing of wisdom";
}
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_HOLY)
{
return "blessing of wisdom";
}
break;
}
@ -167,9 +94,10 @@ inline std::string const GetActualBlessingOfMight(Unit* target)
inline std::string const GetActualBlessingOfWisdom(Unit* target)
{
if (!target->ToPlayer())
{
return "blessing of might";
uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
}
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass())
{
case CLASS_WARRIOR:
@ -180,15 +108,21 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target)
break;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ENHANCEMENT)
{
return "blessing of might";
}
break;
case CLASS_DRUID:
if (tab == DRUID_TAB_FERAL)
{
return "blessing of might";
}
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION)
{
return "blessing of might";
}
break;
}
@ -197,41 +131,32 @@ inline std::string const GetActualBlessingOfWisdom(Unit* target)
inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot)
{
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY))
return "";
Player* targetPlayer = target->ToPlayer();
if (!targetPlayer)
Player* tp = target->ToPlayer();
if (!tp)
return "";
if (auto* botAI = GET_PLAYERBOT_AI(bot))
if (auto* ai = GET_PLAYERBOT_AI(bot))
{
if (Unit* mainTank =
botAI->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
if (Unit* mt = ai->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
{
if (mainTank == target)
if (mt == target)
return "blessing of sanctuary";
}
}
if (targetPlayer->HasTankSpec())
if (tp->HasTankSpec())
return "blessing of sanctuary";
return "";
}
Unit* CastBlessingOfMightOnPartyAction::GetTarget()
Value<Unit*>* CastBlessingOnPartyAction::GetTargetValue()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
return !HasBlessingAura(botAI, player,
{ "blessing of might", "greater blessing of might",
"blessing of wisdom", "greater blessing of wisdom",
"blessing of sanctuary", "greater blessing of sanctuary" });
});
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
}
bool CastBlessingOfMightAction::Execute(Event /*event*/)
@ -241,6 +166,9 @@ bool CastBlessingOfMightAction::Execute(Event /*event*/)
return false;
std::string castName = GetActualBlessingOfMight(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
@ -248,22 +176,20 @@ Value<Unit*>* CastBlessingOfMightOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of might,greater blessing of might,blessing of wisdom,"
"greater blessing of wisdom,blessing of sanctuary,"
"greater blessing of sanctuary"
"blessing of might,greater blessing of might,blessing of wisdom,greater blessing of wisdom,blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfMight(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
@ -274,58 +200,45 @@ bool CastBlessingOfWisdomAction::Execute(Event /*event*/)
return false;
std::string castName = GetActualBlessingOfWisdom(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
Unit* CastBlessingOfWisdomOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && IsTankRole(player))
return false;
return !HasBlessingAura(botAI, player,
{ "blessing of might", "greater blessing of might",
"blessing of wisdom", "greater blessing of wisdom",
"blessing of sanctuary", "greater blessing of sanctuary" });
});
}
Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,"
"blessing of sanctuary,greater blessing of sanctuary"
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget();
if (!target)
return false;
Player* targetPlayer = target->ToPlayer();
if (Group* group = bot->GetGroup())
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
if (Group* g = bot->GetGroup())
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
return false;
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) &&
if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) &&
targetPlayer && IsTankRole(targetPlayer))
{
LOG_DEBUG("playerbots", "[Wisdom/bmana] Skip tank {} (Kings only)", target->GetName());
return false;
}
std::string castName = GetActualBlessingOfWisdom(target);
if (castName.empty())
return false;
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
@ -339,31 +252,32 @@ Value<Unit*>* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue()
bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY))
return false;
Unit* target = GetTarget();
if (!target)
{
// Fallback: GetTarget() can be null if no one needs a buff.
// Keep a valid pointer for the checks/logs that follow.
target = bot;
}
Player* targetPlayer = target ? target->ToPlayer() : nullptr;
const auto HasKingsAura = [&](Unit* unit) -> bool {
return botAI->HasAura("blessing of kings", unit) ||
botAI->HasAura("greater blessing of kings", unit);
// Small helpers to check relevant auras
const auto HasKingsAura = [&](Unit* u) -> bool {
return botAI->HasAura("blessing of kings", u) || botAI->HasAura("greater blessing of kings", u);
};
const auto HasSanctAura = [&](Unit* unit) -> bool {
return botAI->HasAura("blessing of sanctuary", unit) ||
botAI->HasAura("greater blessing of sanctuary", unit);
const auto HasSanctAura = [&](Unit* u) -> bool {
return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u);
};
if (Group* group = bot->GetGroup())
if (Group* g = bot->GetGroup())
{
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
{
LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring");
target = bot;
targetPlayer = bot->ToPlayer();
}
@ -374,6 +288,9 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
bool selfHasSanct = HasSanctAura(self);
bool needSelf = IsTankRole(self) && !selfHasSanct;
LOG_DEBUG("playerbots", "[Sanct] {} isTank={} selfHasSanct={} needSelf={}",
bot->GetName(), IsTankRole(self), selfHasSanct, needSelf);
if (needSelf)
{
target = self;
@ -381,6 +298,7 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
}
}
// Try to re-target a valid tank in group if needed
bool targetOk = false;
if (targetPlayer)
{
@ -390,20 +308,20 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
if (!targetOk)
{
if (Group* group = bot->GetGroup())
if (Group* g = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next())
{
Player* player = ref->GetSource();
if (!player) continue;
if (!player->IsInWorld() || !player->IsAlive()) continue;
if (!IsTankRole(player)) continue;
Player* p = gref->GetSource();
if (!p) continue;
if (!p->IsInWorld() || !p->IsAlive()) continue;
if (!IsTankRole(p)) continue;
bool hasSanct = HasSanctAura(player);
bool hasSanct = HasSanctAura(p);
if (!hasSanct)
{
target = player;
targetPlayer = player;
target = p; // prioritize this tank
targetPlayer = p;
targetOk = true;
break;
}
@ -411,147 +329,150 @@ bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
}
}
if (GetActualBlessingOfSanctuary(target, bot).empty())
{
bool hasKings = HasKingsAura(target);
bool hasSanct = HasSanctAura(target);
bool knowSanct = bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY);
LOG_DEBUG("playerbots", "[Sanct] Final target={} hasKings={} hasSanct={} knowSanct={}",
target->GetName(), hasKings, hasSanct, knowSanct);
}
std::string castName = GetActualBlessingOfSanctuary(target, bot);
// If internal logic didn't recognize the tank (e.g., bear druid), force single-target Sanctuary
if (castName.empty())
{
if (targetPlayer)
{
if (IsTankRole(targetPlayer))
return botAI->CastSpell("blessing of sanctuary", target);
castName = "blessing of sanctuary"; // force single-target
else
return false;
}
else
return false;
}
return botAI->CastSpell("blessing of sanctuary", target);
}
Unit* CastBlessingOfSanctuaryOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
if (targetPlayer && !IsTankRole(targetPlayer))
{
return IsTankRole(player) &&
!HasBlessingAura(botAI, player,
{ "blessing of sanctuary", "greater blessing of sanctuary" });
});
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
}
else
{
castName = "blessing of sanctuary";
}
bool ok = botAI->CastSpell(castName, target);
LOG_DEBUG("playerbots", "[Sanct] Cast {} on {} result={}", castName, target->GetName(), ok);
return ok;
}
Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of kings,greater blessing of kings,"
"blessing of sanctuary,greater blessing of sanctuary"
"blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary"
);
}
Unit* CastBlessingOfKingsOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
const bool onlyPaladinInGroup = IsOnlyPaladinInGroup(bot);
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
const bool isTank = IsTankRole(player);
const bool hasKingsOrSanct = HasBlessingAura(botAI, player,
{ "blessing of kings", "greater blessing of kings",
"blessing of sanctuary", "greater blessing of sanctuary" });
if (hasKingsOrSanct)
return false;
if (hasBwisdom)
return isTank;
if (hasBkings)
{
if (isTank)
return false;
if (onlyPaladinInGroup && player == bot)
return false;
}
return true;
});
}
bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget();
if (!target)
return false;
Group* group = bot->GetGroup();
if (!group)
Group* g = bot->GetGroup();
if (!g)
return false;
if (botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT) &&
IsOnlyPaladinInGroup(bot))
// Added for patch solo paladin, never buff itself to not remove his sanctuary buff
if (botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT) && IsOnlyPaladinInGroup(bot))
{
if (target->GetGUID() == bot->GetGUID())
{
LOG_DEBUG("playerbots", "[Kings/bstats-solo] Skip self to keep Sanctuary on {}", bot->GetName());
return false;
}
}
// End solo paladin patch
Player* targetPlayer = target->ToPlayer();
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
return false;
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
const bool hasBmana = botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT);
const bool hasBstats = botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT);
if (hasBwisdom && (!targetPlayer || !IsTankRole(targetPlayer)))
return false;
if (hasBmana)
{
if (!targetPlayer || !IsTankRole(targetPlayer))
{
LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName());
return false;
}
}
if (targetPlayer)
{
const bool isTank = IsTankRole(targetPlayer);
const bool hasSanctFromMe =
target->HasAura(ai::paladin::SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
target->HasAura(ai::paladin::SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
target->HasAura(SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
target->HasAura(SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
const bool hasSanctAny =
botAI->HasAura("blessing of sanctuary", target) ||
botAI->HasAura("greater blessing of sanctuary", target);
if (isTank && hasSanctFromMe)
{
LOG_DEBUG("playerbots", "[Kings] Skip: {} has my Sanctuary and is a tank", target->GetName());
return false;
}
if (hasBkings && isTank && hasSanctAny)
if (hasBstats && isTank && hasSanctAny)
{
LOG_DEBUG("playerbots", "[Kings] Skip (bstats): {} already has Sanctuary and is a tank", target->GetName());
return false;
}
}
return botAI->CastSpell("blessing of kings", target);
std::string castName = "blessing of kings";
bool allowGreater = true;
if (hasBmana)
allowGreater = false;
if (allowGreater && hasBstats && targetPlayer)
{
switch (targetPlayer->getClass())
{
case CLASS_WARRIOR:
case CLASS_PALADIN:
case CLASS_DRUID:
case CLASS_DEATH_KNIGHT:
allowGreater = false;
break;
default:
break;
}
}
if (allowGreater)
{
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
}
return botAI->CastSpell(castName, target);
}
bool CastSealSpellAction::isUseful()
{
return AI_VALUE2(bool, "combat", "self target");
}
bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); }
Value<Unit*>* CastTurnUndeadAction::GetTargetValue()
{
return context->GetValue<Unit*>("cc target", getName());
}
Value<Unit*>* CastTurnUndeadAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", getName()); }
Unit* CastHandOfFreedomOnPartyAction::GetTarget()
{
bool const selfImpaired = botAI->IsMovementImpaired(bot);
bool const hasSelfHand =
selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
if (!bot->GetGroup())
{
@ -578,8 +499,7 @@ bool CastHandOfFreedomOnPartyAction::isUseful()
if (!target)
return false;
return CastBuffSpellAction::isUseful() &&
!ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
}
Unit* CastRighteousDefenseAction::GetTarget()

View File

@ -8,6 +8,10 @@
#include "AiObject.h"
#include "GenericSpellActions.h"
#include "SharedDefines.h"
class PlayerbotAI;
class Unit;
// seals
BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness");
@ -84,13 +88,24 @@ public:
bool Execute(Event event) override;
};
class CastBlessingOnPartyAction : public BuffOnPartyAction
{
public:
CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name)
: BuffOnPartyAction(botAI, name), name(name) {}
Value<Unit*>* GetTargetValue() override;
private:
std::string name;
};
class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction
{
public:
CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {}
std::string const getName() override { return "blessing of might on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};
@ -109,7 +124,6 @@ public:
CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {}
std::string const getName() override { return "blessing of wisdom on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};
@ -120,13 +134,12 @@ public:
CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {}
};
class CastBlessingOfKingsOnPartyAction : public BuffOnPartyAction
class CastBlessingOfKingsOnPartyAction : public CastBlessingOnPartyAction
{
public:
CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of kings") {}
CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : CastBlessingOnPartyAction(botAI, "blessing of kings") {}
std::string const getName() override { return "blessing of kings on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override; // added for Sanctuary priority
bool Execute(Event event) override; // added for 2 paladins logic
};
@ -143,7 +156,6 @@ public:
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
std::string const getName() override { return "blessing of sanctuary on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,267 +0,0 @@
/*
* 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.
*/
#ifndef _PLAYERBOT_PALADINGREATERBLESSINGACTION_H
#define _PLAYERBOT_PALADINGREATERBLESSINGACTION_H
#include <array>
#include <string>
#include <vector>
#include "Action.h"
#include "AiFactory.h"
#include "Playerbots.h"
#include "SharedDefines.h"
class UntypedValue;
namespace ai::gbless
{
enum RoleProfile : uint8
{
ROLE_CASTER = 0,
ROLE_PHYSICAL_DPS = 1,
ROLE_HYBRID_DPS = 2,
ROLE_DRUID_TANK = 3,
ROLE_WARRIOR_DK_TANK = 4,
ROLE_PALADIN_TANK = 5,
ROLE_PROFILE_COUNT = 6
};
enum BlessingType : uint8
{
BLESSING_NONE = 0,
BLESSING_MIGHT_SINGLE = 1,
BLESSING_MIGHT_GREATER = 2,
BLESSING_WISDOM_SINGLE = 3,
BLESSING_WISDOM_GREATER = 4,
BLESSING_KINGS_SINGLE = 5,
BLESSING_KINGS_GREATER = 6,
BLESSING_SANCTUARY_SINGLE = 7,
BLESSING_SANCTUARY_GREATER = 8
};
enum BaseBlessingCategory : uint8
{
BASE_NONE = 0,
BASE_MIGHT = 1,
BASE_WISDOM = 2,
BASE_KINGS = 3,
BASE_SANCTUARY = 4
};
inline constexpr BaseBlessingCategory BaseBlessingOf(BlessingType type)
{
switch (type)
{
case BLESSING_MIGHT_SINGLE:
case BLESSING_MIGHT_GREATER: return BASE_MIGHT;
case BLESSING_WISDOM_SINGLE:
case BLESSING_WISDOM_GREATER: return BASE_WISDOM;
case BLESSING_KINGS_SINGLE:
case BLESSING_KINGS_GREATER: return BASE_KINGS;
case BLESSING_SANCTUARY_SINGLE:
case BLESSING_SANCTUARY_GREATER: return BASE_SANCTUARY;
default: return BASE_NONE;
}
}
inline constexpr bool IsSingleVariant(BlessingType type)
{
return type == BLESSING_MIGHT_SINGLE || type == BLESSING_WISDOM_SINGLE ||
type == BLESSING_KINGS_SINGLE || type == BLESSING_SANCTUARY_SINGLE;
}
inline constexpr bool IsGreaterVariant(BlessingType type)
{
return type == BLESSING_MIGHT_GREATER || type == BLESSING_WISDOM_GREATER ||
type == BLESSING_KINGS_GREATER || type == BLESSING_SANCTUARY_GREATER;
}
inline constexpr BlessingType ToSingleVariant(BaseBlessingCategory category)
{
switch (category)
{
case BASE_MIGHT: return BLESSING_MIGHT_SINGLE;
case BASE_WISDOM: return BLESSING_WISDOM_SINGLE;
case BASE_KINGS: return BLESSING_KINGS_SINGLE;
case BASE_SANCTUARY: return BLESSING_SANCTUARY_SINGLE;
default: return BLESSING_NONE;
}
}
inline constexpr BlessingType ToSingleVariant(BlessingType type)
{
return ToSingleVariant(BaseBlessingOf(type));
}
inline constexpr BlessingType ToGreaterVariant(BaseBlessingCategory category)
{
switch (category)
{
case BASE_MIGHT: return BLESSING_MIGHT_GREATER;
case BASE_WISDOM: return BLESSING_WISDOM_GREATER;
case BASE_KINGS: return BLESSING_KINGS_GREATER;
case BASE_SANCTUARY: return BLESSING_SANCTUARY_GREATER;
default: return BLESSING_NONE;
}
}
inline constexpr BlessingType ToGreaterVariant(BlessingType type)
{
return ToGreaterVariant(BaseBlessingOf(type));
}
inline std::string BlessingSpellName(BlessingType type)
{
switch (type)
{
case BLESSING_MIGHT_SINGLE: return "blessing of might";
case BLESSING_MIGHT_GREATER: return "greater blessing of might";
case BLESSING_WISDOM_SINGLE: return "blessing of wisdom";
case BLESSING_WISDOM_GREATER: return "greater blessing of wisdom";
case BLESSING_KINGS_SINGLE: return "blessing of kings";
case BLESSING_KINGS_GREATER: return "greater blessing of kings";
case BLESSING_SANCTUARY_SINGLE: return "blessing of sanctuary";
case BLESSING_SANCTUARY_GREATER: return "greater blessing of sanctuary";
default: return "";
}
}
struct BaseBlessingPriorityEntry
{
BaseBlessingCategory priorities[4];
};
inline constexpr BaseBlessingPriorityEntry BASE_BLESSING_PRIORITIES[ROLE_PROFILE_COUNT] =
{
// All casters
{{ BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY, BASE_MIGHT }},
// Physical DPS (no mana)
{{ BASE_MIGHT, BASE_KINGS, BASE_SANCTUARY, BASE_NONE }},
// Hybrid DPS
{{ BASE_MIGHT, BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY }},
// Druid tanks
{{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_WISDOM, }},
// Warrior and DK tanks
{{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_NONE }},
// Paladin tanks
{{ BASE_SANCTUARY, BASE_MIGHT, BASE_WISDOM, BASE_KINGS }},
};
constexpr uint32 SPELL_IMPROVED_MIGHT_R1 = 20042;
constexpr uint32 SPELL_IMPROVED_MIGHT_R2 = 20045;
constexpr uint32 SPELL_IMPROVED_WISDOM_R1 = 20244;
constexpr uint32 SPELL_IMPROVED_WISDOM_R2 = 20245;
inline RoleProfile ResolveRoleProfile(Player* player)
{
if (!player)
return ROLE_CASTER;
uint8 cls = player->getClass();
int tab = AiFactory::GetPlayerSpecTab(player);
bool isTank = PlayerbotAI::IsTank(player);
switch (cls)
{
case CLASS_WARRIOR:
if (isTank)
return ROLE_WARRIOR_DK_TANK;
return ROLE_PHYSICAL_DPS;
case CLASS_DEATH_KNIGHT:
if (isTank)
return ROLE_WARRIOR_DK_TANK;
return ROLE_PHYSICAL_DPS;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ENHANCEMENT)
return ROLE_HYBRID_DPS;
return ROLE_CASTER;
case CLASS_PALADIN:
if (isTank)
return ROLE_PALADIN_TANK;
if (tab == PALADIN_TAB_HOLY)
return ROLE_CASTER;
return ROLE_HYBRID_DPS;
case CLASS_DRUID:
if (tab == DRUID_TAB_FERAL)
return isTank ? ROLE_DRUID_TANK : ROLE_HYBRID_DPS;
return ROLE_CASTER;
case CLASS_ROGUE:
return ROLE_PHYSICAL_DPS;
case CLASS_HUNTER:
return ROLE_HYBRID_DPS;
case CLASS_MAGE:
return ROLE_CASTER;
case CLASS_WARLOCK:
return ROLE_CASTER;
case CLASS_PRIEST:
return ROLE_CASTER;
default:
return ROLE_CASTER;
}
}
struct GreaterBlessingPlayerAssignment
{
Player* player = nullptr;
BlessingType blessing = BLESSING_NONE;
};
struct CachedBlessingBucketAssignment
{
uint8 classId = 0;
RoleProfile role = ROLE_CASTER;
bool byRole = false;
BlessingType blessing = BLESSING_NONE;
};
struct CachedBlessingAssignments
{
uint32 groupKey = 0;
bool valid = false;
std::vector<CachedBlessingBucketAssignment> assignments;
};
struct CachedPendingBlessingAssignment
{
uint32 groupKey = 0;
bool valid = false;
GreaterBlessingPlayerAssignment assignment;
std::string spellName;
};
UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI);
UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI);
bool IsEligibleGroupForAutoBlessings(Group const* group);
}
class CastGreaterBlessingAssignmentAction : public Action
{
public:
CastGreaterBlessingAssignmentAction(PlayerbotAI* botAI);
bool Execute(Event event) override;
bool isUseful() override;
bool HasPendingAssignment();
private:
bool FindPendingAssignment(
ai::gbless::GreaterBlessingPlayerAssignment& outAssignment,
std::string& outSpellName);
};
#endif

View File

@ -7,7 +7,6 @@
#include "DpsPaladinStrategy.h"
#include "GenericPaladinNonCombatStrategy.h"
#include "PaladinGreaterBlessingAction.h"
#include "HealPaladinStrategy.h"
#include "NamedObjectContext.h"
#include "OffhealRetPaladinStrategy.h"
@ -71,17 +70,17 @@ class PaladinBuffStrategyFactoryInternal : public NamedObjectContext<Strategy>
public:
PaladinBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
{
creators["bsanc"] = &PaladinBuffStrategyFactoryInternal::bsanc;
creators["bwisdom"] = &PaladinBuffStrategyFactoryInternal::bwisdom;
creators["bmight"] = &PaladinBuffStrategyFactoryInternal::bmight;
creators["bkings"] = &PaladinBuffStrategyFactoryInternal::bkings;
creators["bhealth"] = &PaladinBuffStrategyFactoryInternal::bhealth;
creators["bmana"] = &PaladinBuffStrategyFactoryInternal::bmana;
creators["bdps"] = &PaladinBuffStrategyFactoryInternal::bdps;
creators["bstats"] = &PaladinBuffStrategyFactoryInternal::bstats;
}
private:
static Strategy* bsanc(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
static Strategy* bwisdom(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
static Strategy* bmight(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
static Strategy* bkings(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
static Strategy* bhealth(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
static Strategy* bmana(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
static Strategy* bdps(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
static Strategy* bstats(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
};
class PaladinCombatStrategyFactoryInternal : public NamedObjectContext<Strategy>
@ -155,7 +154,6 @@ public:
creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party;
creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath;
creators["greater blessing needed"] = &PaladinTriggerFactoryInternal::greater_blessing_needed;
}
private:
@ -213,8 +211,8 @@ private:
static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); }
static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); }
static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); }
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new BeaconOfLightOnMainTankTrigger(botAI); }
static Trigger* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new SacredShieldOnMainTankTrigger(botAI); }
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); }
static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); }
static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); }
static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); }
@ -229,10 +227,6 @@ private:
}
static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); }
static Trigger* greater_blessing_needed(PlayerbotAI* botAI)
{
return new GreaterBlessingNeededTrigger(botAI);
}
};
class PaladinAiObjectContextInternal : public NamedObjectContext<Action>
@ -322,8 +316,6 @@ public:
creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice;
creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice;
creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party;
creators["cast greater blessing assignment"] =
&PaladinAiObjectContextInternal::cast_greater_blessing_assignment;
}
private:
@ -422,41 +414,15 @@ private:
static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); }
static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); }
static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); }
static Action* divine_plea(PlayerbotAI* botAI) { return new CastDivinePleaAction(botAI); }
static Action* shield_of_righteousness(PlayerbotAI* botAI) { return new ShieldOfRighteousnessAction(botAI); }
static Action* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new CastBeaconOfLightOnMainTankAction(botAI); }
static Action* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new CastSacredShieldOnMainTankAction(botAI); }
static Action* avenging_wrath(PlayerbotAI* botAI) { return new CastAvengingWrathAction(botAI); }
static Action* divine_illumination(PlayerbotAI* botAI) { return new CastDivineIlluminationAction(botAI); }
static Action* divine_sacrifice(PlayerbotAI* botAI) { return new CastDivineSacrificeAction(botAI); }
static Action* cancel_divine_sacrifice(PlayerbotAI* botAI) { return new CastCancelDivineSacrificeAction(botAI); }
static Action* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new CastHandOfFreedomOnPartyAction(botAI); }
static Action* cast_greater_blessing_assignment(PlayerbotAI* botAI)
{
return new CastGreaterBlessingAssignmentAction(botAI);
}
};
class PaladinValueContextInternal : public NamedObjectContext<UntypedValue>
{
public:
PaladinValueContextInternal()
{
creators["greater blessing assignments"] = &PaladinValueContextInternal::greater_blessing_assignments;
creators["greater blessing pending assignment"] =
&PaladinValueContextInternal::greater_blessing_pending_assignment;
}
private:
static UntypedValue* greater_blessing_assignments(PlayerbotAI* botAI)
{
return ai::gbless::greater_blessing_assignments_value(botAI);
}
static UntypedValue* greater_blessing_pending_assignment(PlayerbotAI* botAI)
{
return ai::gbless::greater_blessing_pending_assignment_value(botAI);
}
static Action* divine_plea(PlayerbotAI* ai) { return new CastDivinePleaAction(ai); }
static Action* shield_of_righteousness(PlayerbotAI* ai) { return new ShieldOfRighteousnessAction(ai); }
static Action* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new CastBeaconOfLightOnMainTankAction(ai); }
static Action* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new CastSacredShieldOnMainTankAction(ai); }
static Action* avenging_wrath(PlayerbotAI* ai) { return new CastAvengingWrathAction(ai); }
static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); }
static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); }
static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); }
static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); }
};
SharedNamedObjectContextList<Strategy> PaladinAiObjectContext::sharedStrategyContexts;
@ -501,5 +467,4 @@ void PaladinAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContext
void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& valueContexts)
{
AiObjectContext::BuildSharedValueContexts(valueContexts);
valueContexts.Add(new PaladinValueContextInternal());
}

View File

@ -30,7 +30,4 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) }));
triggers.push_back(new TriggerNode("greater blessing needed",
{ NextAction("cast greater blessing assignment", ACTION_NORMAL) }));
}

View File

@ -16,7 +16,7 @@ public:
PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bwisdom"; }
std::string const getName() override { return "bmana"; }
};
class PaladinBuffHealthStrategy : public Strategy
@ -25,7 +25,7 @@ public:
PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bsanc"; }
std::string const getName() override { return "bhealth"; }
};
class PaladinBuffDpsStrategy : public Strategy
@ -34,7 +34,7 @@ public:
PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bmight"; }
std::string const getName() override { return "bdps"; }
};
class PaladinBuffArmorStrategy : public Strategy
@ -88,7 +88,7 @@ public:
PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bkings"; }
std::string const getName() override { return "bstats"; }
};
class PaladinShadowResistanceStrategy : public Strategy

View File

@ -95,14 +95,13 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
}
)
);
triggers.push_back(
new TriggerNode(
"light aoe",
{
NextAction("avenger's shield", ACTION_HIGH + 5)
}
)
);
triggers.push_back(new TriggerNode(
"light aoe",
{
NextAction("avenger's shield", ACTION_HIGH + 5)
}
)
);
triggers.push_back(
new TriggerNode(
"medium aoe",
@ -123,6 +122,13 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back(
new TriggerNode(
"medium health",
{ NextAction("holy shield", ACTION_HIGH + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"low health",
{
NextAction("holy shield", ACTION_HIGH + 4)
}
@ -130,12 +136,20 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
);
triggers.push_back(
new TriggerNode(
"avenging wrath",
"critical health",
{
NextAction("avenging wrath", ACTION_HIGH + 2)
NextAction("holy shield", ACTION_HIGH + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"avenging wrath",
{
NextAction("avenging wrath", ACTION_HIGH + 2)
}
)
);
triggers.push_back(
new TriggerNode(
"target critical health",

View File

@ -5,11 +5,10 @@
#include "PaladinTriggers.h"
#include "GenericBuffUtils.h"
#include "PaladinGreaterBlessingAction.h"
#include "PaladinActions.h"
#include "PaladinHelper.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "PaladinHelper.h"
bool SealTrigger::IsActive()
{
@ -29,9 +28,8 @@ bool CrusaderAuraTrigger::IsActive()
bool BlessingTrigger::IsActive()
{
Unit* target = GetTarget();
return SpellTrigger::IsActive() &&
!botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr);
return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr);
}
bool DivineShieldLowHealthTrigger::IsActive()
@ -64,8 +62,7 @@ bool HandOfFreedomOnPartyTrigger::IsActive()
if (!target)
return false;
if (target != bot &&
bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f)
if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f)
return false;
if (!botAI->CanCastSpell("hand of freedom", target))
@ -78,29 +75,3 @@ bool NotSensingUndeadTrigger::IsActive()
{
return !botAI->HasAura("sense undead", bot);
}
bool GreaterBlessingNeededTrigger::IsActive()
{
if (!ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()))
return false;
if (ai::buff::ShouldDeferGreaterBlessingAssignmentForRecentLogin(bot))
return false;
Group* group = bot->GetGroup();
uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0;
Value<ai::gbless::CachedPendingBlessingAssignment>* pendingValue =
context->GetValue<ai::gbless::CachedPendingBlessingAssignment>("greater blessing pending assignment");
if (!pendingValue)
return false;
ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get();
if (pendingAssignment.groupKey != groupKey)
{
pendingValue->Reset();
pendingAssignment = pendingValue->Get();
}
return pendingAssignment.valid && pendingAssignment.groupKey == groupKey;
}

View File

@ -13,6 +13,32 @@
class PlayerbotAI;
inline std::string const GetActualBlessingOfMight(Unit* target)
{
switch (target->getClass())
{
case CLASS_MAGE:
case CLASS_PRIEST:
case CLASS_WARLOCK:
return "blessing of wisdom";
}
return "blessing of might";
}
inline std::string const GetActualBlessingOfWisdom(Unit* target)
{
switch (target->getClass())
{
case CLASS_WARRIOR:
case CLASS_ROGUE:
case CLASS_DEATH_KNIGHT:
return "blessing of might";
}
return "blessing of wisdom";
}
BUFF_TRIGGER(HolyShieldTrigger, "holy shield");
BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury");
@ -186,55 +212,42 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
{
public:
BeaconOfLightOnMainTankTrigger(PlayerbotAI* botAI)
: BuffOnMainTankTrigger(botAI, "beacon of light", true) {}
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai)
: BuffOnMainTankTrigger(ai, "beacon of light", true) {}
};
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
{
public:
SacredShieldOnMainTankTrigger(PlayerbotAI* botAI)
: BuffOnMainTankTrigger(botAI, "sacred shield", false) {}
SacredShieldOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "sacred shield", false) {}
};
class BlessingOfKingsOnPartyTrigger : public BlessingOnPartyTrigger
class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI)
: BlessingOnPartyTrigger(botAI)
{
spell = "blessing of kings";
}
: BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {}
};
class BlessingOfWisdomOnPartyTrigger : public BlessingOnPartyTrigger
class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI)
: BlessingOnPartyTrigger(botAI)
{
spell = "blessing of might,blessing of wisdom";
}
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
};
class BlessingOfMightOnPartyTrigger : public BlessingOnPartyTrigger
class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI)
: BlessingOnPartyTrigger(botAI)
{
spell = "blessing of might,blessing of wisdom";
}
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
};
class BlessingOfSanctuaryOnPartyTrigger : public BlessingOnPartyTrigger
class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
: BlessingOnPartyTrigger(botAI)
{
spell = "blessing of sanctuary";
}
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {}
};
class HandOfFreedomOnPartyTrigger : public Trigger
@ -253,13 +266,4 @@ public:
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
};
class GreaterBlessingNeededTrigger : public Trigger
{
public:
GreaterBlessingNeededTrigger(PlayerbotAI* botAI)
: Trigger(botAI, "greater blessing needed", 4) {}
bool IsActive() override;
};
#endif

View File

@ -18,8 +18,6 @@ static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022;
static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038;
static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044;
static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940;
static constexpr uint32 SPELL_BLESSING_OF_SANCTUARY = 20911;
static constexpr uint32 SPELL_GREATER_BLESSING_OF_SANCTUARY = 25899;
inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list<uint32> spellIds)
{

View File

@ -13,19 +13,9 @@
class PlayerbotAI;
// disc
class CastPowerWordFortitudeAction : public GroupBuffSpellAction
{
public:
CastPowerWordFortitudeAction(PlayerbotAI* botAI)
: GroupBuffSpellAction(botAI, "power word: fortitude") {}
};
class CastPowerWordFortitudeOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastPowerWordFortitudeOnPartyAction(PlayerbotAI* botAI)
: GroupBuffOnPartyAction(botAI, "power word: fortitude") {}
};
BUFF_ACTION(CastPowerWordFortitudeAction, "power word: fortitude");
BUFF_PARTY_ACTION(CastPowerWordFortitudeOnPartyAction, "power word: fortitude");
BUFF_PARTY_ACTION(CastPrayerOfFortitudeOnPartyAction, "prayer of fortitude");
BUFF_ACTION(CastPowerWordShieldAction, "power word: shield");
BUFF_ACTION(CastInnerFireAction, "inner fire");
@ -36,19 +26,9 @@ CC_ACTION(CastShackleUndeadAction, "shackle undead");
SPELL_ACTION_U(CastManaBurnAction, "mana burn",
AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20);
BUFF_ACTION(CastLevitateAction, "levitate");
class CastDivineSpiritAction : public GroupBuffSpellAction
{
public:
CastDivineSpiritAction(PlayerbotAI* botAI)
: GroupBuffSpellAction(botAI, "divine spirit") {}
};
class CastDivineSpiritOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastDivineSpiritOnPartyAction(PlayerbotAI* botAI)
: GroupBuffOnPartyAction(botAI, "divine spirit") {}
};
BUFF_ACTION(CastDivineSpiritAction, "divine spirit");
BUFF_PARTY_ACTION(CastDivineSpiritOnPartyAction, "divine spirit");
BUFF_PARTY_ACTION(CastPrayerOfSpiritOnPartyAction, "prayer of spirit");
// disc 2.4.3
SPELL_ACTION(CastMassDispelAction, "mass dispel");
@ -123,23 +103,13 @@ SPELL_ACTION(CastMindBlastAction, "mind blast");
SPELL_ACTION(CastPsychicScreamAction, "psychic scream");
DEBUFF_ACTION(CastMindSootheAction, "mind soothe");
BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup());
class CastShadowProtectionAction : public GroupBuffSpellAction
{
public:
CastShadowProtectionAction(PlayerbotAI* botAI)
: GroupBuffSpellAction(botAI, "shadow protection") {}
};
class CastShadowProtectionOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastShadowProtectionOnPartyAction(PlayerbotAI* botAI)
: GroupBuffOnPartyAction(botAI, "shadow protection") {}
};
BUFF_ACTION(CastShadowProtectionAction, "shadow protection");
BUFF_PARTY_ACTION(CastShadowProtectionOnPartyAction, "shadow protection");
BUFF_PARTY_ACTION(CastPrayerOfShadowProtectionAction, "prayer of shadow protection");
// shadow talents
SPELL_ACTION(CastMindFlayAction, "mind flay");
BUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace");
DEBUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace");
BUFF_ACTION(CastShadowformAction, "shadowform");
SPELL_ACTION(CastSilenceAction, "silence");
ENEMY_HEALER_ACTION(CastSilenceOnEnemyHealerAction, "silence");

View File

@ -92,6 +92,8 @@ public:
creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection;
creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party;
creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead;
creators["prayer of fortitude on party"] = &PriestTriggerFactoryInternal::prayer_of_fortitude_on_party;
creators["prayer of spirit on party"] = &PriestTriggerFactoryInternal::prayer_of_spirit_on_party;
creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire;
creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness;
creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness;
@ -134,6 +136,8 @@ private:
static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); }
static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); }
static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); }
static Trigger* prayer_of_fortitude_on_party(PlayerbotAI* botAI) { return new PrayerOfFortitudeTrigger(botAI); }
static Trigger* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new PrayerOfSpiritTrigger(botAI); }
static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); }
static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); }
static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); }
@ -203,6 +207,8 @@ public:
creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection;
creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party;
creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead;
creators["prayer of fortitude on party"] = &PriestAiObjectContextInternal::prayer_of_fortitude_on_party;
creators["prayer of spirit on party"] = &PriestAiObjectContextInternal::prayer_of_spirit_on_party;
creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party;
creators["silence"] = &PriestAiObjectContextInternal::silence;
creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer;
@ -305,6 +311,11 @@ private:
static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); }
static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); }
static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); }
static Action* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new CastPrayerOfSpiritOnPartyAction(botAI); }
static Action* prayer_of_fortitude_on_party(PlayerbotAI* botAI)
{
return new CastPrayerOfFortitudeOnPartyAction(botAI);
}
static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); }
static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); }
static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); }

View File

@ -19,8 +19,6 @@ void PriestNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back(
new TriggerNode("inner fire",{ NextAction("inner fire", 10.0f) }));
triggers.push_back(
new TriggerNode("vampiric embrace", { NextAction("vampiric embrace", 16.0f) }));
triggers.push_back(new TriggerNode(
"party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11),
NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) }));
@ -56,6 +54,12 @@ void PriestBuffStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode("prayer of fortitude on party",
{ NextAction("prayer of fortitude on party", 12.0f) }));
triggers.push_back(
new TriggerNode("prayer of spirit on party",
{ NextAction("prayer of spirit on party", 14.0f) }));
triggers.push_back(
new TriggerNode("power word: fortitude on party",
{ NextAction("power word: fortitude on party", 11.0f) }));

View File

@ -30,6 +30,8 @@ public:
creators["flash heal"] = &flash_heal;
creators["flash heal on party"] = &flash_heal_on_party;
creators["circle of healing on party"] = &circle_of_healing;
creators["prayer of fortitude on party"] = &prayer_of_fortitude_on_party;
creators["prayer of spirit on party"] = &prayer_of_spirit_on_party;
}
private:
@ -132,6 +134,20 @@ private:
/*A*/ {},
/*C*/ {});
}
static ActionNode* prayer_of_fortitude_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("prayer of fortitude on party",
/*P*/ { NextAction("remove shadowform") },
/*A*/ { NextAction("power word: fortitude on party") },
/*C*/ {});
}
static ActionNode* prayer_of_spirit_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("prayer of spirit on party",
/*P*/ { NextAction("remove shadowform") },
/*A*/ { NextAction("divine spirit on party") },
/*C*/ {});
}
};
#endif

View File

@ -51,6 +51,14 @@ void ShadowPriestStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
}
)
);
triggers.push_back(
new TriggerNode(
"vampiric embrace",
{
NextAction("vampiric embrace", 16.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"silence",

View File

@ -8,9 +8,10 @@
#include "Player.h"
#include "Playerbots.h"
bool ShadowProtectionTrigger::IsActive()
bool PowerWordFortitudeOnPartyTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("prayer of shadow protection", GetTarget());
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("power word : fortitude", GetTarget()) &&
!botAI->HasAura("prayer of fortitude", GetTarget());
}
bool PowerWordFortitudeTrigger::IsActive()
@ -19,12 +20,43 @@ bool PowerWordFortitudeTrigger::IsActive()
!botAI->HasAura("prayer of fortitude", GetTarget());
}
bool DivineSpiritOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
!botAI->HasAura("prayer of spirit", GetTarget());
}
bool DivineSpiritTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
!botAI->HasAura("prayer of spirit", GetTarget());
}
bool PrayerOfFortitudeTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsPlayer())
return false;
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of fortitude", GetTarget()) &&
botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) &&
botAI->GetBuffedCount((Player*)GetTarget(), "prayer of fortitude") < 4 &&
!botAI->GetBuffedCount((Player*)GetTarget(), "power word: fortitude");
}
bool PrayerOfSpiritTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsPlayer())
return false;
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of spirit", GetTarget()) &&
botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) &&
// botAI->GetManaPercent() > 50 &&
botAI->GetBuffedCount((Player*)GetTarget(), "prayer of spirit") < 4 &&
!botAI->GetBuffedCount((Player*)GetTarget(), "divine spirit");
}
bool InnerFireTrigger::IsActive()
{
Unit* target = GetTarget();

View File

@ -27,6 +27,8 @@ BUFF_TRIGGER_A(InnerFireTrigger, "inner fire");
BUFF_TRIGGER_A(ShadowformTrigger, "shadowform");
BOOST_TRIGGER(PowerInfusionTrigger, "power infusion");
BUFF_TRIGGER(InnerFocusTrigger, "inner focus");
BUFF_TRIGGER(ShadowProtectionTrigger, "shadow protection");
BUFF_PARTY_TRIGGER(ShadowProtectionOnPartyTrigger, "shadow protection");
CC_TRIGGER(ShackleUndeadTrigger, "shackle undead");
INTERRUPT_TRIGGER(SilenceTrigger, "silence");
INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence");
@ -42,34 +44,20 @@ SNARE_TRIGGER(ChastiseTrigger, "chastise");
BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend");
class ShadowProtectionTrigger : public BuffTrigger
{
public:
ShadowProtectionTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "shadow protection", 4 * 2000) {}
bool IsActive() override;
};
class ShadowProtectionOnPartyTrigger : public BuffOnPartyTrigger
{
public:
ShadowProtectionOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "shadow protection", 4 * 2000) {}
};
class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger
{
public:
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) {}
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000)
{
}
bool IsActive() override;
};
class PowerWordFortitudeTrigger : public BuffTrigger
{
public:
PowerWordFortitudeTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
PowerWordFortitudeTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
bool IsActive() override;
};
@ -77,15 +65,31 @@ public:
class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger
{
public:
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
bool IsActive() override;
};
class DivineSpiritTrigger : public BuffTrigger
{
public:
DivineSpiritTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "divine spirit", 4 * 2000) {}
DivineSpiritTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine spirit", 4 * 2000) {}
bool IsActive() override;
};
class PrayerOfFortitudeTrigger : public BuffOnPartyTrigger
{
public:
PrayerOfFortitudeTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of fortitude", 3 * 2000) {}
bool IsActive() override;
};
class PrayerOfSpiritTrigger : public BuffOnPartyTrigger
{
public:
PrayerOfSpiritTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of spirit", 2 * 2000) {}
bool IsActive() override;
};
@ -102,7 +106,9 @@ class MindSearChannelCheckTrigger : public Trigger
{
public:
MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2)
: Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) {}
: Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies)
{
}
bool IsActive() override;

View File

@ -1462,6 +1462,15 @@ bool HodirBitingColdJumpAction::Execute(Event /*event*/)
// float speed = 7.96f;
// UpdateMovementState();
// if (!IsMovingAllowed(mapId, x, y, z))
//{
// return false;
// }
// MovementPriority priority;
// if (IsWaitingForLastMove(priority))
//{
// return false;
// }
// MotionMaster& mm = *bot->GetMotionMaster();
// mm.Clear();

View File

@ -331,11 +331,8 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI)
{
// Yield to attack-anything ONLY if a mob needed by this exact
// quest+objective is right next to us. The broad variant (any
// quest in the log) yielded for every nearby mob and derailed
// turn-ins / cross-zone travel through other quests' clusters.
if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx))
// yield to attack-anything if a quest mob is right next to us
if (HasNearbyQuestMob(15.0f))
return false;
// Note: previously yielded ~10%/tick when any hostile was

View File

@ -48,19 +48,11 @@
bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
{
if (dest == WorldPosition())
{
EmitDebugMove("MoveFar", "empty-dest", 0.0f, 0.0f, 0.0f);
return false;
}
UpdateMovementState();
if (!IsMovingAllowed())
{
EmitDebugMove("MoveFar", "cant-move",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
// performance optimization
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
return false;
}
// Already-at-dest short-stop. Below targetPosRecalcDistance the
// move is effectively done — stop any active spline and clear
@ -76,12 +68,26 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
lastMove.clear();
}
bot->StopMoving();
EmitDebugMove("MoveFar", "arrived",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
// Let an in-flight spline finish before recomputing — prevents
// oscillation when re-resolve produces a slightly different endpoint.
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
{
float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
if (remaining > 10.0f)
{
EmitDebugMove("MoveFar", "spline-plan",
lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
return true;
}
}
}
// 10% lastPath reuse — if the cached path's endpoint is still
// close (within 10%) to the new dest, trim the cached path to
// the bot's current position via makeShortCut and re-dispatch.
@ -132,35 +138,33 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
float disToDest = bot->GetDistance(dest);
float dis = bot->GetExactDist(dest);
// Try the travel-node graph for cross-map or moves longer than the
// bot's sight distance; otherwise the chained mmap probe handles it.
// BGs skip the graph.
// Try the travel-node graph first for cross-map or > 50y moves;
// fall back to chained mmap probe otherwise. BGs skip the graph.
constexpr float TRAVELNODE_THRESHOLD = 50.0f;
bool tryNodes = sPlayerbotAIConfig.enableTravelNodes &&
!bot->InBattleground() &&
((bot->GetMapId() != dest.GetMapId()) ||
(dis > sPlayerbotAIConfig.sightDistance));
(dis > TRAVELNODE_THRESHOLD));
// Per-tick re-resolve (cmangos pattern). Rebuild the travel plan
// from the bot's CURRENT position every tick rather than caching
// a multi-step plan and advancing through it. Recovers naturally
// from knockback, off-route drift, mid-execution destination
// changes, and blocked waypoints. Cost: per-tick GetFullPath call;
// the lastPath cache (10% reuse block above) handles the common
// case where the cached path still ends near the same destination
// and avoids re-derivation.
// Ride the active node plan only if its dest still matches.
// A stale plan would steer the bot past a new target.
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
{
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
botAI->rpgInfo.ClearTravel();
else
return UpdateTravelPlan();
}
// PRIORITY: try the travel-node graph FIRST when the move is
// long enough to need it.
if (tryNodes)
{
if (botAI->rpgInfo.HasActiveTravelPlan() &&
botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
botAI->rpgInfo.ClearTravel();
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
{
// No `travelplan` label here — per-tick re-resolve calls
// StartTravelPlan every tick, which would whisper-spam.
// The executor emits per-step labels (TravelPlan:walk-start,
// TravelPlan:flight, TravelPlan:transport-*) on actual dispatch.
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
}
// Graph returned no plan — fall through to mmap probe.
@ -213,10 +217,26 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
// Walk the chained probe's full waypoint chain via DispatchPathPoints.
if (!probe.empty() && probe.size() >= 2)
{
float endDistToDest = dest.GetExactDist(probe.back().GetPositionX(),
probe.back().GetPositionY(), probe.back().GetPositionZ());
WorldPosition stepDest = probe.back();
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
stepDest.GetPositionY(), stepDest.GetPositionZ());
if (endDistToDest + 5.0f < disToDest)
{
// Z gap check: if the probe's last waypoint is well below
// the requested destination Z, the chain walked the ground
// polygon graph toward an elevated target it can't reach
// (quest giver on top of Aldrassil etc.). Refuse to dispatch
// — bot waits instead of tunneling into the visual model.
// 10y tolerates normal terrain variation (ramp ends, hill
// tops) while still catching clearly unreachable elevations.
if (std::fabs(stepDest.GetPositionZ() - dest.GetPositionZ()) > 10.0f)
{
EmitDebugMove("MoveFar", "z-mismatch",
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
return false;
}
Movement::PointsArray points;
points.reserve(probe.size());
for (auto const& wp : probe)
@ -233,23 +253,39 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
}
}
// Probe failed or didn't progress. Attempt straight-line MoveTo to
// the destination — engine PathFinder handles per-poly filtering and
// the bot's STEEP/water filter is honored via CreateFilter. If even
// that fails, the engine falls back to a direct spline.
if (bot->GetMapId() != dest.GetMapId())
// Probe failed or didn't progress — emit visibility whisper so
// the user can see WHY mmap didn't dispatch.
{
EmitDebugMove("MoveFar", "cross-map",
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
(dest.GetExactDist(probe.back().GetPositionX(),
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
if (!probeProgressed)
{
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
EmitDebugMove("MoveFar", reason,
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
}
}
// Empty-probe fallback: single-waypoint MoveTo via engine PathGenerator.
// Cross-map can't be served by a single-map spline — bail.
if (bot->GetMapId() != dest.GetMapId())
return false;
// LOS gate: don't air-walk through trees/walls when the engine
// would otherwise drop to a straight-line BuildShortcut spline.
if (!bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()))
{
EmitDebugMove("MoveFar", "spline-blocked",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
EmitDebugMove("MoveFar", reason,
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ(), false, false, false, false);
EmitDebugMove("MoveFar", "spline",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
false, false, false, false);
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
@ -288,7 +324,52 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
return false;
}
// Save planner output for next-tick reuse.
// Sparse-segment clip (cmangos parity): if any consecutive segment
// is longer than ~11.18y, truncate the path at that point. Short,
// dense waypoints reduce spline interpolation across visual
// obstacles between sparse points; bot re-plans from a closer
// position next tick.
{
constexpr float SPARSE_SEG_SQ = 125.0f; // sqrt(125) ≈ 11.18y
for (size_t i = 1; i < points.size(); ++i)
{
float dx = points[i].x - points[i - 1].x;
float dy = points[i].y - points[i - 1].y;
float dz = points[i].z - points[i - 1].z;
if (dx * dx + dy * dy + dz * dz > SPARSE_SEG_SQ)
{
points.resize(i);
break;
}
}
if (points.size() < 2)
return false;
}
// LOS gate: reject paths whose segments pass through visual
// geometry. mmap is blind to M2 models (trees, decorative props)
// and will route through them; vmap LOS catches the cases that
// matter — solid trunks, walls, terrain features.
if (Map* map = bot->GetMap())
{
float const eye = bot->GetCollisionHeight();
for (size_t i = 0; i + 1 < points.size(); ++i)
{
if (!map->isInLineOfSight(points[i].x, points[i].y, points[i].z + eye,
points[i + 1].x, points[i + 1].y, points[i + 1].z + eye,
bot->GetPhaseMask(),
LINEOFSIGHT_ALL_CHECKS,
VMAP::ModelIgnoreFlags::Nothing))
{
EmitDebugMove("MoveFar", "blocked",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
}
// Save planner output before clip/fixup so next-tick reuse sees
// the original intent, not a truncated tail.
{
LastMovement& lm = AI_VALUE(LastMovement&, "last movement");
std::vector<WorldPosition> wpts;
@ -298,6 +379,26 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
lm.setPath(TravelPath(wpts));
}
// Underwater fixup: lift submerged waypoints to the surface,
// unless the destination is itself underwater.
if (Map* map = bot->GetMap())
{
WorldPosition destWp = dest;
if (!destWp.isUnderWater())
{
for (auto& pt : points)
{
WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z);
if (wp.isUnderWater())
{
float surface = map->GetWaterLevel(pt.x, pt.y);
if (surface != INVALID_HEIGHT && surface > pt.z)
pt.z = surface;
}
}
}
}
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
@ -373,14 +474,17 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
}
}
// Match master's walk pace when they're walking and within 5y.
// Match master's walk pace when they're nearby and walking.
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (Player* master = botAI->GetMaster())
if (sPlayerbotAIConfig.walkDistance > 0.0f)
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < 5.0f)
if (Player* master = botAI->GetMaster())
{
moveMode = FORCED_MOVEMENT_WALK;
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < sPlayerbotAIConfig.walkDistance)
{
moveMode = FORCED_MOVEMENT_WALK;
}
}
}
@ -441,28 +545,53 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
if (!map)
return false;
// 8 angles around the target starting at the bot's preferred follow
// angle (group-aware spread). For each angle, ask the engine for a
// valid nearby ground point at the requested distance — that snaps
// to terrain/collision. LOS check ignores M2 models so long-distance
// NPCs through forested terrain still pass; the mmap probe in
// MoveFarTo is the authoritative reachability check.
float const followAngle = GetFollowAngle();
float const searchSize = bot->GetObjectSize();
// 8-angle deterministic iteration around the target. For each angle,
// validate the candidate against the navmesh with a strict ground-only
// filter (NAV_GROUND, exclude STEEP/WATER/MAGMA/SLIME). Reject if no
// valid poly within 5y XY+Z or if the snap drifts the Z by >10y.
// First angle that passes both LOS and navmesh-snap wins.
dtNavMeshQuery const* navMeshQuery =
map->GetMapCollisionData().GetMMapData().GetNavMeshQuery();
float const baseAngle = object->GetAngle(bot);
for (float step = 0.0f; step < 2.0f * static_cast<float>(M_PI);
step += static_cast<float>(M_PI) / 4.0f)
{
float const angle = followAngle + step;
float x = object->GetPositionX();
float y = object->GetPositionY();
float const angle = baseAngle + step;
float x = object->GetPositionX() + std::cos(angle) * distance;
float y = object->GetPositionY() + std::sin(angle) * distance;
float z = object->GetPositionZ();
object->GetNearPoint(bot, x, y, z, searchSize, distance, angle);
if (!bot->IsWithinLOS(x, y, z + bot->GetCollisionHeight(),
VMAP::ModelIgnoreFlags::M2))
// LOS check at eye height.
if (!bot->IsWithinLOS(x, y, z + bot->GetCollisionHeight()))
continue;
// Strict navmesh-snap validation (cmangos ClosestCorrectPoint port).
if (navMeshQuery)
{
dtQueryFilter filter;
filter.setIncludeFlags(NAV_GROUND);
filter.setExcludeFlags(NAV_GROUND_STEEP | NAV_WATER | NAV_MAGMA | NAV_SLIME);
float const point[VERTEX_SIZE] = { y, z, x };
float const extents[VERTEX_SIZE] = { 5.0f, 5.0f, 5.0f };
float closest[VERTEX_SIZE] = { 0.0f, 0.0f, 0.0f };
dtPolyRef polyRef = INVALID_POLYREF;
if (!dtStatusSucceed(navMeshQuery->findNearestPoly(
point, extents, &filter, &polyRef, closest)) ||
polyRef == INVALID_POLYREF)
continue;
float const snappedZ = closest[1];
if (std::fabs(snappedZ - z) > 10.0f)
continue;
x = closest[2];
y = closest[0];
z = snappedZ;
}
return MoveFarTo(WorldPosition(object->GetMapId(), x, y, z));
}
@ -471,15 +600,50 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, WorldObject* center)
{
float const distance = (0.4f + rand_norm() * 0.6f) * moveStep;
float const angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float const dx = bot->GetPositionX() + distance * cos(angle);
float const dy = bot->GetPositionY() + distance * sin(angle);
float const dz = bot->GetPositionZ();
if (IsWaitingForLastMove(priority))
return false;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority);
EmitDebugMove("MoveRandomNear", moved ? "mmap" : "fail", dx, dy, dz);
return moved;
Map* map = bot->GetMap();
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
// Retry random samples so one bad roll doesn't lock the bot in place.
for (int attempt = 0; attempt < 8; ++attempt)
{
float distance = (0.4f + rand_norm() * 0.6f) * moveStep;
float angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float dx = x + distance * cos(angle);
float dy = y + distance * sin(angle);
float dz = z;
PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK, /*forceDestination=*/false);
if (!path.reachable)
continue;
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
continue;
if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight()))
continue;
// Reject samples whose straight-line passes through visual
// obstacles (trees, models) that aren't in the navmesh. The
// smooth-path step can otherwise interpolate a waypoint inside
// a tree, making the bot visibly walk through it.
if (!bot->IsWithinLOS(dx, dy, dz))
continue;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority);
if (moved)
{
EmitDebugMove("MoveRandomNear", "mmap", dx, dy, dz);
return true;
}
}
EmitDebugMove("MoveRandomNear", "all-fail", x, y, z);
return false;
}
bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority)
@ -1420,79 +1584,6 @@ bool NewRpgBaseAction::HasNearbyQuestMob(float range)
return false;
}
bool NewRpgBaseAction::HasNearbyQuestMobForObjective(float range, uint32 questId, int32 objectiveIdx)
{
if (!questId)
return false;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
return false;
// Turn-in path: completed quest has no remaining mob objective.
if (bot->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE)
return false;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
uint32 neededCreatureEntry = 0;
uint32 neededItemId = 0;
if (objectiveIdx >= 0 && objectiveIdx < QUEST_OBJECTIVES_COUNT)
{
int32 entry = quest->RequiredNpcOrGo[objectiveIdx];
if (entry > 0 &&
qs.CreatureOrGOCount[objectiveIdx] < quest->RequiredNpcOrGoCount[objectiveIdx])
{
neededCreatureEntry = uint32(entry);
}
// Item objective sometimes lives in the same slot range.
if (objectiveIdx < QUEST_ITEM_OBJECTIVES_COUNT &&
quest->RequiredItemId[objectiveIdx] &&
qs.ItemCount[objectiveIdx] < quest->RequiredItemCount[objectiveIdx])
{
neededItemId = quest->RequiredItemId[objectiveIdx];
}
}
else if (objectiveIdx >= QUEST_OBJECTIVES_COUNT &&
objectiveIdx < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
int32 itemSlot = objectiveIdx - QUEST_OBJECTIVES_COUNT;
if (quest->RequiredItemId[itemSlot] &&
qs.ItemCount[itemSlot] < quest->RequiredItemCount[itemSlot])
{
neededItemId = quest->RequiredItemId[itemSlot];
}
}
if (!neededCreatureEntry && !neededItemId)
return false;
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible targets");
for (ObjectGuid guid : possibleTargets)
{
Creature* c = botAI->GetCreature(guid);
if (!c || !c->IsInWorld() || !c->IsAlive())
continue;
if (!(c->GetPhaseMask() & bot->GetPhaseMask()))
continue;
if (bot->GetDistance(c) > range)
continue;
if (neededCreatureEntry && c->GetEntry() == neededCreatureEntry)
return true;
if (neededItemId)
{
CreatureTemplate const* tmpl = c->GetCreatureTemplate();
if (tmpl && tmpl->lootid &&
LootTemplates_Creature.HaveQuestLootForPlayer(tmpl->lootid, bot))
return true;
}
}
return false;
}
ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit)
{

View File

@ -68,12 +68,6 @@ protected:
// travel so we yield to attack-anything instead of running past.
bool HasNearbyQuestMob(float range = 20.0f);
// Narrower variant: only yields for mobs needed by the SPECIFIC
// quest+objective the bot is currently working on. Without this,
// do-quest yields for any quest in the log, derailing turn-ins
// and cross-zone travel through other quests' mob clusters.
bool HasNearbyQuestMobForObjective(float range, uint32 questId, int32 objectiveIdx);
protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);

View File

@ -9,7 +9,7 @@ bool NewRpgOutdoorPvpAction::Execute(Event event)
botAI->rpgInfo.ChangeToIdle();
return false;
}
if (!bot->IsOutdoorPvPActive())
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive())
return false;
uint32 zoneId = bot->GetZoneId();
@ -113,6 +113,9 @@ OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCap
bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius)
{
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
return false;
// Randomly pause at the current spot before picking a new patrol point
if (urand(0, 2) == 0)
return ForceToWait(urand(3000, 6000));

View File

@ -500,21 +500,21 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const
switch (player->getClass())
{
case CLASS_PRIEST:
nonCombatEngine->addStrategiesNoInit("dps assist", "cure", "rshadow", nullptr);
nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr);
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_PROTECTION)
{
nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr);
if (player->GetLevel() >= 20)
nonCombatEngine->addStrategy("bsanc", false);
nonCombatEngine->addStrategy("bhealth", false);
else
nonCombatEngine->addStrategy("bmight", false);
nonCombatEngine->addStrategy("bdps", false);
}
else if (tab == PALADIN_TAB_HOLY)
nonCombatEngine->addStrategiesNoInit("dps assist", "bwisdom", "bcast", nullptr);
nonCombatEngine->addStrategiesNoInit("dps assist", "bmana", "bcast", nullptr);
else
nonCombatEngine->addStrategiesNoInit("dps assist", "bmight", "baoe", nullptr);
nonCombatEngine->addStrategiesNoInit("dps assist", "bdps", "baoe", nullptr);
nonCombatEngine->addStrategiesNoInit("cure", nullptr);
break;

View File

@ -5986,6 +5986,29 @@ void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot)
LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str());
}
uint32 PlayerbotAI::GetBuffedCount(Player* player, std::string const spellname)
{
uint32 bcount = 0;
if (Group* group = bot->GetGroup())
{
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member || !member->IsInWorld())
continue;
if (!member->IsInSameRaidWith(player))
continue;
if (HasAura(spellname, member, true))
bcount++;
}
}
return bcount;
}
int32 PlayerbotAI::GetNearGroupMemberCount(float dis)
{
int count = 1; // yourself

View File

@ -494,6 +494,7 @@ public:
void ImbueItem(Item* item, Unit* target);
void ImbueItem(Item* item);
void EnchantItemT(uint32 spellid, uint8 slot);
uint32 GetBuffedCount(Player* player, std::string const spellname);
int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance);
virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr);

View File

@ -720,10 +720,9 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
PathGenerator path(pathUnit);
// Source is a temp Creature, so CreateFilter's bot block doesn't
// fire — apply the same bot cost biases here so generated paths
// match what bots prefer at runtime (STEEP/water are reachable
// but not preferred).
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
// fire — apply the same bot rules here so generated paths match
// what bots can actually walk at runtime.
path.SetExcludeFlags(NAV_GROUND_STEEP);
path.SetNavTerrainCost(NAV_WATER, 10.0f);
auto result = getPathStepFrom(startPos, path);
@ -858,8 +857,8 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
PathGenerator path(pathUnit);
// Same reason as getPathStepFrom: temp-Creature source doesn't trip
// CreateFilter's bot block, so apply the bot cost biases manually.
path.SetNavTerrainCost(NAV_GROUND_STEEP, 5.0f);
// CreateFilter's bot block, so apply the bot rules manually.
path.SetExcludeFlags(NAV_GROUND_STEEP);
path.SetNavTerrainCost(NAV_WATER, 10.0f);
// Limit the pathfinding attempts

View File

@ -161,8 +161,18 @@ float TravelNodePath::getCost(Player* bot, uint32 cGold)
if (factionAnnoyance > 0)
modifier += 0.3 * factionAnnoyance; // For each level the whole path takes 10% longer.
}
if (getPathType() == TravelNodePathType::flyingMount)
{
if (!bot->IsAlive() || bot->GetLevel() < 70 || !bot->CanFly())
return -1.0f;
float flySpeed = bot->GetSpeed(MOVE_FLIGHT);
if (flySpeed < 1.0f)
flySpeed = 20.0f; // 280% base flying speed fallback
return (distance / flySpeed) * modifier;
}
}
else if (getPathType() == TravelNodePathType::flightPath)
else if (getPathType() == TravelNodePathType::flightPath || getPathType() == TravelNodePathType::flyingMount)
return -1.0f;
if (getPathType() != TravelNodePathType::walk)
@ -875,28 +885,33 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
continue;
}
if (nodePath->getPathType() == TravelNodePathType::areaTrigger)
if (nodePath->getPathType() == TravelNodePathType::portal ||
nodePath->getPathType() == TravelNodePathType::staticPortal) // Teleport to next node.
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_AREA_TRIGGER, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_AREA_TRIGGER, nodePath->getPathObject());
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_PORTAL, nodePath->getPathObject()); // Entry point
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_PORTAL, nodePath->getPathObject()); // Exit point
}
else if (nodePath->getPathType() == TravelNodePathType::staticPortal)
else if (nodePath->getPathType() == TravelNodePathType::transport) // Move onto transport
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_STATIC_PORTAL, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_STATIC_PORTAL, nodePath->getPathObject());
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TRANSPORT,
nodePath->getPathObject()); // Departure point
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TRANSPORT, nodePath->getPathObject()); // Arrival point
}
else if (nodePath->getPathType() == TravelNodePathType::transport)
else if (nodePath->getPathType() == TravelNodePathType::flightPath) // Use the flightpath
{
// Emit the transport's full waypoint route, not just board+exit.
// Intermediate points carry NODE_TRANSPORT type so the executor
// sees consecutive transport waypoints as one block (board at
// first, disembark at last).
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_TRANSPORT, nodePath->getPathObject());
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_FLIGHTPATH,
nodePath->getPathObject()); // Departure point
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); // Arrival point
}
else if (nodePath->getPathType() == TravelNodePathType::flightPath)
else if (nodePath->getPathType() == TravelNodePathType::teleportSpell)
{
// Full taxi waypoint route; same reasoning as transport.
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject());
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
}
else if (nodePath->getPathType() == TravelNodePathType::flyingMount)
{
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_FLYING_MOUNT, 0);
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_FLYING_MOUNT, 0);
}
else
{
@ -907,12 +922,14 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
path.pop_back();
if (path.size() > 1 && prevNode->isPortal() &&
nodePath->getPathType() != TravelNodePathType::areaTrigger &&
nodePath->getPathType() != TravelNodePathType::staticPortal)
nodePath->getPathType() != TravelNodePathType::portal &&
nodePath->getPathType() != TravelNodePathType::staticPortal) // Do not move to the area trigger if we
// don't plan to take the portal.
path.erase(path.begin());
if (path.size() > 1 && prevNode->isTransport() &&
nodePath->getPathType() != TravelNodePathType::transport)
nodePath->getPathType() !=
TravelNodePathType::transport) // Do not move to the transport if we aren't going to take it.
path.erase(path.begin());
travelPath.addPath(path, PathNodeType::NODE_PATH);
@ -1266,153 +1283,47 @@ bool TravelNodeMap::GetFullPath(TravelPlan& plan,
WorldPosition botPos, uint32 botZoneId,
WorldPosition destination, Unit* bot)
{
// Capture previous pathToStart from the about-to-be-reset plan so we
// can try cropPathTo to reuse it across the per-tick re-resolve.
std::vector<WorldPosition> prevPathToStart;
for (auto const& pt : plan.steps.GetPathRef())
{
if (pt.type == PathNodeType::NODE_PREPATH)
prevPathToStart.push_back(pt.point);
else
break; // PREPATH is always at the head
}
plan.Reset();
plan.destination = destination;
// mmap-probe first: if a 40-step probe makes meaningful progress,
// prefer it over the graph. Loosened from "reaches within spellDistance"
// because the strict gate falls through to graph routing whenever the
// probe stops a few yards short of the destination (e.g., bot can't
// reach the exact GO position, or destination is inside an area the
// probe can't fully enter). Graph paths come from DB-cached walk
// edges baked at offline generation time and can route through
// terrain that current mmaps treat as unwalkable.
//
// Accept the probe if EITHER:
// (a) it reaches within 30y of destination, OR
// (b) it makes >50% progress and got at least 30y total
// mmap-probe first: if a 40-step probe reaches dest, skip the
// graph entirely — a direct walk beats a node hop.
if (botPos.GetMapId() == destination.GetMapId())
{
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
if (probe.size() >= 2)
if (probe.size() >= 2 && destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
{
float const totalDist = botPos.distance(destination);
float const probeEndToDest = destination.distance(probe.back());
float const probeProgress = totalDist - probeEndToDest;
bool const closeEnough = probeEndToDest < 30.0f;
bool const meaningfulProgress = probeProgress > totalDist * 0.5f && probeProgress > 30.0f;
if (closeEnough || meaningfulProgress)
{
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
for (size_t i = 1; i < probe.size(); ++i)
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
return true;
}
plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH);
for (size_t i = 1; i < probe.size(); ++i)
plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH);
return true;
}
}
std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);
// K-nearest start + end node candidates (cmangos parity: K=5).
// Iterate combinations — first pair with a graph route wins. The
// single-nearest may have no route while the 2nd/3rd does.
constexpr uint32 K = 5;
auto pickKNearest = [&](WorldPosition pos, uint32 zoneId) -> std::vector<TravelNode*>
{
std::vector<TravelNode*> const& zoneNodes = GetNodesInZone(zoneId);
std::vector<TravelNode*> candidates(zoneNodes.begin(), zoneNodes.end());
if (candidates.empty())
{
// Fallback to per-map scan
for (TravelNode* n : nodes)
if (n && n->getPosition()->GetMapId() == pos.GetMapId())
candidates.push_back(n);
}
if (candidates.empty())
return {};
uint32 n = std::min<uint32>(K, candidates.size());
std::partial_sort(candidates.begin(), candidates.begin() + n, candidates.end(),
[pos](TravelNode* i, TravelNode* j) { return i->fDist(pos) < j->fDist(pos); });
candidates.resize(n);
return candidates;
};
// Find nearest nodes (zone-indexed, fast)
TravelNode* startNode = GetNearestNodeInZone(botPos, botZoneId);
if (!startNode)
startNode = GetNearestNodeOnMap(botPos);
uint32 destZone = sMapMgr->GetZoneId(PHASEMASK_NORMAL, destination);
std::vector<TravelNode*> startCandidates = pickKNearest(botPos, botZoneId);
std::vector<TravelNode*> endCandidates = pickKNearest(destination, destZone);
TravelNode* endNode = GetNearestNodeInZone(destination, destZone);
if (!endNode)
endNode = GetNearestNodeOnMap(destination);
if (startCandidates.empty() || endCandidates.empty())
if (!startNode || !endNode || startNode == endNode)
return false;
TravelNode* startNode = nullptr;
TravelNode* endNode = nullptr;
TravelNodeRoute route;
for (TravelNode* s : startCandidates)
{
for (TravelNode* e : endCandidates)
{
if (!s || !e || s == e)
continue;
if (!s->hasRouteTo(e))
continue;
TravelNodeRoute r = GetNodeRoute(s, e, nullptr);
if (r.isEmpty())
continue;
startNode = s;
endNode = e;
route = r;
break;
}
if (!route.isEmpty())
break;
}
if (route.isEmpty() || !startNode || !endNode)
if (!startNode->hasRouteTo(endNode))
return false;
WorldPosition startNodePos = *startNode->getPosition();
WorldPosition endNodePos = *endNode->getPosition();
// pathToStart: mmap-path from bot to the first node. Try cropping
// the previous pathToStart first (cmangos parity) — if it still
// reaches the chosen startNode within reactDistance we avoid a full
// re-probe. Falls back to fresh getPathTo if crop fails or invalid.
std::vector<WorldPosition> pathToStart;
if (!prevPathToStart.empty())
{
std::vector<WorldPosition> cropped = prevPathToStart;
bool ok = startNodePos.cropPathTo(cropped, sPlayerbotAIConfig.reactDistance);
if (ok && cropped.size() >= 2)
pathToStart = cropped;
}
if (pathToStart.empty() && bot && botPos.GetMapId() == startNodePos.GetMapId())
{
std::vector<WorldPosition> probe = botPos.getPathTo(startNodePos, bot);
if (probe.size() >= 2)
pathToStart = probe;
}
if (pathToStart.empty())
pathToStart = {botPos};
// pathToEnd: mmap-path from the last node to the destination.
// Single-map case: use bot's PathGenerator directly.
// Cross-map case: pass nullptr — getPathTo constructs a tempCreature
// on the destination's base map so we can pathfind there even though
// bot isn't loaded into it.
std::vector<WorldPosition> pathToEnd;
if (endNodePos.GetMapId() == destination.GetMapId())
{
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
std::vector<WorldPosition> probe = endNodePos.getPathTo(destination, pathBot);
if (probe.size() >= 2)
pathToEnd = probe;
}
if (pathToEnd.empty())
pathToEnd = {destination};
TravelNodeRoute route = GetNodeRoute(startNode, endNode, nullptr);
if (route.isEmpty())
return false;
std::vector<WorldPosition> pathToStart = {botPos};
std::vector<WorldPosition> pathToEnd = {destination};
plan.steps = route.BuildPath(pathToStart, pathToEnd, nullptr);
return !plan.steps.empty();
@ -1596,9 +1507,11 @@ void TravelNodeMap::generateStartNodes()
void TravelNodeMap::generateAreaTriggerNodes()
{
// Entrance nodes
for (auto const& itr : sObjectMgr->GetAllAreaTriggerTeleports())
{
AreaTriggerTeleport const& atEntry = itr.second;
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(itr.first);
if (!at)
continue;
@ -1608,6 +1521,7 @@ void TravelNodeMap::generateAreaTriggerNodes()
atEntry.target_Orientation);
std::string nodeName;
if (!outPos.isOverworld())
nodeName = outPos.getAreaName(false) + " entrance";
else if (!inPos.isOverworld())
@ -1618,10 +1532,12 @@ void TravelNodeMap::generateAreaTriggerNodes()
TravelNodeMap::instance().addNode(inPos, nodeName, true, true);
}
// Exit nodes + area-trigger link
// Exit nodes
for (auto const& itr : sObjectMgr->GetAllAreaTriggerTeleports())
{
AreaTriggerTeleport const& atEntry = itr.second;
AreaTrigger const* at = sObjectMgr->GetAreaTrigger(itr.first);
if (!at)
continue;
@ -1631,6 +1547,7 @@ void TravelNodeMap::generateAreaTriggerNodes()
atEntry.target_Orientation);
std::string nodeName;
if (!outPos.isOverworld())
nodeName = outPos.getAreaName(false) + " entrance";
else if (!inPos.isOverworld())
@ -1638,12 +1555,16 @@ void TravelNodeMap::generateAreaTriggerNodes()
else
nodeName = inPos.getAreaName(false) + " portal";
TravelNode* outNode = TravelNodeMap::instance().addNode(outPos, nodeName, true, true);
TravelNode* inNode = TravelNodeMap::instance().getNode(inPos, nullptr, 5.0f);
//TravelNode* entryNode = TravelNodeMap::instance().getNode(outPos, nullptr, 20.0f); // Entry side, portal exit. //not used, line marked for removal.
TravelNode* outNode = TravelNodeMap::instance().addNode(outPos, nodeName, true, true); // Exit size, portal exit.
TravelNode* inNode = TravelNodeMap::instance().getNode(inPos, nullptr, 5.0f); // Entry side, portal center.
// Portal link from area trigger to area trigger destination.
if (outNode && inNode)
{
TravelNodePath travelPath(0.1f, 3.0f, (uint8)TravelNodePathType::areaTrigger, itr.first, true);
TravelNodePath travelPath(0.1f, 3.0f, (uint8)TravelNodePathType::portal, itr.first, true);
travelPath.setPath({*inNode->getPosition(), *outNode->getPosition()});
inNode->setPathTo(outNode, travelPath);
}

View File

@ -39,11 +39,12 @@
//
// Edge types (TravelNodePathType):
// walk(1) — Walk via navmesh waypoints (stored in DB)
// areaTrigger(2) — AreaTrigger teleport (auto-discovered at startup)
// portal(2) — AreaTrigger teleport (auto-discovered at startup)
// transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT)
// flightPath(4) — Taxi flight between flight masters
// teleportSpell(5) — Spell-based teleport (e.g. mage portals)
// staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation)
// flyingMount (7) — Use Bots Flying mount to travel (Not currently enabled)
//
// On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared
// (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds
@ -90,13 +91,12 @@ enum class TravelNodePathType : uint8
{
none = 0,
walk = 1,
areaTrigger = 2,
portal = 2,
transport = 3,
flightPath = 4,
// value 5 (teleportSpell) reserved — no generator emits it and no
// consumer handles it. Re-add when a teleport-spell edge generator
// / executor handler returns.
staticPortal = 6
teleportSpell = 5,
staticPortal = 6,
flyingMount = 7
};
// A connection between two nodes.
@ -267,9 +267,10 @@ public:
bool isPortal()
{
for (auto const& link : *getLinks())
if (link.second->getPathType() == TravelNodePathType::areaTrigger ||
if (link.second->getPathType() == TravelNodePathType::portal ||
link.second->getPathType() == TravelNodePathType::staticPortal)
return true;
return false;
}
@ -413,12 +414,11 @@ enum class PathNodeType : uint8
NODE_PREPATH = 0,
NODE_PATH = 1,
NODE_NODE = 2,
NODE_AREA_TRIGGER = 3,
NODE_PORTAL = 3,
NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5,
// value 6 (NODE_TELEPORT) reserved — no consumer; re-add when a
// teleport-spell handler / generator returns.
NODE_STATIC_PORTAL = 7
NODE_TELEPORT = 6,
NODE_FLYING_MOUNT = 7
};
struct PathNodePoint
@ -564,7 +564,9 @@ struct TravelPlan
// Spline scratch (used by executor):
std::vector<G3D::Vector3> walkPoints;
uint32 expectedDuration{0}; // used to derive the lastMove delay
bool splineActive{false};
uint32 splineStartTime{0};
uint32 expectedDuration{0};
// Taxi scratch:
std::vector<uint32> route;
@ -577,6 +579,8 @@ struct TravelPlan
steps.clear();
stepIdx = 0;
walkPoints.clear();
splineActive = false;
splineStartTime = 0;
expectedDuration = 0;
route.clear();
}

View File

@ -83,6 +83,8 @@ bool PlayerbotAIConfig::Initialize()
sitDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.SitDelay", 20000);
returnDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.ReturnDelay", 2000);
lootDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.LootDelay", 1000);
minBotsForGreaterBuff = sConfigMgr->GetOption<int32>("AiPlayerbot.MinBotsForGreaterBuff", 3);
rpWarningCooldown = sConfigMgr->GetOption<int32>("AiPlayerbot.RPWarningCooldown", 30);
disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30);
disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption<int32>("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300);
@ -97,6 +99,7 @@ bool PlayerbotAIConfig::Initialize()
tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f);
meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f);
followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f);
walkDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WalkDistance", 5.0f);
whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f);
contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f);
aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f);
@ -113,32 +116,6 @@ bool PlayerbotAIConfig::Initialize()
highMana = sConfigMgr->GetOption<int32>("AiPlayerbot.HighMana", 65);
autoSaveMana = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoSaveMana", true);
saveManaThreshold = sConfigMgr->GetOption<int32>("AiPlayerbot.SaveManaThreshold", 60);
switch (sConfigMgr->GetOption<uint32>("AiPlayerbot.AutoGreaterBlessings", 1))
{
case 0:
autoGreaterBlessings = AutoPartyBuffMode::DISABLED;
break;
case 2:
autoGreaterBlessings = AutoPartyBuffMode::GROUP_OR_RAID;
break;
case 1:
default:
autoGreaterBlessings = AutoPartyBuffMode::RAID_ONLY;
break;
}
switch (sConfigMgr->GetOption<uint32>("AiPlayerbot.AutoPartyBuffs", 2))
{
case 0:
autoPartyBuffs = AutoPartyBuffMode::DISABLED;
break;
case 1:
autoPartyBuffs = AutoPartyBuffMode::RAID_ONLY;
break;
case 2:
default:
autoPartyBuffs = AutoPartyBuffMode::GROUP_OR_RAID;
break;
}
autoAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoAvoidAoe", true);
maxAoeAvoidRadius = sConfigMgr->GetOption<float>("AiPlayerbot.MaxAoeAvoidRadius", 15.0f);
LoadSet<std::set<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"),

View File

@ -40,13 +40,6 @@ enum class HealingManaEfficiency : uint8
SUPERIOR = 32
};
enum class AutoPartyBuffMode : uint8
{
DISABLED = 0,
RAID_ONLY = 1,
GROUP_OR_RAID = 2
};
enum NewRpgStatus : int
{
//Initial Status
@ -96,13 +89,11 @@ public:
bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
targetPosRecalcDistance, farDistance, healDistance, aggroDistance;
targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance;
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
uint32 lowMana, mediumMana, highMana;
bool autoSaveMana;
uint32 saveManaThreshold;
AutoPartyBuffMode autoGreaterBlessings;
AutoPartyBuffMode autoPartyBuffs;
bool autoAvoidAoe;
float maxAoeAvoidRadius;
std::set<uint32> aoeAvoidSpellWhitelist;
@ -155,6 +146,12 @@ public:
uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay;
bool randomBotJoinLfg;
// Buff system
// Min group size to use Greater buffs (Paladin, Mage, Druid). Default: 3
int32 minBotsForGreaterBuff;
// Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff. Default: 30
int32 rpWarningCooldown;
// Professions
bool enableFishingWithMaster;
uint32 classMatchingProfessionChance;