mirror of
https://github.com/mod-playerbots/mod-playerbots
synced 2025-11-29 15:58:20 +08:00
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.
493 lines
18 KiB
C++
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;
|
|
}
|