Files
mod-playerbots/src/strategy/actions/CheckMountStateAction.cpp
bashermens 610a032379 Bots fly/follow (movePoint core refactor), water walking fixes (#1825)
Closer the original solution, i dont wanna drift to much away without
really good reason. At this point i still see NPC and bots moving
through the levels or even falling out the levels here and there. I
verified whether the MovePoint signature changes and related params
itself in playerbots has anything todo with it, even when params are
hardcoded the behavior remains. It could be deeper problem, but for now
it seems core problem. Therefore i dont wanna change to much until the
dust has settled a bit in core itself.

I havent implemented moveTakeOff or moveLand which are basically
responsible for the transitions phases between ground and air visa
versa. I have version where i use takeOff for the animation, which means
moving vertical. For now i am gonna leave for what it is.

PS: also includes additional movement fix for AreaTriggerAction which we
missed first time after the core update on movements.

@Wishmaster117 Been testing and trying a lot in the background on find
solutions and causes. The general solutions remains removed some tweaks,
altered code here and there. With the general idea to keep closer to the
original code for now at least.

Testing:
- Class abilities: Slow fall (priest), Flight Form (druid) : Green
- BG: Green
- Swimming and walking shallow waters: Green
- Takeoff and land when following master: Green
- Boat and zeppelins: Green
- Flymount and ground walking: Green
- Water Walking (shaman), Path of Frost (DK): Green
- Water Walking (shaman), Path of Frost (DK) transisions; flying,
swimming, water walking: Green


Skipping pets when group water walking, path of frost, once core pathing
changes has settled more i will add it. Which is easy but i dont wanna
add more edge cases and code branches for now.
2025-11-16 22:39:46 +01:00

493 lines
18 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "CheckMountStateAction.h"
#include "BattleGroundTactics.h"
#include "BattlegroundEY.h"
#include "BattlegroundWS.h"
#include "Event.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "ServerFacade.h"
#include "SpellAuraEffects.h"
// Define the static map / init bool for caching bot preferred mount data globally
std::unordered_map<uint32, PreferredMountCache> CheckMountStateAction::mountCache;
bool CheckMountStateAction::preferredMountTableChecked = false;
MountData CollectMountData(const Player* bot)
{
MountData data;
for (auto& entry : bot->GetSpellMap())
{
uint32 spellId = entry.first;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo || spellInfo->Effects[0].ApplyAuraName != SPELL_AURA_MOUNTED)
continue;
if (entry.second->State == PLAYERSPELL_REMOVED || !entry.second->Active || spellInfo->IsPassive())
continue;
int32 effect1 = spellInfo->Effects[1].BasePoints;
int32 effect2 = spellInfo->Effects[2].BasePoints;
int32 speed = std::max(effect1, effect2);
// Update max speed if appropriate.
if (speed > data.maxSpeed)
data.maxSpeed = speed; // In BG, clamp max speed to 99 later; here we just store the maximum found.
// Determine index: flight if either effect has flight aura or specific mount ID.
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
// This mount is meant to autoscale from a 150% flyer
// up to a 280% as you train your flying skill up.
// 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 : 0;
data.allSpells[index][speed].push_back(spellId);
}
return data;
}
bool CheckMountStateAction::isUseful()
{
// Not useful when:
if (botAI->IsInVehicle() || bot->isDead() || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) ||
!bot->IsOutdoors() || bot->InArena())
return false;
master = GetMaster();
// Get shapeshift states, only applicable when there's a master
if (master)
{
botInShapeshiftForm = bot->GetShapeshiftForm();
masterInShapeshiftForm = master->GetShapeshiftForm();
}
// Not useful when in combat and not currently mounted / travel formed
if ((bot->IsInCombat() || botAI->GetState() == BOT_STATE_COMBAT) &&
!bot->IsMounted() && botInShapeshiftForm != FORM_TRAVEL && botInShapeshiftForm != FORM_FLIGHT && botInShapeshiftForm != FORM_FLIGHT_EPIC)
return false;
// 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)
float posZ = bot->GetPositionZ();
float groundLevel = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), posZ);
if (!bot->IsMounted() && !bot->HasWaterWalkAura() && posZ < groundLevel)
return false;
// Not useful when bot does not have mount strat and is not currently mounted
if (!GET_PLAYERBOT_AI(bot)->HasStrategy("mount", BOT_STATE_NON_COMBAT) && !bot->IsMounted())
return false;
// Not useful when level lower than minimum required
if (bot->GetLevel() < sPlayerbotAIConfig->useGroundMountAtMinLevel)
return false;
// Allow mounting while transformed only if the form allows it
if (bot->HasAuraType(SPELL_AURA_TRANSFORM) && bot->IsInDisallowedMountForm())
return false;
// BG Logic
if (bot->InBattleground())
{
// Do not use when carrying BG Flags
if (bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) || bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG) || bot->HasAura(BG_EY_NETHERSTORM_FLAG_SPELL))
return false;
// Only mount if BG starts in less than 30 sec
if (Battleground* bg = bot->GetBattleground())
if (bg->GetStatus() == STATUS_WAIT_JOIN && bg->GetStartDelayTime() > BG_START_DELAY_30S)
return false;
}
return true;
}
bool CheckMountStateAction::Execute(Event /*event*/)
{
// Determine if there are no attackers
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;
Unit* currentTarget = AI_VALUE(Unit*, "current target");
if (currentTarget)
{
float dismountDistance = CalculateDismountDistance();
float mountDistance = CalculateMountDistance();
float combatReach = bot->GetCombatReach() + currentTarget->GetCombatReach();
float distanceToTarget = bot->GetExactDist(currentTarget);
shouldDismount = (distanceToTarget <= dismountDistance + combatReach);
shouldMount = (distanceToTarget > mountDistance + combatReach);
}
else
{
shouldMount = true;
}
// If should dismount, or master (if any) is no longer in travel form, yet bot still is, remove the shapeshifts
if (shouldDismount ||
(masterInShapeshiftForm != FORM_TRAVEL && botInShapeshiftForm == FORM_TRAVEL) ||
(masterInShapeshiftForm != FORM_FLIGHT && botInShapeshiftForm == FORM_FLIGHT && master && !master->IsMounted()) ||
(masterInShapeshiftForm != FORM_FLIGHT_EPIC && botInShapeshiftForm == FORM_FLIGHT_EPIC && master && !master->IsMounted()))
botAI->RemoveShapeshift();
if (shouldDismount && bot->IsMounted())
{
Dismount();
return true;
}
bool inBattleground = bot->InBattleground();
// If there is a master and bot not in BG, follow master's mount state regardless of group leader
if (master && !inBattleground)
{
if (ShouldFollowMasterMountState(master, noAttackers, shouldMount))
return Mount();
else if (ShouldDismountForMaster(master) && bot->IsMounted())
{
Dismount();
return true;
}
return false;
}
// If there is no master or bot in BG
if ((!master || inBattleground) && !bot->IsMounted() &&
noAttackers && shouldMount && !bot->IsInCombat())
return Mount();
if (!bot->IsFlying() && shouldDismount && bot->IsMounted() &&
(enemy || dps || (!noAttackers && bot->IsInCombat())))
{
Dismount();
return true;
}
return false;
}
bool CheckMountStateAction::Mount()
{
// Remove current Shapeshift if need be
if (botInShapeshiftForm != FORM_TRAVEL &&
botInShapeshiftForm != FORM_FLIGHT &&
botInShapeshiftForm != FORM_FLIGHT_EPIC)
{
botAI->RemoveShapeshift();
botAI->RemoveAura("tree of life");
}
if (TryPreferredMount(master))
return true;
// Get bot mount data
MountData mountData = CollectMountData(bot);
int32 masterMountType = GetMountType(master);
int32 masterSpeed = CalculateMasterMountSpeed(master, mountData);
// Try shapeshift
if (TryForms(master, masterMountType, masterSpeed))
return true;
// Try random mount
auto spellsIt = mountData.allSpells.find(masterMountType);
if (spellsIt != mountData.allSpells.end())
{
auto& spells = spellsIt->second;
if (TryRandomMountFiltered(spells, masterSpeed))
return true;
}
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "mount");
if (!items.empty())
return UseItemAuto(*items.begin());
return false;
}
void CheckMountStateAction::Dismount()
{
if (bot->isMoving())
bot->StopMoving();
WorldPacket emptyPacket;
bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket);
}
bool CheckMountStateAction::TryForms(Player* master, int32 masterMountType, int32 masterSpeed) const
{
if (!master)
return false;
// If both master and bot are in matching forms or master is mounted with corresponding speed, nothing to do
else if
((masterInShapeshiftForm == FORM_TRAVEL && botInShapeshiftForm == FORM_TRAVEL) ||
((masterInShapeshiftForm == FORM_FLIGHT || (masterMountType == 1 && masterSpeed == 149)) && botInShapeshiftForm == FORM_FLIGHT) ||
((masterInShapeshiftForm == FORM_FLIGHT_EPIC || (masterMountType == 1 && masterSpeed == 279)) && botInShapeshiftForm == FORM_FLIGHT_EPIC))
return true;
// Check if master is in Travel Form and bot can do the same
if (botAI->CanCastSpell(SPELL_TRAVEL_FORM, bot, true) &&
masterInShapeshiftForm == FORM_TRAVEL && botInShapeshiftForm != FORM_TRAVEL)
{
botAI->CastSpell(SPELL_TRAVEL_FORM, bot);
return true;
}
// Check if master is in Flight Form or has a flying mount and bot can flight form
if (botAI->CanCastSpell(SPELL_FLIGHT_FORM, bot, true) &&
((masterInShapeshiftForm == FORM_FLIGHT && botInShapeshiftForm != FORM_FLIGHT) ||
(masterMountType == 1 && masterSpeed == 149)))
{
botAI->CastSpell(SPELL_FLIGHT_FORM, bot);
// Compensate speedbuff
bot->SetSpeed(MOVE_RUN, 2.5, true);
return true;
}
// Check if master is in Swift Flight Form or has an epic flying mount and bot can swift flight form
if (botAI->CanCastSpell(SPELL_SWIFT_FLIGHT_FORM, bot, true) &&
((masterInShapeshiftForm == FORM_FLIGHT_EPIC && botInShapeshiftForm != FORM_FLIGHT_EPIC) ||
(masterMountType == 1 && masterSpeed == 279)))
{
botAI->CastSpell(SPELL_SWIFT_FLIGHT_FORM, bot);
// Compensate speedbuff
bot->SetSpeed(MOVE_RUN, 3.8, true);
return true;
}
return false;
}
bool CheckMountStateAction::TryPreferredMount(Player* master) const
{
uint32 botGUID = bot->GetGUID().GetRawValue();
// Build cache (only once)
if (!preferredMountTableChecked)
{
// Verify preferred mounts table existance in the database
QueryResult checkTable = PlayerbotsDatabase.Query(
"SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_schema = 'acore_playerbots' AND table_name = 'playerbots_preferred_mounts')");
if (checkTable && checkTable->Fetch()[0].Get<uint32>() == 1)
{
preferredMountTableChecked = true;
// Cache all mounts of both types globally, for all entries
QueryResult result = PlayerbotsDatabase.Query("SELECT guid, spellid, type FROM playerbots_preferred_mounts");
if (result)
{
uint32 totalResults = 0;
while (auto row = result->Fetch())
{
uint32 guid = row[0].Get<uint32>();
uint32 spellId = row[1].Get<uint32>();
uint32 mountType = row[2].Get<uint32>();
if (mountType == 0)
mountCache[guid].groundMounts.push_back(spellId);
else if (mountType == 1)
mountCache[guid].flightMounts.push_back(spellId);
totalResults++;
result->NextRow();
}
LOG_INFO("playerbots", "Preferred mounts initialized | Total records: {}", totalResults);
}
}
else // If the SQL table is missing, log an error and return false
{
preferredMountTableChecked = true;
LOG_DEBUG("playerbots", "Preferred mounts SQL table playerbots_preferred_mounts does not exist!");
return false;
}
}
// Pick a random preferred mount from the selection, if available
uint32 chosenMountId = 0;
if (GetMountType(master) == 0 && !mountCache[botGUID].groundMounts.empty())
{
uint32 index = urand(0, mountCache[botGUID].groundMounts.size() - 1);
chosenMountId = mountCache[botGUID].groundMounts[index];
}
else if (GetMountType(master) == 1 && !mountCache[botGUID].flightMounts.empty())
{
uint32 index = urand(0, mountCache[botGUID].flightMounts.size() - 1);
chosenMountId = mountCache[botGUID].flightMounts[index];
}
// No suitable preferred mount found
if (chosenMountId == 0)
return false;
// Check if spell exists
if (!sSpellMgr->GetSpellInfo(chosenMountId))
{
LOG_ERROR("playerbots", "Preferred mount failed: Invalid spell {} | Bot Guid: {}", chosenMountId, botGUID);
return false;
}
// Required here as otherwise bots won't mount in BG's due to them constant moving
if (bot->isMoving())
bot->StopMoving();
// Check if spell can be cast - for now allow all, even if the bot does not have the actual mount
//if (botAI->CanCastSpell(mountId, botAI->GetBot()))
//{
botAI->CastSpell(chosenMountId, botAI->GetBot());
return true;
//}
LOG_DEBUG("playerbots", "Preferred mount failed! | Bot Guid: {}", botGUID);
return false;
}
bool CheckMountStateAction::TryRandomMountFiltered(const std::map<int32, std::vector<uint32>>& spells, int32 masterSpeed) const
{
for (auto const& pair : spells)
{
int32 currentSpeed = pair.first;
if ((masterSpeed > 59 && currentSpeed < 99) || (masterSpeed > 149 && currentSpeed < 279))
continue;
// Pick a random mount from the candidate group.
auto const& ids = pair.second;
if (!ids.empty())
{
// Required here as otherwise bots won't mount in BG's due to them constant moving
if (bot->isMoving())
bot->StopMoving();
uint32 index = urand(0, ids.size() - 1);
if (botAI->CanCastSpell(ids[index], bot))
{
botAI->CastSpell(ids[index], bot);
return true;
}
}
}
return false;
}
float CheckMountStateAction::CalculateDismountDistance() const
{
// Warrior bots should dismount far enough to charge (because it's important for generating some initial rage),
// a real player would be riding toward enemy mashing the charge key but the bots won't 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);
float baseDistance = isMelee ? sPlayerbotAIConfig->meleeDistance + 10.0f : sPlayerbotAIConfig->spellDistance + 10.0f;
return std::max(21.0f, baseDistance);
}
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 MountData& mountData) const
{
// Check riding skill and level requirements
int32 ridingSkill = bot->GetPureSkillValue(SKILL_RIDING);
int32 botLevel = bot->GetLevel();
if (ridingSkill <= 75 && botLevel < static_cast<int32>(sPlayerbotAIConfig->useFastGroundMountAtMinLevel))
return 59;
// If there is a master and bot not in BG, use master's aura effects.
if (master && !bot->InBattleground())
{
auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED);
if (!auraEffects.empty())
{
SpellInfo const* masterSpell = auraEffects.front()->GetSpellInfo();
int32 effect1 = masterSpell->Effects[1].BasePoints;
int32 effect2 = masterSpell->Effects[2].BasePoints;
return std::max(effect1, effect2);
}
else if (masterInShapeshiftForm == FORM_FLIGHT_EPIC)
return 279;
else if (masterInShapeshiftForm == FORM_FLIGHT)
return 149;
}
else
{
// Bots on their own.
int32 speed = mountData.maxSpeed;
if (bot->InBattleground() && speed > 99)
return 99;
return speed;
}
return 59;
}
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;
}