Refactor of the CheckMountStateAction class (#919)

* Update CheckMountStateAction.cpp

The refactoring focused on improving readability, reducing redundancy, and optimizing performance where possible.

* Update CheckMountStateAction.h

* Update CheckMountStateAction.cpp

* Update CheckMountStateAction.cpp

* Update CheckMountStateAction.cpp

* Update CheckMountStateAction.h

* Fix lowest level mount from config

* Properly fix

* Reduced masterInShapeshiftForm lookups to 1

Reduced masterInShapeshiftForm lookups to 1

* Fix for missing useFastGroundMountAtMinLevel

* Final commit: Ensure max 99 speed in BG
This commit is contained in:
SaW
2025-01-28 09:08:21 +01:00
committed by GitHub
parent a8d8f37019
commit f52f999c09
2 changed files with 241 additions and 200 deletions

View File

@@ -4,7 +4,6 @@
*/
#include "CheckMountStateAction.h"
#include "BattlegroundWS.h"
#include "Event.h"
#include "PlayerbotAI.h"
@@ -15,109 +14,81 @@
bool CheckMountStateAction::Execute(Event event)
{
bool noattackers = !AI_VALUE2(bool, "combat", "self target") || !AI_VALUE(uint8, "attacker count");
bool noAttackers = !AI_VALUE2(bool, "combat", "self target") || !AI_VALUE(uint8, "attacker count");
bool enemy = AI_VALUE(Unit*, "enemy player target");
bool dps = AI_VALUE(Unit*, "dps target");
bool shouldDismount = false;
bool shouldMount = false;
if (Unit* currentTarget = AI_VALUE(Unit*, "current target"))
Unit* currentTarget = AI_VALUE(Unit*, "current target");
if (currentTarget)
{
float dismount_distance;
float mount_distance;
if (PlayerbotAI::IsMelee(bot))
{
dismount_distance = sPlayerbotAIConfig->meleeDistance + 2.0f;
mount_distance = sPlayerbotAIConfig->meleeDistance + 10.0f;
}
else
{
dismount_distance = sPlayerbotAIConfig->spellDistance + 2.0f;
mount_distance = sPlayerbotAIConfig->spellDistance + 10.0f;
}
// warrior bots should dismount far enough to charge (because its important for generating some initial rage),
// a real player would be riding toward enemy mashing the charge key but the bots wont cast charge while mounted
if (CLASS_WARRIOR == bot->getClass())
dismount_distance = std::max(18.0f, dismount_distance);
// mount_distance should be >= 21 regardless of class, because when travelling a distance < 21 it takes longer
// to cast mount-spell than the time saved from the speed increase. At a distance of 21 both approaches take 3
// seconds:
// 21 / 7 = 21 / 14 + 1.5 = 3 (7 = dismounted speed 14 = epic-mount speed 1.5 = mount-spell cast time)
mount_distance = std::max(21.0f, mount_distance);
float dismountDistance = CalculateDismountDistance();
float mountDistance = CalculateMountDistance();
float combatReach = bot->GetCombatReach() + currentTarget->GetCombatReach();
float disToTarget = bot->GetExactDist(currentTarget);
shouldDismount = disToTarget <= dismount_distance + combatReach;
shouldMount = disToTarget > mount_distance + combatReach;
float distanceToTarget = bot->GetExactDist(currentTarget);
shouldDismount = distanceToTarget <= dismountDistance + combatReach;
shouldMount = distanceToTarget > mountDistance + combatReach;
}
else
{
shouldDismount = false;
shouldMount = true;
}
if (bot->IsMounted() && shouldDismount)
{
WorldPacket emptyPacket;
bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket);
Dismount();
return true;
}
// If there is a master and bot not in BG
Player* master = GetMaster();
if (master != nullptr && !bot->InBattleground())
if (master && !bot->InBattleground())
{
masterInShapeshiftForm = master->GetShapeshiftForm();
if (!bot->GetGroup() || bot->GetGroup()->GetLeaderGUID() != master->GetGUID())
return false;
auto masterInShapeshiftForm = master->GetShapeshiftForm();
// bool farFromMaster = sServerFacade->GetDistance2d(bot, master) > sPlayerbotAIConfig->sightDistance;
if ((master->IsMounted() || masterInShapeshiftForm == FORM_FLIGHT || masterInShapeshiftForm == FORM_FLIGHT_EPIC || masterInShapeshiftForm == FORM_TRAVEL)
&& !bot->IsMounted() && noattackers && shouldMount && !bot->IsInCombat() && botAI->GetState() != BOT_STATE_COMBAT)
{
if (ShouldFollowMasterMountState(master, noAttackers, shouldMount))
return Mount();
}
if ((!master->IsMounted() && masterInShapeshiftForm != FORM_FLIGHT && masterInShapeshiftForm != FORM_FLIGHT_EPIC && masterInShapeshiftForm != FORM_TRAVEL)
&& bot->IsMounted())
if (ShouldDismountForMaster(master))
{
WorldPacket emptyPacket;
bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket);
Dismount();
return true;
}
return false;
}
// For random bots
if (!bot->InBattleground() && !master)
// If there is no master and bot not in BG
if (!master && !bot->InBattleground())
{
if (!bot->IsMounted() && noattackers && shouldMount && !bot->IsInCombat())
if (!bot->IsMounted() && noAttackers && shouldMount && !bot->IsInCombat())
{
return Mount();
}
}
if (bot->InBattleground() && shouldMount && noattackers && !bot->IsInCombat() && !bot->IsMounted())
// If the bot is in BG
if (bot->InBattleground() && shouldMount && noAttackers && !bot->IsInCombat() && !bot->IsMounted())
{
// WSG Specific - Do not mount when carrying the flag
if (bot->GetBattlegroundTypeId() == BATTLEGROUND_WS)
{
BattlegroundWS* bg = (BattlegroundWS*)botAI->GetBot()->GetBattleground();
if (bot->HasAura(23333) || bot->HasAura(23335))
{
return false;
}
}
return Mount();
}
if (!bot->IsFlying() && shouldDismount && bot->IsMounted() && (enemy || dps || (!noattackers && bot->IsInCombat())))
if (!bot->IsFlying() && shouldDismount && bot->IsMounted() && (enemy || dps || (!noAttackers && bot->IsInCombat())))
{
WorldPacket emptyPacket;
bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket);
Dismount();
return true;
}
@@ -126,51 +97,32 @@ bool CheckMountStateAction::Execute(Event event)
bool CheckMountStateAction::isUseful()
{
// do not use on vehicle
if (botAI->IsInVehicle())
if (botAI->IsInVehicle() || bot->isDead() || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || !bot->IsOutdoors() || bot->InArena())
return false;
if (bot->isDead())
return false;
if (bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
return false;
if (!bot->IsOutdoors())
return false;
// in addition to checking IsOutdoors, also check whether bot is clipping below floor slightly because that will
// In addition to checking IsOutdoors, also check whether bot is clipping below floor slightly because that will
// cause bot to falsly indicate they are outdoors. This fixes bug where bot tries to mount indoors (which seems
// to mostly be an issue in tunnels of WSG and AV)
if (!bot->IsMounted() && bot->GetPositionZ() < bot->GetMapWaterOrGroundLevel(
bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()))
return false;
if (bot->InArena())
if (!bot->IsMounted() && bot->GetPositionZ() < bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()))
return false;
if (!GET_PLAYERBOT_AI(bot)->HasStrategy("mount", BOT_STATE_NON_COMBAT) && !bot->IsMounted())
return false;
bool firstmount = bot->GetLevel() >= sPlayerbotAIConfig->useGroundMountAtMinLevel;
if (!firstmount)
// Do not mount when level lower than minimum required
if (bot->GetLevel() < sPlayerbotAIConfig->useGroundMountAtMinLevel)
return false;
// Do not use with BG Flags
if (bot->HasAura(23333) || bot->HasAura(23335) || bot->HasAura(34976))
{
return false;
}
// Only mount if BG starts in less than 30 sec
if (bot->InBattleground())
{
if (Battleground* bg = bot->GetBattleground())
if (bg->GetStatus() == STATUS_WAIT_JOIN)
{
if (bg->GetStartDelayTime() > BG_START_DELAY_30S)
return false;
}
if (bg->GetStatus() == STATUS_WAIT_JOIN && bg->GetStartDelayTime() > BG_START_DELAY_30S)
return false;
}
return true;
@@ -178,40 +130,98 @@ bool CheckMountStateAction::isUseful()
bool CheckMountStateAction::Mount()
{
uint32 secondmount = sPlayerbotAIConfig->useFastGroundMountAtMinLevel;
if (bot->isMoving())
{
bot->StopMoving();
// bot->GetMotionMaster()->Clear();
// bot->GetMotionMaster()->MoveIdle();
}
Player* master = GetMaster();
botAI->RemoveShapeshift();
botAI->RemoveAura("tree of life");
int32 masterSpeed = 59;
int32 masterMountType = 0;
SpellInfo const* masterSpell = nullptr;
int32 masterSpeed = CalculateMasterMountSpeed(master);
bool hasSwiftMount = CheckForSwiftMount();
auto allSpells = GetAllMountSpells();
int32 masterMountType = GetMountType(master);
if (TryPreferredMount(master))
return true;
auto& spells = allSpells[masterMountType];
if (hasSwiftMount)
FilterMountsBySpeed(spells, masterSpeed);
// No preferred mount found (or invalid), continue with random mount selection
if (TryRandomMount(spells))
return true;
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "mount");
if (!items.empty())
return UseItemAuto(*items.begin());
return false;
}
float CheckMountStateAction::CalculateDismountDistance() const
{
// Warrior bots should dismount far enough to charge (because its important for generating some initial rage),
// a real player would be riding toward enemy mashing the charge key but the bots wont cast charge while mounted
bool isMelee = PlayerbotAI::IsMelee(bot);
float dismountDistance = isMelee ? sPlayerbotAIConfig->meleeDistance + 2.0f : sPlayerbotAIConfig->spellDistance + 2.0f;
return bot->getClass() == CLASS_WARRIOR ? std::max(18.0f, dismountDistance) : dismountDistance;
}
float CheckMountStateAction::CalculateMountDistance() const
{
// Mount distance should be >= 21 regardless of class, because when travelling a distance < 21 it takes longer
// to cast mount-spell than the time saved from the speed increase. At a distance of 21 both approaches take 3
// seconds:
// 21 / 7 = 21 / 14 + 1.5 = 3 (7 = dismounted speed 14 = epic-mount speed 1.5 = mount-spell cast time)
bool isMelee = PlayerbotAI::IsMelee(bot);
return std::max(21.0f, isMelee ? sPlayerbotAIConfig->meleeDistance + 10.0f : sPlayerbotAIConfig->spellDistance + 10.0f);
}
void CheckMountStateAction::Dismount()
{
WorldPacket emptyPacket;
bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket);
}
bool CheckMountStateAction::ShouldFollowMasterMountState(Player* master, bool noAttackers, bool shouldMount) const
{
bool isMasterMounted = master->IsMounted() || masterInShapeshiftForm == FORM_FLIGHT || masterInShapeshiftForm == FORM_FLIGHT_EPIC || masterInShapeshiftForm == FORM_TRAVEL;
return isMasterMounted && !bot->IsMounted() && noAttackers && shouldMount && !bot->IsInCombat() && botAI->GetState() != BOT_STATE_COMBAT;
}
bool CheckMountStateAction::ShouldDismountForMaster(Player* master) const
{
bool isMasterMounted = master->IsMounted() || masterInShapeshiftForm == FORM_FLIGHT || masterInShapeshiftForm == FORM_FLIGHT_EPIC || masterInShapeshiftForm == FORM_TRAVEL;
return !isMasterMounted && bot->IsMounted();
}
int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master) const
{
if (bot->GetPureSkillValue(SKILL_RIDING) <= 75 && bot->GetLevel() < sPlayerbotAIConfig->useFastGroundMountAtMinLevel)
return 59;
// If there ia a master and bot not in BG
if (master != nullptr && !bot->InBattleground())
{
auto masterInShapeshiftForm = master->GetShapeshiftForm();
auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED);
if (!auraEffects.empty())
{
SpellInfo const* masterSpell = auraEffects.front()->GetSpellInfo();
return std::max(masterSpell->Effects[1].BasePoints, masterSpell->Effects[2].BasePoints);
}
if (!master->GetAuraEffectsByType(SPELL_AURA_MOUNTED).empty())
{
masterSpell = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED).front()->GetSpellInfo();
masterSpeed = std::max(masterSpell->Effects[1].BasePoints, masterSpell->Effects[2].BasePoints);
}
else if (masterInShapeshiftForm == FORM_FLIGHT || masterInShapeshiftForm == FORM_FLIGHT_EPIC)
{
masterMountType = 1;
masterSpeed = (masterInShapeshiftForm == FORM_FLIGHT_EPIC) ? 279 : 149;
}
else if (masterInShapeshiftForm == FORM_FLIGHT_EPIC)
return 279;
else if (masterInShapeshiftForm == FORM_FLIGHT)
return 149;
}
else
// Bots on their own
{
masterSpeed = 59;
for (PlayerSpellMap::iterator itr = bot->GetSpellMap().begin(); itr != bot->GetSpellMap().end(); ++itr)
{
uint32 spellId = itr->first;
@@ -222,36 +232,54 @@ bool CheckMountStateAction::Mount()
if (itr->second->State == PLAYERSPELL_REMOVED || !itr->second->Active || spellInfo->IsPassive())
continue;
int32 effect = std::max(spellInfo->Effects[1].BasePoints, spellInfo->Effects[2].BasePoints);
if (effect > masterSpeed)
masterSpeed = effect;
int32 speed = std::max(spellInfo->Effects[1].BasePoints, spellInfo->Effects[2].BasePoints);
if (speed > 59)
{
// Ensure max speed of 99 in BG
if (bot->InBattleground())
return (speed > 99) ? 99 : speed;
return speed;
}
}
}
if (bot->GetPureSkillValue(SKILL_RIDING) <= 75 && bot->GetLevel() < secondmount)
masterSpeed = 59;
return 59;
}
if (bot->InBattleground() && masterSpeed > 99)
masterSpeed = 99;
bool hasSwiftMount = false;
// std::map<int32, std::vector<uint32> > spells;
std::map<uint32, std::map<int32, std::vector<uint32>>> allSpells;
for (PlayerSpellMap::iterator itr = bot->GetSpellMap().begin(); itr != bot->GetSpellMap().end(); ++itr)
bool CheckMountStateAction::CheckForSwiftMount() const
{
for (auto& [spellId, spellState] : bot->GetSpellMap())
{
uint32 spellId = itr->first;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo || spellInfo->Effects[0].ApplyAuraName != SPELL_AURA_MOUNTED)
continue;
if (itr->second->State == PLAYERSPELL_REMOVED || !itr->second->Active || spellInfo->IsPassive())
if (spellState->State == PLAYERSPELL_REMOVED || !spellState->Active || spellInfo->IsPassive())
continue;
int32 effect = std::max(spellInfo->Effects[1].BasePoints, spellInfo->Effects[2].BasePoints);
// if (effect < masterSpeed)
// continue;
if ((effect > 59 && spellInfo->Effects[1].ApplyAuraName != SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) ||
(effect > 149 && spellInfo->Effects[1].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED))
return true;
}
return false;
}
std::map<uint32, std::map<int32, std::vector<uint32>>> CheckMountStateAction::GetAllMountSpells() const
{
std::map<uint32, std::map<int32, std::vector<uint32>>> allSpells;
for (auto& [spellId, spellState] : bot->GetSpellMap())
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo || spellInfo->Effects[0].ApplyAuraName != SPELL_AURA_MOUNTED)
continue;
if (spellState->State == PLAYERSPELL_REMOVED || !spellState->Active || spellInfo->IsPassive())
continue;
int32 effect = std::max(spellInfo->Effects[1].BasePoints, spellInfo->Effects[2].BasePoints);
uint32 index = (spellInfo->Effects[1].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED ||
spellInfo->Effects[2].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED ||
// Winged Steed of the Ebon Blade
@@ -260,102 +288,100 @@ bool CheckMountStateAction::Mount()
// This incorrectly gets categorised as a ground mount, force this to flyer only.
// TODO: Add other scaling mounts here if they have the same issue, or adjust above
// checks so that they are all correctly detected.
spellInfo->Id == 54729)
? 1 // Flying Mount
: 0; // Ground Mount
if (index == 0 &&
std::max(spellInfo->Effects[EFFECT_1].BasePoints, spellInfo->Effects[EFFECT_2].BasePoints) > 59)
hasSwiftMount = true;
if (index == 1 &&
std::max(spellInfo->Effects[EFFECT_1].BasePoints, spellInfo->Effects[EFFECT_2].BasePoints) > 149)
hasSwiftMount = true;
spellInfo->Id == 54729) ? 1 : 0;
allSpells[index][effect].push_back(spellId);
}
return allSpells;
}
if (masterSpell)
bool CheckMountStateAction::TryPreferredMount(Player* master) const
{
static bool tableExists = false;
static bool tableChecked = false;
if (!tableChecked)
{
masterMountType = (masterSpell->Effects[1].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED ||
masterSpell->Effects[2].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED)
? 1
: 0;
// Check for preferred mounts table in db
QueryResult checkTable = PlayerbotsDatabase.Query(
"SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_schema = 'acore_playerbots' AND table_name = 'playerbots_preferred_mounts')");
tableExists = checkTable && checkTable->Fetch()[0].Get<uint32>() == 1;
tableChecked = true;
}
// Check for preferred mounts table in db
QueryResult checkTable = PlayerbotsDatabase.Query(
"SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_schema = 'acore_playerbots' AND table_name = 'playerbots_preferred_mounts')");
if (checkTable)
if (tableExists)
{
uint32 tableExists = checkTable->Fetch()[0].Get<uint32>();
if (tableExists == 1)
{
// Check for preferred mount entry
QueryResult result = PlayerbotsDatabase.Query(
// Check for preferred mount entry
QueryResult result = PlayerbotsDatabase.Query(
"SELECT spellid FROM playerbots_preferred_mounts WHERE guid = {} AND type = {}",
bot->GetGUID().GetCounter(), masterMountType);
bot->GetGUID().GetCounter(), GetMountType(master));
if (result)
{
std::vector<uint32> mounts;
do
{
Field* fields = result->Fetch();
uint32 spellId = fields[0].Get<uint32>();
mounts.push_back(spellId);
} while (result->NextRow());
uint32 index = urand(0, mounts.size() - 1);
// Validate spell ID
if (index < mounts.size() && sSpellMgr->GetSpellInfo(mounts[index]))
{
// TODO: May want to do checks for 'bot riding skill > skill required to ride the mount'
return botAI->CastSpell(mounts[index], bot);
}
}
}
}
// No preferred mount found (or invalid), continue with random mount selection
std::map<int32, std::vector<uint32>>& spells = allSpells[masterMountType];
if (hasSwiftMount)
{
for (auto i : spells)
if (result)
{
std::vector<uint32> ids = i.second;
for (auto itr : ids)
std::vector<uint32> mounts;
do
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr);
if (!spellInfo)
continue;
mounts.push_back(result->Fetch()[0].Get<uint32>());
} while (result->NextRow());
if (masterMountType == 0 && masterSpeed > 59 &&
std::max(spellInfo->Effects[EFFECT_1].BasePoints, spellInfo->Effects[EFFECT_2].BasePoints) < 99)
spells[59].clear();
if (masterMountType == 1 && masterSpeed > 149 &&
std::max(spellInfo->Effects[EFFECT_1].BasePoints, spellInfo->Effects[EFFECT_2].BasePoints) < 279)
spells[149].clear();
}
// Validate spell ID
// TODO: May want to do checks for 'bot riding skill > skill required to ride the mount'
uint32 index = urand(0, mounts.size() - 1);
if (index < mounts.size() && sSpellMgr->GetSpellInfo(mounts[index]))
return botAI->CastSpell(mounts[index], bot);
}
}
for (std::map<int32, std::vector<uint32>>::iterator i = spells.begin(); i != spells.end(); ++i)
{
std::vector<uint32>& ids = i->second;
uint32 index = urand(0, ids.size() - 1);
if (index >= ids.size())
continue;
return botAI->CastSpell(ids[index], bot);
;
}
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "mount");
if (!items.empty())
return UseItemAuto(*items.begin());
return false;
}
uint32 CheckMountStateAction::GetMountType(Player* master) const
{
if (!master)
return 0;
auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED);
if (!auraEffects.empty())
{
SpellInfo const* masterSpell = auraEffects.front()->GetSpellInfo();
return (masterSpell->Effects[1].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED ||
masterSpell->Effects[2].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) ? 1 : 0;
}
else if (masterInShapeshiftForm == FORM_FLIGHT || masterInShapeshiftForm == FORM_FLIGHT_EPIC)
return 1;
return 0;
}
void CheckMountStateAction::FilterMountsBySpeed(std::map<int32, std::vector<uint32>>& spells, int32 masterSpeed) const
{
for (auto& [speed, ids] : spells)
{
for (auto& id : ids)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(id);
if (!spellInfo)
continue;
if (masterSpeed > 59 && std::max(spellInfo->Effects[1].BasePoints, spellInfo->Effects[2].BasePoints) < 99)
spells[59].clear();
if (masterSpeed > 149 && std::max(spellInfo->Effects[1].BasePoints, spellInfo->Effects[2].BasePoints) < 279)
spells[149].clear();
}
}
}
bool CheckMountStateAction::TryRandomMount(const std::map<int32, std::vector<uint32>>& spells) const
{
for (const auto& [speed, ids] : spells)
{
if (ids.empty())
continue;
uint32 index = urand(0, ids.size() - 1);
if (index < ids.size())
return botAI->CastSpell(ids[index], bot);
}
return false;
}

View File

@@ -19,6 +19,21 @@ public:
bool isUseful() override;
bool isPossible() override { return true; }
bool Mount();
private:
ShapeshiftForm masterInShapeshiftForm;
float CalculateDismountDistance() const;
float CalculateMountDistance() const;
void Dismount();
bool ShouldFollowMasterMountState(Player* master, bool noAttackers, bool shouldMount) const;
bool ShouldDismountForMaster(Player* master) const;
int32 CalculateMasterMountSpeed(Player* master) const;
bool CheckForSwiftMount() const;
std::map<uint32, std::map<int32, std::vector<uint32>>> GetAllMountSpells() const;
bool TryPreferredMount(Player* master) const;
uint32 GetMountType(Player* master) const;
void FilterMountsBySpeed(std::map<int32, std::vector<uint32>>& spells, int32 masterSpeed) const;
bool TryRandomMount(const std::map<int32, std::vector<uint32>>& spells) const;
};
#endif