mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
53 Commits
0c9131692c
...
400c563e3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
400c563e3d | ||
|
|
7d26a5b81e | ||
|
|
80ea99c2f9 | ||
|
|
a352905de2 | ||
|
|
68e876a9e2 | ||
|
|
15aa83f340 | ||
|
|
a625b0522a | ||
|
|
e3eb060106 | ||
|
|
c6c3079f26 | ||
|
|
41c3c517e5 | ||
|
|
461253b39e | ||
|
|
cce4b19cb2 | ||
|
|
f94e4087aa | ||
|
|
8cfa286a17 | ||
|
|
3c8659a6e4 | ||
|
|
e8b4461d33 | ||
|
|
c04ee595de | ||
|
|
da0377766a | ||
|
|
11f1eda3e0 | ||
|
|
8bc988d731 | ||
|
|
f0ec70c3ea | ||
|
|
795384d1f8 | ||
|
|
d841b21250 | ||
|
|
d4699aff6f | ||
|
|
56d69bf075 | ||
|
|
c5ab22345e | ||
|
|
af82b4c296 | ||
|
|
2cc8e46571 | ||
|
|
2ecbd8d48e | ||
|
|
a9654d4ff3 | ||
|
|
ce9406d401 | ||
|
|
f82904db87 | ||
|
|
52b7120453 | ||
|
|
afab6fd814 | ||
|
|
5ccf9c9799 | ||
|
|
a322e54b0e | ||
|
|
bdd386714b | ||
|
|
e3ec6d01c6 | ||
|
|
8776868ae5 | ||
|
|
424df402e6 | ||
|
|
33a4e4b4b2 | ||
|
|
5c11a831b9 | ||
|
|
7de2d9ce3e | ||
|
|
f420e36599 | ||
|
|
955f61b1ff | ||
|
|
2e5fcd08bb | ||
|
|
749670fc30 | ||
|
|
4a2ead82f9 | ||
|
|
e5dacf8bed | ||
|
|
c7175d67c2 | ||
|
|
75493b5f89 | ||
|
|
f5745bd923 | ||
|
|
018d5e5933 |
@ -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
|
||||
#
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
@ -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
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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) }));
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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) }));
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user