mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp

3795 lines
136 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 "MovementActions.h"
#include <cmath>
#include <cstdlib>
#include <iomanip>
#include <sstream>
#include <string>
#include "Corpse.h"
#include "DBCStores.h"
#include "Event.h"
#include "FleeManager.h"
#include "G3D/Vector3.h"
#include "GameObject.h"
#include "LastMovementValue.h"
#include "LootObjectStack.h"
#include "Map.h"
#include "ModelIgnoreFlags.h"
#include "MotionMaster.h"
#include "MoveSpline.h"
#include "MoveSplineInitArgs.h"
#include "TravelNode.h"
#include "MovementGenerator.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "PathGenerator.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "Position.h"
#include "PositionValue.h"
#include "Random.h"
#include "ServerFacade.h"
#include "SharedDefines.h"
#include "SpellAuraEffects.h"
#include "SpellInfo.h"
#include "Stances.h"
#include "Timer.h"
#include "Transport.h"
#include "Unit.h"
#include "Vehicle.h"
#include "WaypointMovementGenerator.h"
MovementAction::MovementAction(PlayerbotAI* botAI, std::string const name) : Action(botAI, name)
{
bot = botAI->GetBot();
}
void MovementAction::EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra)
{
if (!botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
return;
auto resolveName = [&](ObjectGuid guid) -> std::string
{
if (!guid)
return "";
if (WorldObject* obj = botAI->GetWorldObject(guid))
return obj->GetName();
return "";
};
NewRpgInfo& info = botAI->rpgInfo;
NewRpgStatus status = info.GetStatus();
char const* statusName =
status == RPG_IDLE ? "idle" :
status == RPG_GO_GRIND ? "go-grind" :
status == RPG_GO_CAMP ? "go-camp" :
status == RPG_WANDER_NPC ? "wander-npc" :
status == RPG_WANDER_RANDOM ? "wander-random" :
status == RPG_REST ? "rest" :
status == RPG_DO_QUEST ? "do-quest" :
status == RPG_TRAVEL_FLIGHT ? "travel-flight" :
status == RPG_OUTDOOR_PVP ? "outdoor-pvp" : "?";
// Resolve a human-readable target name from the RPG context. When
// we can name the target (quest objective, wander NPC, flight
// master, travel-node hop, etc.), it replaces the loc=(x,y,z)
// field — names are far more useful than coordinates. When no
// target can be named (combat moves, follow, flee, ad-hoc), we
// fall through to loc=(x,y,z).
std::string targetName;
switch (status)
{
case RPG_DO_QUEST:
if (auto* data = std::get_if<NewRpgInfo::DoQuest>(&info.data))
{
if (data->quest)
{
bool turnIn = data->questId &&
bot->GetQuestStatus(data->questId) == QUEST_STATUS_COMPLETE;
if (turnIn)
{
std::ostringstream t;
t << "turn-in:" << data->quest->GetTitle() << "(" << data->questId << ")";
targetName = t.str();
}
else
{
Quest const* q = data->quest;
QuestStatusData const& qs = bot->getQuestStatusMap().at(data->questId);
std::string goal;
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = q->RequiredNpcOrGo[i];
if (entry != 0 && qs.CreatureOrGOCount[i] < q->RequiredNpcOrGoCount[i])
{
if (entry > 0)
{
if (CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(entry))
goal = "mob:" + ct->Name;
}
else
{
if (GameObjectTemplate const* gt = sObjectMgr->GetGameObjectTemplate(-entry))
goal = "go:" + gt->name;
}
break;
}
uint32 item = q->RequiredItemId[i];
if (item && bot->GetItemCount(item, true) < q->RequiredItemCount[i])
{
if (ItemTemplate const* it = sObjectMgr->GetItemTemplate(item))
goal = "item:" + it->Name1;
break;
}
}
if (goal.empty())
{
std::ostringstream t;
t << "quest:" << q->GetTitle() << "(" << data->questId << ")";
goal = t.str();
}
targetName = goal;
}
}
}
break;
case RPG_WANDER_NPC:
if (auto* data = std::get_if<NewRpgInfo::WanderNpc>(&info.data))
{
std::string n = resolveName(data->npcOrGo);
if (!n.empty())
targetName = "npc:" + n;
}
break;
case RPG_TRAVEL_FLIGHT:
if (auto* data = std::get_if<NewRpgInfo::TravelFlight>(&info.data))
{
if (CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(data->flightMasterEntry))
targetName = "flightmaster:" + ct->Name;
}
break;
case RPG_GO_GRIND: targetName = "grind-pos"; break;
case RPG_GO_CAMP: targetName = "camp-pos"; break;
case RPG_WANDER_RANDOM: targetName = "wander-random"; break;
default: break;
}
// Travel-plan override: when actively routing through the node
// graph, prefer the next-hop node name over any RPG-level target.
if (info.HasActiveTravelPlan())
{
TravelPlan const& plan = info.travelPlan;
if (plan.stepIdx < plan.steps.GetPathRef().size())
{
PathNodePoint const& pnt = plan.steps.GetPathRef()[plan.stepIdx];
if (pnt.type == PathNodeType::NODE_NODE || pnt.type == PathNodeType::NODE_PATH)
{
if (TravelNode* n = sTravelNodeMap.getNode(pnt.point, nullptr, 5.0f))
targetName = "node:" + n->getName();
}
}
}
float dis = bot->GetExactDist(x, y, z);
std::ostringstream out;
out << "[M] | " << method
<< " | " << (generator && *generator ? generator : "-")
<< " | " << statusName
<< " | " << std::fixed << std::setprecision(2) << dis << " yard"
<< " | " << (targetName.empty() ? "-" : targetName.c_str());
if (extra && *extra)
out << " | " << extra;
botAI->TellMasterNoFacing(out);
}
void MovementAction::CreateWp(Player* wpOwner, float x, float y, float z, float o, uint32 entry, bool important)
{
float dist = wpOwner->GetDistance(x, y, z);
float delay = 1000.0f * dist / wpOwner->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay;
// if (!important)
// delay *= 0.25;
Creature* wpCreature = wpOwner->SummonCreature(entry, x, y, z - 1, o, TEMPSUMMON_TIMED_DESPAWN, delay);
botAI->GetBot()->AddAura(246, wpCreature);
if (!important)
wpCreature->SetObjectScale(0.5f);
}
bool MovementAction::JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority)
{
UpdateMovementState();
if (!IsMovingAllowed())
return false;
if (IsDuplicateMove(x, y, z))
return false;
if (IsWaitingForLastMove(priority))
return false;
float speed = bot->GetSpeed(MOVE_RUN);
MotionMaster& mm = *bot->GetMotionMaster();
mm.Clear();
mm.MoveJump(x, y, z, speed, speed, 1);
AI_VALUE(LastMovement&, "last movement").Set(mapId, x, y, z, bot->GetOrientation(), 1000, priority);
return true;
}
bool MovementAction::MoveNear(uint32 mapId, float x, float y, float z, float distance, MovementPriority priority)
{
float angle = GetFollowAngle();
EmitDebugMove("MoveNear", "mmap", x, y, z);
return MoveTo(mapId, x + cos(angle) * distance, y + sin(angle) * distance, z, false, false, false, false, priority);
}
bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPriority priority)
{
if (!target)
return false;
distance += target->GetCombatReach();
float followAngle = GetFollowAngle();
for (float angle = followAngle; angle <= followAngle + static_cast<float>(2 * M_PI);
angle += static_cast<float>(M_PI / 4.f))
{
float x = target->GetPositionX() + cos(angle) * distance;
float y = target->GetPositionY() + sin(angle) * distance;
float z = target->GetPositionZ();
// Clamp Z to the terrain under the offset point so we don't
// hand PointMovementGenerator a Z that matches the target's
// floor but not the sampled (x,y) — avoids straight-line
// fallbacks through geometry.
bot->UpdateAllowedPositionZ(x, y, z);
if (!bot->IsWithinLOS(x, y, z))
continue;
bool moved = MoveTo(target->GetMapId(), x, y, z, false, false, false, false, priority);
if (moved)
return true;
}
return false;
}
bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
{
if (!target)
return false;
float x = target->GetPositionX();
float y = target->GetPositionY();
float z = target->GetPositionZ();
EmitDebugMove("MoveToLOS", "mmap", x, y, z);
// Use standard PathGenerator to find a route.
PathResult path = GeneratePath(x, y, z, DEFAULT_PATH_ACCEPT_MASK, false);
if (!path.reachable)
return false;
if (!ranged)
return MoveTo((Unit*)target, target->GetCombatReach());
float dist = FLT_MAX;
PositionInfo dest;
if (!path.points.empty())
{
for (auto& point : path.points)
{
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
CreateWp(bot, point.x, point.y, point.z, 0.0, 2334);
float distPoint = target->GetDistance(point.x, point.y, point.z);
if (distPoint < dist && target->IsWithinLOS(point.x, point.y, point.z + bot->GetCollisionHeight()))
{
dist = distPoint;
dest.Set(point.x, point.y, point.z, target->GetMapId());
if (ranged)
break;
}
}
}
if (dest.isSet())
return MoveTo(dest.mapId, dest.x, dest.y, dest.z);
else
botAI->TellError("All paths not in LOS");
return false;
}
bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool react, bool normal_only,
bool exact_waypoint, MovementPriority priority, bool lessDelay, bool backwards)
{
UpdateMovementState();
if (!IsMovingAllowed())
{
return false;
}
if (IsDuplicateMove(x, y, z))
{
return false;
}
if (IsWaitingForLastMove(priority))
{
return false;
}
bool generatePath = !bot->IsFlying() && !bot->isSwimming();
bool disableMoveSplinePath =
sPlayerbotAIConfig.disableMoveSplinePath >= 2 ||
(sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground());
if (Vehicle* vehicle = bot->GetVehicle())
{
VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot);
Unit* vehicleBase = vehicle->GetBase();
generatePath = !vehicleBase || !vehicleBase->CanFly();
if (!vehicleBase || !seat || !seat->CanControl()) // is passenger and cant move anyway
return false;
float distance = vehicleBase->GetExactDist(x, y, z); // use vehicle distance, not bot
if (distance > 0.01f)
{
DoMovePoint(vehicleBase, x, y, z, generatePath, backwards);
float speed = backwards ? vehicleBase->GetSpeed(MOVE_RUN_BACK) : vehicleBase->GetSpeed(MOVE_RUN);
float delay = 1000.0f * (distance / speed);
if (lessDelay)
{
delay -= botAI->GetReactDelay();
}
delay = std::max(.0f, delay);
delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay);
AI_VALUE(LastMovement&, "last movement").Set(mapId, x, y, z, bot->GetOrientation(), delay, priority);
return true;
}
}
else if (exact_waypoint || disableMoveSplinePath || !generatePath)
{
float distance = bot->GetExactDist(x, y, z);
if (distance > 0.01f)
{
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
// if (bot->IsNonMeleeSpellCast(true))
// {
// bot->CastStop();
// botAI->InterruptSpell();
// }
DoMovePoint(bot, x, y, z, generatePath, backwards);
float delay = 1000.0f * MoveDelay(distance, backwards);
if (lessDelay)
{
delay -= botAI->GetReactDelay();
}
delay = std::max(.0f, delay);
delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay);
AI_VALUE(LastMovement&, "last movement").Set(mapId, x, y, z, bot->GetOrientation(), delay, priority);
return true;
}
}
else
{
// Direct dispatch — engine MovePoint(generatePath=true) handles
// pathfinding. Avoid ±z probes: their "shortest path" preference
// can pick an unreachable ledge and air-walk via NOPATH fallback.
float distance = bot->GetExactDist(x, y, z);
if (distance > 0.01f)
{
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
DoMovePoint(bot, x, y, z, generatePath, backwards);
float delay = 1000.0f * MoveDelay(distance, backwards);
if (lessDelay)
delay -= botAI->GetReactDelay();
delay = std::max(.0f, delay);
delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay);
AI_VALUE(LastMovement&, "last movement")
.Set(mapId, x, y, z, bot->GetOrientation(), delay, priority);
return true;
}
}
return false;
//
// // LOG_DEBUG("playerbots", "IsMovingAllowed {}", IsMovingAllowed());
// bot->AddUnitMovementFlag()
// bool isVehicle = false;
// Unit* mover = bot;
// if (Vehicle* vehicle = bot->GetVehicle())
// {
// VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot);
// LOG_DEBUG("playerbots", "!seat || !seat->CanControl() {}", !seat || !seat->CanControl());
// if (!seat || !seat->CanControl())
// return false;
// isVehicle = true;
// mover = vehicle->GetBase();
// }
// bool detailedMove = botAI->AllowActivity(DETAILED_MOVE_ACTIVITY);
// if (!detailedMove)
// {
// time_t now = time(nullptr);
// if (AI_VALUE(LastMovement&, "last movement").nextTeleport > now) // We can not teleport yet. Wait.
// {
// LOG_DEBUG("playerbots", "AI_VALUE(LastMovement&, \"last movement\").nextTeleport > now");
// botAI->SetNextCheckDelay((AI_VALUE(LastMovement&, "last movement").nextTeleport - now) * 1000);
// return true;
// }
// }
// float minDist = sPlayerbotAIConfig.targetPosRecalcDistance; //Minium distance a bot should move.
// float maxDist = sPlayerbotAIConfig.reactDistance; //Maxium distance a bot can move in one single
// action. float originalZ = z; // save original destination height to check
// if bot needs to fly up
// bool generatePath = !bot->IsFlying() && !bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING) && !bot->IsInWater() &&
// !bot->IsUnderWater(); if (generatePath)
// {
// z += CONTACT_DISTANCE;
// mover->UpdateAllowedPositionZ(x, y, z);
// }
// if (!isVehicle && !IsMovingAllowed() && bot->isDead())
// {
// bot->StopMoving();
// LOG_DEBUG("playerbots", "!isVehicle && !IsMovingAllowed() && bot->isDead()");
// return false;
// }
// if (!isVehicle && bot->isMoving() && !IsMovingAllowed())
// {
// if (!bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
// bot->StopMoving();
// LOG_DEBUG("playerbots", "!isVehicle && bot->isMoving() && !IsMovingAllowed()");
// return false;
// }
// LastMovement& lastMove = *context->GetValue<LastMovement&>("last movement");
// WorldPosition startPosition = WorldPosition(bot); //Current location of the bot
// WorldPosition endPosition = WorldPosition(mapId, x, y, z, 0); //The requested end location
// WorldPosition movePosition; //The actual end location
// float totalDistance = startPosition.distance(endPosition); //Total distance to where we want to go
// float maxDistChange = totalDistance * 0.1; //Maximum change between previous destination
// before needing a recalulation
// if (totalDistance < minDist)
// {
// if (lastMove.lastMoveShort.distance(endPosition) < maxDistChange)
// AI_VALUE(LastMovement&, "last movement").clear();
// mover->StopMoving();
// LOG_DEBUG("playerbots", "totalDistance < minDist");
// return false;
// }
// TravelPath movePath;
// if (lastMove.lastMoveShort.distance(endPosition) < maxDistChange &&
// startPosition.distance(lastMove.lastMoveShort) < maxDist) //The last short movement was to the same place we want
// to move now.
// movePosition = endPosition;
// else if (!lastMove.lastPath.empty() && lastMove.lastPath.getBack().distance(endPosition) < maxDistChange) //The
// last long movement was to the same place we want to move now.
// {
// movePath = lastMove.lastPath;
// }
// else
// {
// movePosition = endPosition;
// if (startPosition.GetMapId() != endPosition.GetMapId() || totalDistance > maxDist)
// {
// if (!TravelNodeMap::instance().getNodes().empty() && !bot->InBattleground())
// {
// if (sPlayerbotAIConfig.tweakValue)
// {
// if (lastMove.future.valid())
// {
// movePath = lastMove.future.get();
// }
// else
// {
// lastMove.future = std::async(&TravelNodeMap::getFullPath, startPosition, endPosition, bot);
// LOG_DEBUG("playerbots", "lastMove.future = std::async(&TravelNodeMap::getFullPath,
// startPosition, endPosition, bot);"); return true;
// }
// }
// else
// movePath = TravelNodeMap::instance().getFullPath(startPosition, endPosition, bot);
// if (movePath.empty())
// {
// //We have no path. Beyond 450yd the standard PathGenerator will probably move the wrong way.
// if (ServerFacade::instance().IsDistanceGreaterThan(totalDistance, maxDist * 3))
// {
// movePath.clear();
// movePath.addPoint(endPosition);
// AI_VALUE(LastMovement&, "last movement").setPath(movePath);
// bot->StopMoving();
// if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
// botAI->TellMasterNoFacing("I have no path");
// LOG_DEBUG("playerbots", "ServerFacade::instance().IsDistanceGreaterThan(totalDistance, maxDist * 3)");
// return false;
// }
// movePosition = endPosition;
// }
// }
// else
// {
// //Use standard PathGenerator to find a route.
// movePosition = endPosition;
// }
// }
// }
// if (movePath.empty() && movePosition.distance(startPosition) > maxDist)
// {
// //Use standard PathGenerator to find a route.
// PathGenerator path(mover);
// path.CalculatePath(movePosition.GetPositionX(), movePosition.GetPositionY(), movePosition.GetPositionZ(), false);
// PathType type = path.GetPathType();
// Movement::PointsArray const& points = path.GetPath();
// movePath.addPath(startPosition.fromPointsArray(points));
// }
// if (!movePath.empty())
// {
// if (movePath.makeShortCut(startPosition, maxDist))
// if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
// botAI->TellMasterNoFacing("Found a shortcut.");
// if (movePath.empty())
// {
// AI_VALUE(LastMovement&, "last movement").setPath(movePath);
// if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
// botAI->TellMasterNoFacing("Too far from path. Rebuilding.");
// LOG_DEBUG("playerbots", "movePath.empty()");
// return true;
// }
// TravelNodePathType pathType;
// uint32 entry;
// movePosition = movePath.getNextPoint(startPosition, maxDist, pathType, entry);
// if (pathType == TravelNodePathType::portal) // && !botAI->isRealPlayer())
// {
// //Log bot movement
// if (sPlayerbotAIConfig.hasLog("bot_movement.csv"))
// {
// WorldPosition telePos;
// if (entry)
// {
// if (AreaTriggerTeleport const* at = sObjectMgr->GetAreaTriggerTeleport(entry))
// telePos = WorldPosition(at->target_mapId, at->target_X, at->target_Y, at->target_Z,
// at->target_Orientation);
// }
// else
// telePos = movePosition;
// std::ostringstream out;
// out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
// out << bot->GetName() << ",";
// if (telePos && telePos.GetExactDist(movePosition) > 0.001)
// startPosition.printWKT({ startPosition, movePosition, telePos }, out, 1);
// else
// startPosition.printWKT({ startPosition, movePosition }, out, 1);
// out << std::to_string(bot->getRace()) << ",";
// out << std::to_string(bot->getClass()) << ",";
// out << bot->GetLevel() << ",";
// out << (entry ? -1 : entry);
// sPlayerbotAIConfig.log("bot_movement.csv", out.str().c_str());
// }
// if (entry)
// {
// AI_VALUE(LastMovement&, "last area trigger").lastAreaTrigger = entry;
// }
// else
// {
// LOG_DEBUG("playerbots", "!entry");
// return bot->TeleportTo(movePosition.GetMapId(), movePosition.GetPositionX(), movePosition.GetPositionY(),
// movePosition.GetPositionZ(), movePosition.GetOrientation(), 0);
// }
// }
// if (pathType == TravelNodePathType::transport && entry)
// {
// if (!bot->GetTransport())
// {
// for (auto& transport : movePosition.getTransports(entry))
// if (movePosition.sqDistance2d(WorldPosition((WorldObject*)transport)) < 5 * 5)
// transport->AddPassenger(bot, true);
// }
// WaitForReach(100.0f);
// LOG_DEBUG("playerbots", "pathType == TravelNodePathType::transport && entry");
// return true;
// }
// if (pathType == TravelNodePathType::flightPath && entry)
// {
// if (TaxiPathEntry const* tEntry = sTaxiPathStore.LookupEntry(entry))
// {
// Creature* unit = nullptr;
// if (!bot->m_taxi.IsTaximaskNodeKnown(tEntry->from))
// {
// GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs");
// for (GuidVector::iterator i = npcs.begin(); i != npcs.end(); i++)
// {
// Creature* unit = bot->GetNPCIfCanInteractWith(*i, UNIT_NPC_FLAG_FLIGHTMASTER);
// if (!unit)
// continue;
// bot->GetSession()->SendLearnNewTaxiNode(unit);
// unit->SetFacingTo(unit->GetAngle(bot));
// }
// }
// uint32 botMoney = bot->GetMoney();
// if (botAI->HasCheat(BotCheatMask::gold))
// {
// bot->SetMoney(10000000);
// }
// bool goTaxi = bot->ActivateTaxiPathTo({ tEntry->from, tEntry->to }, unit, 1);
// if (botAI->HasCheat(BotCheatMask::gold))
// bot->SetMoney(botMoney);
// LOG_DEBUG("playerbots", "goTaxi");
// return goTaxi;
// }
// }
// // if (pathType == TravelNodePathType::teleportSpell && entry)
// // {
// // if (entry == 8690)
// // {
// // if (!bot->HasSpellCooldown(8690))
// // {
// // return botAI->DoSpecificAction("hearthstone", Event("move action"));
// // }
// // else
// // {
// // movePath.clear();
// // AI_VALUE(LastMovement&, "last movement").setPath(movePath);
// // LOG_DEBUG("playerbots", "bot->HasSpellCooldown(8690)");
// // return false;
// // }
// // }
// // }
// //if (!isTransport && bot->GetTransport())
// // bot->GetTransport()->RemovePassenger(bot);
// }
// AI_VALUE(LastMovement&, "last movement").setPath(movePath);
// if (!movePosition || movePosition.GetMapId() != bot->GetMapId())
// {
// movePath.clear();
// AI_VALUE(LastMovement&, "last movement").setPath(movePath);
// if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
// botAI->TellMasterNoFacing("No point. Rebuilding.");
// LOG_DEBUG("playerbots", "!movePosition || movePosition.GetMapId() != bot->GetMapId()");
// return false;
// }
// if (movePosition.distance(startPosition) > maxDist)
// {
// //Use standard pathfinder to find a route.
// PathGenerator path(mover);
// path.CalculatePath(movePosition.getX(), movePosition.getY(), movePosition.getZ(), false);
// PathType type = path.GetPathType();
// Movement::PointsArray const& points = path.GetPath();
// movePath.addPath(startPosition.fromPointsArray(points));
// TravelNodePathType pathType;
// uint32 entry;
// movePosition = movePath.getNextPoint(startPosition, maxDist, pathType, entry);
// }
// if (movePosition == WorldPosition())
// {
// movePath.clear();
// AI_VALUE(LastMovement&, "last movement").setPath(movePath);
// if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
// botAI->TellMasterNoFacing("No point. Rebuilding.");
// return false;
// }
// //Visual waypoints
// if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
// {
// if (!movePath.empty())
// {
// float cx = x;
// float cy = y;
// float cz = z;
// for (auto i : movePath.getPath())
// {
// CreateWp(bot, i.point.GetPositionX(), i.point.GetPositionY(), i.point.GetPositionZ(), 0.f, 2334);
// cx = i.point.GetPositionX();
// cy = i.point.GetPositionY();
// cz = i.point.GetPositionZ();
// }
// }
// else
// CreateWp(bot, movePosition.GetPositionX(), movePosition.GetPositionY(), movePosition.GetPositionZ(), 0, 2334, true);
// }
// //Log bot movement
// if (sPlayerbotAIConfig.hasLog("bot_movement.csv") && lastMove.lastMoveShort.GetExactDist(movePosition) > 0.001)
// {
// std::ostringstream out;
// out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
// out << bot->GetName() << ",";
// startPosition.printWKT({ startPosition, movePosition }, out, 1);
// out << std::to_string(bot->getRace()) << ",";
// out << std::to_string(bot->getClass()) << ",";
// out << bot->GetLevel();
// out << 0;
// sPlayerbotAIConfig.log("bot_movement.csv", out.str().c_str());
// }
// // LOG_DEBUG("playerbots", "({}, {}) -> ({}, {})", startPosition.GetPositionX(), startPosition.GetPositionY(),
// movePosition.GetPositionX(), movePosition.GetPositionY()); if (!react)
// if (totalDistance > maxDist)
// WaitForReach(startPosition.distance(movePosition) - 10.0f);
// else
// WaitForReach(startPosition.distance(movePosition));
// if (!isVehicle)
// {
// bot->HandleEmoteCommand(0);
// if (bot->IsSitState())
// bot->SetStandState(UNIT_STAND_STATE_STAND);
// if (bot->IsNonMeleeSpellCast(true))
// {
// bot->CastStop();
// botAI->InterruptSpell();
// }
// }
// /* Why do we do this?
// if (lastMove.lastMoveShort.distance(movePosition) < minDist)
// {
// bot->StopMoving();
// bot->GetMotionMaster()->Clear();
// }
// */
// // Clean movement if not already moving the same way.
// // if (mover->GetMotionMaster()->GetCurrentMovementGeneratorType() != POINT_MOTION_TYPE)
// // {
// // mover->StopMoving();
// // mover->GetMotionMaster()->Clear();
// // }
// // else
// // {
// // mover->GetMotionMaster()->GetDestination(x, y, z);
// // if (movePosition.distance(WorldPosition(movePosition.GetMapId(), x, y, z, 0)) > minDist)
// // {
// // mover->StopMoving();
// // mover->GetMotionMaster()->Clear();
// // }
// // }
// if (totalDistance > maxDist && !detailedMove && !botAI->HasPlayerNearby(&movePosition)) // Why walk if you can
// fly?
// {
// time_t now = time(nullptr);
// AI_VALUE(LastMovement&, "last movement").nextTeleport = now +
// (time_t)MoveDelay(startPosition.distance(movePosition)); LOG_DEBUG("playerbots", "totalDistance > maxDist &&
// !detailedMove && !botAI->HasPlayerNearby(&movePosition)"); return bot->TeleportTo(movePosition.GetMapId(),
// movePosition.GetPositionX(), movePosition.GetPositionY(), movePosition.GetPositionZ(), startPosition.getAngleTo(movePosition));
// }
// // walk if master walks and is close
// bool masterWalking = false;
// if (botAI->GetMaster())
// {
// if (botAI->GetMaster()->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_WALKING) &&
// ServerFacade::instance().GetDistance2d(bot, botAI->GetMaster()) < 20.0f)
// masterWalking = true;
// }
// if (masterWalking)
// bot->SetWalk(true);
// bot->SendMovementFlagUpdate();
// // LOG_DEBUG("playerbots", "normal move? {} {} {}",
// !bot->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) && !bot->HasAuraType(SPELL_AURA_FLY),
// // bot->HasUnitFlag(UNIT_FLAG_DISABLE_MOVE), bot->getStandState());
// if (!bot->HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) && !bot->HasAuraType(SPELL_AURA_FLY))
// {
// bot->SetWalk(masterWalking);
// bot->GetMotionMaster()->MovePoint(movePosition.GetMapId(), movePosition.GetPositionX(), movePosition.GetPositionY(),
// movePosition.GetPositionZ(), generatePath); WaitForReach(startPosition.distance(movePosition));
// // LOG_DEBUG("playerbots", "Movepoint to ({}, {})", movePosition.GetPositionX(), movePosition.GetPositionY());
// }
// else
// {
// bool needFly = false;
// bool needLand = false;
// bool isFly = bot->IsFlying();
// if (!isFly && originalZ > bot->GetPositionZ() && (originalZ - bot->GetPositionZ()) > 5.0f)
// needFly = true;
// if (needFly && !isFly)
// {
// WorldPacket data(SMSG_SPLINE_MOVE_SET_FLYING, 9);
// data << bot->GetPackGUID();
// bot->SendMessageToSet(&data, true);
// if (!bot->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_FLYING))
// bot->m_movementInfo.AddMovementFlag(MOVEMENTFLAG_FLYING);
// if (!bot->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY))
// bot->m_movementInfo.AddMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
// }
// if (isFly)
// {
// float ground = bot->GetPositionZ();
// float height = bot->GetMap()->GetWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(),
// bot->GetPositionZ(), ground); if (bot->GetPositionZ() > originalZ && (bot->GetPositionZ() - originalZ
// < 5.0f) && (fabs(originalZ - ground) < 5.0f))
// needLand = true;
// if (needLand)
// {
// WorldPacket data(SMSG_SPLINE_MOVE_UNSET_FLYING, 9);
// data << bot->GetPackGUID();
// bot->SendMessageToSet(&data, true);
// if (bot->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_FLYING))
// bot->m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_FLYING);
// if (bot->m_movementInfo.HasMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY))
// bot->m_movementInfo.RemoveMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
// }
// }
// bot->GetMotionMaster()->MovePoint(movePosition.GetMapId(), Position(movePosition.GetPositionX(), movePosition.GetPositionY(),
// movePosition.GetPositionZ(), 0.f)); WaitForReach(startPosition.distance(movePosition)); LOG_DEBUG("playerbots",
// "Movepoint to ({}, {})", movePosition.GetPositionX(), movePosition.GetPositionY());
// }
// AI_VALUE(LastMovement&, "last movement").setShort(movePosition);
// if (!idle)
// ClearIdleState();
// LOG_DEBUG("playerbots", "return true in the end");
// return true;
}
bool MovementAction::MoveTo(WorldObject* target, float distance, MovementPriority priority)
{
if (!IsMovingAllowed(target))
return false;
float bx = bot->GetPositionX();
float by = bot->GetPositionY();
float bz = bot->GetPositionZ();
float tz = target->GetPositionZ();
float distanceToTarget = bot->GetDistance(target);
float angle = bot->GetAngle(target);
float needToGo = distanceToTarget - distance;
float maxDistance = sPlayerbotAIConfig.spellDistance;
if (needToGo > 0 && needToGo > maxDistance)
needToGo = maxDistance;
else if (needToGo < 0 && needToGo < -maxDistance)
needToGo = -maxDistance;
float dx = cos(angle) * needToGo + bx;
float dy = sin(angle) * needToGo + by;
// Start from a seed Z between bot and target, then clamp to the
// terrain under (dx,dy). Linear interpolation alone ignores hills
// between the two units and fed PointMovementGenerator a Z that
// could be well above/below ground, triggering straight-line
// fallbacks through walls.
float dz;
if (distanceToTarget > CONTACT_DISTANCE)
dz = bz + (tz - bz) * (needToGo / distanceToTarget);
else
dz = tz;
bot->UpdateAllowedPositionZ(dx, dy, dz);
return MoveTo(target->GetMapId(), dx, dy, dz, false, false, false, false, priority);
}
bool MovementAction::ReachCombatTo(Unit* target, float distance)
{
if (!IsMovingAllowed(target))
return false;
float tx = target->GetPositionX();
float ty = target->GetPositionY();
float tz = target->GetPositionZ();
float targetOrientation = target->GetOrientation();
float deltaAngle = Position::NormalizeOrientation(targetOrientation - target->GetAngle(bot));
if (deltaAngle > M_PI)
deltaAngle -= 2.0f * M_PI; // -PI..PI
// if target is moving forward and moving far away, predict the position
bool behind = fabs(deltaAngle) > M_PI_2;
if (target->HasUnitMovementFlag(MOVEMENTFLAG_FORWARD) && behind)
{
float predictDis = std::min(3.0f, target->GetObjectSize() * 2);
tx += cos(target->GetOrientation()) * predictDis;
ty += sin(target->GetOrientation()) * predictDis;
if (!target->GetMap()->CheckCollisionAndGetValidCoords(target, target->GetPositionX(), target->GetPositionY(),
target->GetPositionZ(), tx, ty, tz))
{
tx = target->GetPositionX();
ty = target->GetPositionY();
tz = target->GetPositionZ();
}
}
float combatDistance = bot->GetCombatReach() + target->GetCombatReach();
distance += combatDistance;
if (bot->GetExactDist(tx, ty, tz) <= distance)
return false;
PathGenerator path(bot);
path.CalculatePath(tx, ty, tz, false);
PathType type = path.GetPathType();
int typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_SHORTCUT;
if (!(type & typeOk))
return false;
float shortenTo = distance;
// Avoid walking too far when moving towards each other
float disToGo = bot->GetExactDist(tx, ty, tz) - distance;
if (disToGo >= 6.0f)
shortenTo = disToGo / 2 + distance;
// if (bot->GetExactDist(tx, ty, tz) <= shortenTo)
// return false;
path.ShortenPathUntilDist(G3D::Vector3(tx, ty, tz), shortenTo);
G3D::Vector3 endPos = path.GetPath().back();
bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true);
// Only emit on a successful new commit — combat ticks call this
// many times per second and MoveTo internally suppresses while a
// prior spline is still playing. Emitting before the suppression
// check produces per-tick whisper spam.
if (moved)
EmitDebugMove("ReachCombatTo", "mmap", endPos.x, endPos.y, endPos.z);
return moved;
}
float MovementAction::GetFollowAngle()
{
Player* master = GetMaster();
Group* group = master ? master->GetGroup() : bot->GetGroup();
if (!group)
return 0.0f;
// Prevent bots with orphaned raid groups from dividing by 0, which freezes the server.
uint32 memberCount = group->GetMembersCount();
if (memberCount <= 1)
return 0.0f;
uint32 index = 1;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
if (ref->GetSource() == master)
continue;
if (ref->GetSource() == bot)
return 2 * M_PI / (memberCount - 1) * index;
++index;
}
return 0;
}
bool MovementAction::IsMovingAllowed(WorldObject* target)
{
if (!target)
return false;
if (bot->GetMapId() != target->GetMapId())
return false;
// float distance = ServerFacade::instance().GetDistance2d(bot, target);
// if (!bot->InBattleground() && distance > sPlayerbotAIConfig.reactDistance)
// return false;
return IsMovingAllowed();
}
bool MovementAction::IsDuplicateMove(float x, float y, float z)
{
LastMovement& lastMove = *context->GetValue<LastMovement&>("last movement");
// heuristic 5s
if (lastMove.msTime + sPlayerbotAIConfig.maxWaitForMove < getMSTime() ||
lastMove.lastMoveShort.GetExactDist(x, y, z) > 0.01f)
return false;
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();
}
bool MovementAction::Follow(Unit* target, float distance) { return Follow(target, distance, GetFollowAngle()); }
void MovementAction::UpdateMovementState()
{
const bool isCurrentlyRestricted = // see if the bot is currently slowed, rooted, or otherwise unable to move
bot->HasUnitState(UNIT_STATE_LOST_CONTROL) || bot->IsRooted() || bot->isFrozen() || bot->IsPolymorphed();
// no update movement flags while movement is current restricted.
if (!isCurrentlyRestricted && bot->IsAlive())
{
// state flags
const auto master = botAI ? botAI->GetMaster() : nullptr;
const auto liquidState = bot->GetLiquidData().Status;
const float gZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
const bool onGroundZ = bot->GetPositionZ() < gZ + 1.f;
const bool wantsSwim = liquidState == LIQUID_MAP_IN_WATER || liquidState == LIQUID_MAP_UNDER_WATER;
const bool wantsFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura();
const bool canWaterWalk = bot->HasWaterWalkAura();
const bool isMasterFlying = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) : true;
const bool isMasterSwimming = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING) : true;
const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING);
const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING);
const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
const bool hasGravityDisabled = bot->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
bool movementFlagsUpdated = false;
// handle water (fragile logic do not alter without testing every detail, animation and transition)
if (liquidState != LIQUID_MAP_NO_WATER && !isFlying)
{
if (canWaterWalk && !isMasterSwimming && !isWaterWalking)
{
bot->SetSwim(false);
bot->AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
movementFlagsUpdated = true;
}
else if ((!canWaterWalk || isMasterSwimming) && isWaterWalking)
{
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
if (wantsSwim)
bot->SetSwim(true);
movementFlagsUpdated = true;
}
else if (!wantsSwim && isSwimming)
{
bot->SetSwim(false);
movementFlagsUpdated = true;
}
}
// reset when not around water while swimming or water walking
if (liquidState == LIQUID_MAP_NO_WATER && (isSwimming || isWaterWalking))
{
bot->SetSwim(false);
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING);
movementFlagsUpdated = true;
}
// handle flying
if (wantsFly && !isFlying && isMasterFlying)
{
bot->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY);
bot->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
movementFlagsUpdated = true;
}
else if (!wantsFly && !isWaterWalking && (isFlying || hasGravityDisabled))
{
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY);
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING);
movementFlagsUpdated = true;
}
else if (!isMasterFlying && isFlying && onGroundZ)
{
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY);
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY);
bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING);
movementFlagsUpdated = true;
}
// detect if movement/CC restrictions have been ended, refresh movement state for animations.
if (wasMovementRestricted)
movementFlagsUpdated = true;
// movement flags should only be updated between state changes, if not it will break certain effects.
if (movementFlagsUpdated)
bot->SendMovementFlagUpdate();
}
// Save current state for the next check
wasMovementRestricted = isCurrentlyRestricted;
// Temporary speed increase in group
// if (botAI->HasRealPlayerMaster())
// {
// bot->SetSpeedRate(MOVE_RUN, 1.1f);
// }
// else
// {
// bot->SetSpeedRate(MOVE_RUN, 1.0f);
// }
// check if target is not reachable
// if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE && bot->CanNotReachTarget() &&
// !bot->InBattleground())
// {
// if (Unit* pTarget = ServerFacade::instance().GetChaseTarget(bot))
// {
// if (!bot->IsWithinMeleeRange(pTarget) && pTarget->IsCreature())
// {
// float angle = bot->GetAngle(pTarget);
// float distance = 5.0f;
// float x = bot->GetPositionX() + cos(angle) * distance;
// float y = bot->GetPositionY() + sin(angle) * distance;
// float z = bot->GetPositionZ();
// z += CONTACT_DISTANCE;
// bot->UpdateAllowedPositionZ(x, y, z);
// bot->StopMoving();
// bot->GetMotionMaster()->Clear();
// bot->NearTeleportTo(x, y, z, bot->GetOrientation());
// //bot->GetMotionMaster()->MovePoint(bot->GetMapId(), x, y, z, FORCED_MOVEMENT_RUN, false);
// return;
// /*if (pTarget->IsCreature() && !bot->isMoving() && bot->IsWithinDist(pTarget, 10.0f))
// {
// // Cheating to prevent getting stuck because of bad mmaps.
// bot->StopMoving();
// bot->GetMotionMaster()->Clear();
// bot->GetMotionMaster()->MovePoint(bot->GetMapId(), pTarget->GetPosition(), FORCED_MOVEMENT_RUN,
// false); return;
// }*/
// }
// }
// }
// if ((bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE ||
// bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == POINT_MOTION_TYPE) && bot->CanNotReachTarget()
// && !bot->InBattleground())
// {
// if (Unit* pTarget = ServerFacade::instance().GetChaseTarget(bot))
// {
// if (pTarget != botAI->GetGroupLeader())
// return;
// if (!bot->IsWithinMeleeRange(pTarget))
// {
// if (!bot->isMoving() && bot->IsWithinDist(pTarget, 10.0f))
// {
// // Cheating to prevent getting stuck because of bad mmaps.
// float angle = bot->GetAngle(pTarget);
// float distance = 5.0f;
// float x = bot->GetPositionX() + cos(angle) * distance;
// float y = bot->GetPositionY() + sin(angle) * distance;
// float z = bot->GetPositionZ();
// z += CONTACT_DISTANCE;
// bot->UpdateAllowedPositionZ(x, y, z);
// bot->StopMoving();
// bot->GetMotionMaster()->Clear();
// bot->NearTeleportTo(x, y, z, bot->GetOrientation());
// //bot->GetMotionMaster()->MovePoint(bot->GetMapId(), x, y, z, FORCED_MOVEMENT_RUN, false);
// return;
// }
// }
// }
// }
}
bool MovementAction::Follow(Unit* target, float distance, float angle)
{
UpdateMovementState();
if (!target)
return false;
if (!bot->InBattleground() && ServerFacade::instance().IsDistanceLessOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target),
sPlayerbotAIConfig.followDistance))
{
// botAI->TellError("No need to follow");
return false;
}
/*
if (!bot->InBattleground()
&& ServerFacade::instance().IsDistanceLessOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target->GetPositionX(),
target->GetPositionY()), sPlayerbotAIConfig.sightDistance)
&& abs(bot->GetPositionZ() - target->GetPositionZ()) >= sPlayerbotAIConfig.spellDistance &&
botAI->HasRealPlayerMaster()
&& (target->GetMapId() && bot->GetMapId() != target->GetMapId()))
{
bot->StopMoving();
bot->GetMotionMaster()->Clear();
float x = bot->GetPositionX();
float y = bot->GetPositionY();
float z = target->GetPositionZ();
if (target->GetMapId() && bot->GetMapId() != target->GetMapId())
{
if ((target->GetMap() && target->GetMap()->IsBattlegroundOrArena()) || (bot->GetMap() &&
bot->GetMap()->IsBattlegroundOrArena())) return false;
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(target->GetMapId(), x, y, z, bot->GetOrientation());
}
else
{
bot->Relocate(x, y, z, bot->GetOrientation());
}
AI_VALUE(LastMovement&, "last movement").Set(target);
ClearIdleState();
return true;
}
if (!IsMovingAllowed(target) && botAI->HasRealPlayerMaster())
{
if ((target->GetMap() && target->GetMap()->IsBattlegroundOrArena()) || (bot->GetMap() &&
bot->GetMap()->IsBattlegroundOrArena())) return false;
if (bot->isDead() && botAI->GetMaster()->IsAlive())
{
bot->ResurrectPlayer(1.0f, false);
botAI->TellMasterNoFacing("I live, again!");
}
else
botAI->TellError("I am stuck while following");
bot->CombatStop(true);
botAI->TellMasterNoFacing("I will there soon.");
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(target->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(),
target->GetOrientation()); return false;
}
*/
// Move to target corpse if alive.
if (!target->IsAlive() && bot->IsAlive() && target->GetGUID().IsPlayer())
{
Player* pTarget = (Player*)target;
Corpse* corpse = pTarget->GetCorpse();
if (corpse)
{
WorldPosition botPos(bot);
WorldPosition cPos(corpse);
if (botPos.fDist(cPos) > sPlayerbotAIConfig.spellDistance)
return MoveTo(cPos.GetMapId(), cPos.GetPositionX(), cPos.GetPositionY(), cPos.GetPositionZ());
}
}
if (ServerFacade::instance().IsDistanceGreaterOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target),
sPlayerbotAIConfig.sightDistance))
{
EmitDebugMove("Follow", "mmap", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ());
if (target->GetGUID().IsPlayer())
{
Player* pTarget = (Player*)target;
PlayerbotAI* targetBotAI = GET_PLAYERBOT_AI(pTarget);
if (targetBotAI) // Try to move to where the bot is going if it is closer and in the same direction.
{
WorldPosition botPos(bot);
WorldPosition tarPos(target);
WorldPosition longMove =
targetBotAI->GetAiObjectContext()->GetValue<WorldPosition>("last long move")->Get();
if (longMove)
{
float lDist = botPos.fDist(longMove);
float tDist = botPos.fDist(tarPos);
float ang = botPos.getAngleBetween(tarPos, longMove);
if ((lDist * 1.5 < tDist && ang < static_cast<float>(M_PI) / 2) ||
target->HasUnitState(UNIT_STATE_IN_FLIGHT))
{
return MoveTo(longMove.GetMapId(), longMove.GetPositionX(), longMove.GetPositionY(), longMove.GetPositionZ());
}
}
}
else
{
if (pTarget->HasUnitState(UNIT_STATE_IN_FLIGHT)) // Move to where the player is flying to.
{
TaxiPathNodeList const& tMap =
static_cast<FlightPathMovementGenerator*>(pTarget->GetMotionMaster()->top())->GetPath();
if (!tMap.empty())
{
auto tEnd = tMap.back();
if (tEnd)
return MoveTo(tEnd->mapid, tEnd->x, tEnd->y, tEnd->z);
}
}
}
}
if (!target->HasUnitState(UNIT_STATE_IN_FLIGHT))
return MoveTo(target, sPlayerbotAIConfig.followDistance);
}
if (ServerFacade::instance().IsDistanceLessOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target),
sPlayerbotAIConfig.followDistance))
{
// botAI->TellError("No need to follow");
return false;
}
if (target->IsFriendlyTo(bot) && bot->IsMounted() && AI_VALUE(GuidVector, "all targets").empty())
distance += angle;
if (!bot->InBattleground() && ServerFacade::instance().IsDistanceLessOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target),
sPlayerbotAIConfig.followDistance))
{
// botAI->TellError("No need to follow");
return false;
}
bot->HandleEmoteCommand(0);
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
{
bot->CastStop();
botAI->InterruptSpell();
}
// AI_VALUE(LastMovement&, "last movement").Set(target);
ClearIdleState();
if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE)
{
Unit* currentTarget = ServerFacade::instance().GetChaseTarget(bot);
if (currentTarget && currentTarget->GetGUID() == target->GetGUID())
return false;
}
if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FOLLOW_MOTION_TYPE)
bot->GetMotionMaster()->Clear();
EmitDebugMove("Follow", "follow", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ());
bot->GetMotionMaster()->MoveFollow(target, distance, angle);
return true;
}
bool MovementAction::ChaseTo(WorldObject* obj, float distance)
{
if (!IsMovingAllowed())
return false;
if (obj)
EmitDebugMove("ChaseTo", "chase", obj->GetPositionX(), obj->GetPositionY(), obj->GetPositionZ());
if (Vehicle* vehicle = bot->GetVehicle())
{
VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot);
if (!seat || !seat->CanControl())
return false;
vehicle->GetBase()->GetMotionMaster()->MoveChase((Unit*)obj, 30.0f);
return true;
}
UpdateMovementState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
{
bot->CastStop();
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()->MoveChase((Unit*)obj, distance);
WaitForReach(bot->GetExactDist2d(obj) - distance);
return true;
}
float MovementAction::MoveDelay(float distance, bool backwards)
{
float speed;
if (bot->isSwimming())
{
speed = backwards ? bot->GetSpeed(MOVE_SWIM_BACK) : bot->GetSpeed(MOVE_SWIM);
}
else if (bot->IsFlying())
{
speed = backwards ? bot->GetSpeed(MOVE_FLIGHT_BACK) : bot->GetSpeed(MOVE_FLIGHT);
}
else
{
speed = backwards ? bot->GetSpeed(MOVE_RUN_BACK) : bot->GetSpeed(MOVE_RUN);
}
float delay = distance / speed;
return delay;
}
// TODO should this be removed? (or modified to use "last movement" value?)
void MovementAction::WaitForReach(float distance)
{
float delay = 1000.0f * MoveDelay(distance);
if (delay > sPlayerbotAIConfig.maxWaitForMove)
delay = sPlayerbotAIConfig.maxWaitForMove;
Unit* target = *botAI->GetAiObjectContext()->GetValue<Unit*>("current target");
Unit* player = *botAI->GetAiObjectContext()->GetValue<Unit*>("enemy player target");
if ((player || target) && delay > sPlayerbotAIConfig.globalCoolDown)
delay = sPlayerbotAIConfig.globalCoolDown;
if (delay < 0)
delay = 0;
botAI->SetNextCheckDelay((uint32)delay);
}
// similiar to botAI->SetNextCheckDelay() but only stops movement
void MovementAction::SetNextMovementDelay(float delayMillis)
{
AI_VALUE(LastMovement&, "last movement")
.Set(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetOrientation(),
delayMillis, MovementPriority::MOVEMENT_FORCED);
}
bool MovementAction::Flee(Unit* target)
{
Player* master = GetMaster();
if (!target)
target = master;
if (!target)
return false;
if (!sPlayerbotAIConfig.fleeingEnabled)
return false;
EmitDebugMove("Flee", "flee", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ());
if (!IsMovingAllowed())
{
botAI->TellError("I am stuck while fleeing");
return false;
}
bool foundFlee = false;
time_t lastFlee = AI_VALUE(LastMovement&, "last movement").lastFlee;
time_t now = time(0);
uint32 fleeDelay = urand(2, sPlayerbotAIConfig.returnDelay / 1000);
if (lastFlee)
{
if ((now - lastFlee) <= fleeDelay)
{
return false;
}
}
Unit* currentVictim = target->GetThreatMgr().GetCurrentVictim();
if (currentVictim && currentVictim == bot) // bot is target - try to flee to tank or master
{
if (Group* group = bot->GetGroup())
{
Unit* fleeTarget = nullptr;
float fleeDistance = sPlayerbotAIConfig.sightDistance;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* player = gref->GetSource();
if (!player || player == bot || !player->IsAlive())
continue;
if (botAI->IsTank(player))
{
float distanceToTank = ServerFacade::instance().GetDistance2d(bot, player);
if (distanceToTank < fleeDistance)
{
fleeTarget = player;
fleeDistance = distanceToTank;
}
}
}
if (fleeTarget)
foundFlee = MoveNear(fleeTarget);
if ((!fleeTarget || !foundFlee) && master)
{
foundFlee = MoveNear(master);
}
}
}
else // bot is not targeted, try to flee dps/healers
{
bool isHealer = botAI->IsHeal(bot);
bool needHealer = !isHealer && AI_VALUE2(uint8, "health", "self target") < 50;
bool isRanged = botAI->IsRanged(bot);
Group* group = bot->GetGroup();
if (group)
{
Unit* fleeTarget = nullptr;
float fleeDistance = botAI->GetRange("shoot") * 1.5f;
Unit* spareTarget = nullptr;
float spareDistance = botAI->GetRange("shoot") * 2.0f;
std::vector<Unit*> possibleTargets;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* player = gref->GetSource();
if (!player || player == bot || !player->IsAlive())
continue;
if ((isHealer && botAI->IsHeal(player)) || needHealer)
{
float distanceToHealer = ServerFacade::instance().GetDistance2d(bot, player);
float distanceToTarget = ServerFacade::instance().GetDistance2d(player, target);
if (distanceToHealer < fleeDistance &&
distanceToTarget > (botAI->GetRange("shoot") / 2 + sPlayerbotAIConfig.followDistance) &&
(needHealer || player->IsWithinLOSInMap(target)))
{
fleeTarget = player;
fleeDistance = distanceToHealer;
possibleTargets.push_back(fleeTarget);
}
}
else if (isRanged && botAI->IsRanged(player))
{
float distanceToRanged = ServerFacade::instance().GetDistance2d(bot, player);
float distanceToTarget = ServerFacade::instance().GetDistance2d(player, target);
if (distanceToRanged < fleeDistance &&
distanceToTarget > (botAI->GetRange("shoot") / 2 + sPlayerbotAIConfig.followDistance) &&
player->IsWithinLOSInMap(target))
{
fleeTarget = player;
fleeDistance = distanceToRanged;
possibleTargets.push_back(fleeTarget);
}
}
// remember any group member in case no one else found
float distanceToFlee = ServerFacade::instance().GetDistance2d(bot, player);
float distanceToTarget = ServerFacade::instance().GetDistance2d(player, target);
if (distanceToFlee < spareDistance &&
distanceToTarget > (botAI->GetRange("shoot") / 2 + sPlayerbotAIConfig.followDistance) &&
player->IsWithinLOSInMap(target))
{
spareTarget = player;
spareDistance = distanceToFlee;
possibleTargets.push_back(fleeTarget);
}
}
if (!possibleTargets.empty())
fleeTarget = possibleTargets[urand(0, possibleTargets.size() - 1)];
if (!fleeTarget)
fleeTarget = spareTarget;
if (fleeTarget)
foundFlee = MoveNear(fleeTarget);
if ((!fleeTarget || !foundFlee) && master && master->IsAlive() && master->IsWithinLOSInMap(target))
{
float distanceToTarget = ServerFacade::instance().GetDistance2d(master, target);
if (distanceToTarget > (botAI->GetRange("shoot") / 2 + sPlayerbotAIConfig.followDistance))
foundFlee = MoveNear(master);
}
}
}
if ((foundFlee || lastFlee) && bot->GetGroup())
{
if (!lastFlee)
{
AI_VALUE(LastMovement&, "last movement").lastFlee = now;
}
else
{
if ((now - lastFlee) > fleeDelay)
{
AI_VALUE(LastMovement&, "last movement").lastFlee = 0;
}
else
return false;
}
}
FleeManager manager(bot, botAI->GetRange("flee"), bot->GetAngle(target) + M_PI);
if (!manager.isUseful())
return false;
float rx, ry, rz;
if (!manager.CalculateDestination(&rx, &ry, &rz))
{
botAI->TellError("Nowhere to flee");
return false;
}
bool result = MoveTo(target->GetMapId(), rx, ry, rz);
if (result)
AI_VALUE(LastMovement&, "last movement").lastFlee = time(nullptr);
return result;
}
void MovementAction::ClearIdleState()
{
context->GetValue<time_t>("stay time")->Set(0);
context->GetValue<PositionMap&>("position")->Get()["random"].Reset();
}
bool MovementAction::MoveAway(Unit* target, float distance, bool backwards)
{
if (!target)
{
return false;
}
EmitDebugMove("MoveAway", "mmap", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ());
float init_angle = target->GetAngle(bot);
for (float delta = 0; delta <= M_PI / 2; delta += M_PI / 8)
{
float angle = init_angle + delta;
float dx = bot->GetPositionX() + cos(angle) * distance;
float dy = bot->GetPositionY() + sin(angle) * distance;
float dz = bot->GetPositionZ();
bool exact = true;
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), dx, dy, dz))
{
// disable prediction if position is invalid
dx = bot->GetPositionX() + cos(angle) * distance;
dy = bot->GetPositionY() + sin(angle) * distance;
dz = bot->GetPositionZ();
exact = false;
}
if (MoveTo(target->GetMapId(), dx, dy, dz, false, false, true, exact, MovementPriority::MOVEMENT_COMBAT, false,
backwards))
{
return true;
}
if (delta == 0)
{
continue;
}
exact = true;
angle = init_angle - delta;
dx = bot->GetPositionX() + cos(angle) * distance;
dy = bot->GetPositionY() + sin(angle) * distance;
dz = bot->GetPositionZ();
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), dx, dy, dz))
{
// disable prediction if position is invalid
dx = bot->GetPositionX() + cos(angle) * distance;
dy = bot->GetPositionY() + sin(angle) * distance;
dz = bot->GetPositionZ();
exact = false;
}
if (MoveTo(target->GetMapId(), dx, dy, dz, false, false, true, exact, MovementPriority::MOVEMENT_COMBAT, false,
backwards))
{
return true;
}
}
return false;
}
// just calculates average position of group and runs away from that position
bool MovementAction::MoveFromGroup(float distance)
{
if (Group* group = bot->GetGroup())
{
uint32 mapId = bot->GetMapId();
float closestDist = FLT_MAX;
float x = 0.0f;
float y = 0.0f;
uint32 count = 0;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* player = gref->GetSource();
if (!player || player == bot || !player->IsAlive() || player->GetMapId() != mapId)
continue;
float dist = bot->GetDistance2d(player);
if (closestDist > dist)
closestDist = dist;
x += player->GetPositionX();
y += player->GetPositionY();
count++;
}
if (count && closestDist < distance)
{
x /= count;
y /= count;
// x and y are now average position of the group members
float angle = bot->GetAngle(x, y) + M_PI;
EmitDebugMove("MoveFromGroup", "mmap", x, y, bot->GetPositionZ());
return Move(angle, distance - closestDist);
}
}
return false;
}
bool MovementAction::Move(float angle, float distance)
{
float x = bot->GetPositionX() + cos(angle) * distance;
float y = bot->GetPositionY() + sin(angle) * distance;
// TODO do we need GetMapWaterOrGroundLevel() if we're using CheckCollisionAndGetValidCoords() ?
float z = bot->GetMapWaterOrGroundLevel(x, y, bot->GetPositionZ());
if (z == -100000.0f || z == -200000.0f)
z = bot->GetPositionZ();
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), x, y, z, false))
return false;
return MoveTo(bot->GetMapId(), x, y, z);
}
bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float distance, MovementPriority priority)
{
if (bot->GetDistance2d(x, y) <= distance)
{
return false;
}
EmitDebugMove("MoveInside", "mmap", x, y, z);
return MoveNear(mapId, x, y, z, distance, priority);
}
// float MovementAction::SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range, bool
// normal_only, float step)
// {
// if (!generatePath)
// {
// return z;
// }
// float min_length = 100000.0f;
// float current_z = INVALID_HEIGHT;
// float modified_z;
// float delta;
// for (delta = 0.0f; delta <= range / 2; delta += step) {
// modified_z = bot->GetMapWaterOrGroundLevel(x, y, z + delta);
// PathGenerator gen(bot);
// gen.CalculatePath(x, y, modified_z);
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length)
// {
// min_length = gen.getPathLength();
// current_z = modified_z;
// if (abs(current_z - z) < 0.5f)
// {
// return current_z;
// }
// }
// }
// for (delta = -step; delta >= -range / 2; delta -= step) {
// modified_z = bot->GetMapWaterOrGroundLevel(x, y, z + delta);
// PathGenerator gen(bot);
// gen.CalculatePath(x, y, modified_z);
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length)
// {
// min_length = gen.getPathLength();
// current_z = modified_z;
// if (abs(current_z - z) < 0.5f)
// return current_z;
// }
// }
// for (delta = range / 2 + step; delta <= range; delta += 2) {
// modified_z = bot->GetMapWaterOrGroundLevel(x, y, z + delta);
// PathGenerator gen(bot);
// gen.CalculatePath(x, y, modified_z);
// if (gen.GetPathType() == PATHFIND_NORMAL && gen.getPathLength() < min_length)
// {
// min_length = gen.getPathLength();
// current_z = modified_z;
// if (abs(current_z - z) < 0.5f)
// {
// return current_z;
// }
// }
// }
// if (current_z == INVALID_HEIGHT && normal_only)
// {
// return INVALID_HEIGHT;
// }
// if (current_z == INVALID_HEIGHT && !normal_only)
// {
// return z;
// }
// return current_z;
// }
PathResult MovementAction::GeneratePath(float x, float y, float z, uint32 acceptMask, bool forceDestination)
{
PathResult result;
PathGenerator gen(bot);
gen.CalculatePath(x, y, z, forceDestination);
result.pathType = gen.GetPathType();
result.reachable = !(result.pathType & (~acceptMask));
result.points = gen.GetPath();
result.actualEnd = gen.GetActualEndPosition();
result.end = gen.GetEndPosition();
return result;
}
void MovementAction::DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards)
{
if (!unit)
return;
MotionMaster* mm = unit->GetMotionMaster();
if (!mm)
return;
// bot water collision correction
if (unit->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING) && unit->HasWaterWalkAura())
{
float gZ = unit->GetMapWaterOrGroundLevel(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ());
unit->UpdatePosition(unit->GetPositionX(), unit->GetPositionY(), gZ, false);
}
mm->Clear();
if (backwards)
{
mm->MovePointBackwards(
/*id*/ 0,
/*coords*/ x, y, z,
/*generatePath*/ generatePath,
/*forceDestination*/ false);
return;
}
else
{
mm->MovePoint(
/*id*/ 0,
/*coords*/ x, y, z,
/*forcedMovement*/ FORCED_MOVEMENT_NONE,
/*speed*/ 0.f,
/*orientation*/ 0.f,
/*generatePath*/ generatePath, // true => terrain path (2d mmap); false => straight spline (3d vmap)
/*forceDestination*/ false);
}
}
bool FleeAction::Execute(Event /*event*/)
{
return MoveAway(AI_VALUE(Unit*, "current target"), sPlayerbotAIConfig.fleeDistance, true);
}
bool FleeAction::isUseful()
{
if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr)
return false;
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->IsInWorld() && !bot->IsWithinMeleeRange(target))
return false;
return true;
}
bool FleeWithPetAction::Execute(Event /*event*/)
{
if (bot->GetPet())
botAI->PetFollow();
return Flee(AI_VALUE(Unit*, "current target"));
}
bool AvoidAoeAction::isUseful()
{
if (getMSTime() - moveInterval < lastMoveTimer)
return false;
GuidVector traps = AI_VALUE(GuidVector, "nearest trap with damage");
GuidVector triggers = AI_VALUE(GuidVector, "possible triggers");
return AI_VALUE(Aura*, "area debuff") || !traps.empty() || !triggers.empty();
}
bool AvoidAoeAction::Execute(Event /*event*/)
{
// Case #1: Aura with dynamic object (e.g. rain of fire)
if (AvoidAuraWithDynamicObj())
{
return true;
}
// Case #2: Trap game object with spell (e.g. lava bomb)
if (AvoidGameObjectWithDamage())
{
return true;
}
// Case #3: Trigger npc (e.g. Lesser shadow fissure)
if (AvoidUnitWithDamageAura())
{
return true;
}
return false;
}
bool AvoidAoeAction::AvoidAuraWithDynamicObj()
{
Aura* aura = AI_VALUE(Aura*, "area debuff");
if (!aura || aura->IsRemoved() || aura->IsExpired())
{
return false;
}
if (!aura->GetOwner() || !aura->GetOwner()->IsInWorld())
{
return false;
}
// Crash fix: maybe change owner due to check interval
if (aura->GetType() != DYNOBJ_AURA_TYPE)
{
return false;
}
const SpellInfo* spellInfo = aura->GetSpellInfo();
if (!spellInfo)
{
return false;
}
if (sPlayerbotAIConfig.aoeAvoidSpellWhitelist.find(spellInfo->Id) !=
sPlayerbotAIConfig.aoeAvoidSpellWhitelist.end())
return false;
DynamicObject* dynOwner = aura->GetDynobjOwner();
if (!dynOwner || !dynOwner->IsInWorld())
{
return false;
}
float radius = dynOwner->GetRadius();
if (!radius || radius > sPlayerbotAIConfig.maxAoeAvoidRadius)
return false;
if (bot->GetDistance(dynOwner) > radius)
{
return false;
}
std::ostringstream name;
name << spellInfo->SpellName[LOCALE_enUS]; // << "] (aura)";
if (FleePosition(dynOwner->GetPosition(), radius))
{
if (sPlayerbotAIConfig.tellWhenAvoidAoe && lastTellTimer < time(NULL) - 10)
{
lastTellTimer = time(NULL);
lastMoveTimer = getMSTime();
std::ostringstream out;
out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius << " - [Aura]";
bot->Say(out.str(), LANG_UNIVERSAL);
}
return true;
}
return false;
}
bool AvoidAoeAction::AvoidGameObjectWithDamage()
{
GuidVector traps = AI_VALUE(GuidVector, "nearest trap with damage");
if (traps.empty())
{
return false;
}
for (ObjectGuid& guid : traps)
{
GameObject* go = botAI->GetGameObject(guid);
if (!go || !go->IsInWorld())
{
continue;
}
if (go->GetGoType() != GAMEOBJECT_TYPE_TRAP)
{
continue;
}
const GameObjectTemplate* goInfo = go->GetGOInfo();
if (!goInfo)
{
continue;
}
// 0 trap with no despawn after cast. 1 trap despawns after cast. 2 bomb casts on spawn.
if (goInfo->trap.type != 0)
continue;
uint32 spellId = goInfo->trap.spellId;
if (!spellId)
{
continue;
}
if (sPlayerbotAIConfig.aoeAvoidSpellWhitelist.find(spellId) !=
sPlayerbotAIConfig.aoeAvoidSpellWhitelist.end())
continue;
const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo || spellInfo->IsPositive())
{
continue;
}
float radius = (float)goInfo->trap.diameter / 2 + go->GetCombatReach();
if (!radius || radius > sPlayerbotAIConfig.maxAoeAvoidRadius)
continue;
if (bot->GetDistance(go) > radius)
{
continue;
}
std::ostringstream name;
name << spellInfo->SpellName[LOCALE_enUS]; // << "] (object)";
if (FleePosition(go->GetPosition(), radius))
{
if (sPlayerbotAIConfig.tellWhenAvoidAoe && lastTellTimer < time(NULL) - 10)
{
lastTellTimer = time(NULL);
lastMoveTimer = getMSTime();
std::ostringstream out;
out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius
<< " - [Trap]";
bot->Say(out.str(), LANG_UNIVERSAL);
}
return true;
}
}
return false;
}
bool AvoidAoeAction::AvoidUnitWithDamageAura()
{
GuidVector triggers = AI_VALUE(GuidVector, "possible triggers");
if (triggers.empty())
{
return false;
}
for (ObjectGuid& guid : triggers)
{
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsInWorld())
{
continue;
}
if (!unit->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE))
{
return false;
}
Unit::AuraEffectList const& aurasPeriodicTriggerSpell =
unit->GetAuraEffectsByType(SPELL_AURA_PERIODIC_TRIGGER_SPELL);
Unit::AuraEffectList const& aurasPeriodicTriggerWithValueSpell =
unit->GetAuraEffectsByType(SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE);
for (const Unit::AuraEffectList& list : {aurasPeriodicTriggerSpell, aurasPeriodicTriggerWithValueSpell})
{
for (auto i = list.begin(); i != list.end(); ++i)
{
AuraEffect* aurEff = *i;
const SpellInfo* spellInfo = aurEff->GetSpellInfo();
if (!spellInfo)
continue;
const SpellInfo* triggerSpellInfo =
sSpellMgr->GetSpellInfo(spellInfo->Effects[aurEff->GetEffIndex()].TriggerSpell);
if (!triggerSpellInfo)
continue;
if (sPlayerbotAIConfig.aoeAvoidSpellWhitelist.find(triggerSpellInfo->Id) !=
sPlayerbotAIConfig.aoeAvoidSpellWhitelist.end())
return false;
for (int j = 0; j < MAX_SPELL_EFFECTS; j++)
{
if (triggerSpellInfo->Effects[j].Effect == SPELL_EFFECT_SCHOOL_DAMAGE)
{
float radius = triggerSpellInfo->Effects[j].CalcRadius();
if (bot->GetDistance(unit) > radius)
{
break;
}
if (!radius || radius > sPlayerbotAIConfig.maxAoeAvoidRadius)
continue;
std::ostringstream name;
name << triggerSpellInfo->SpellName[LOCALE_enUS]; //<< "] (unit)";
if (FleePosition(unit->GetPosition(), radius))
{
if (sPlayerbotAIConfig.tellWhenAvoidAoe && lastTellTimer < time(NULL) - 10)
{
lastTellTimer = time(NULL);
lastMoveTimer = getMSTime();
std::ostringstream out;
out << "I'm avoiding " << name.str() << " (" << triggerSpellInfo->Id << ")"
<< " Radius " << radius << " - [Unit Trigger]";
bot->Say(out.str(), LANG_UNIVERSAL);
}
}
}
}
}
}
}
return false;
}
Position MovementAction::BestPositionForMeleeToFlee(Position pos, float radius)
{
Unit* currentTarget = AI_VALUE(Unit*, "current target");
std::vector<CheckAngle> possibleAngles;
if (currentTarget)
{
// Normally, move to left or right is the best position
bool isTanking = (!currentTarget->isFrozen()
&& !currentTarget->HasRootAura()) && (currentTarget->GetVictim() == bot);
float angle = bot->GetAngle(currentTarget);
float angleLeft = angle + (float)M_PI / 2;
float angleRight = angle - (float)M_PI / 2;
possibleAngles.push_back({angleLeft, false});
possibleAngles.push_back({angleRight, false});
possibleAngles.push_back({angle, true});
if (isTanking)
{
possibleAngles.push_back({angle + (float)M_PI, false});
possibleAngles.push_back({bot->GetAngle(&pos) - (float)M_PI, false});
}
}
else
{
float angleTo = bot->GetAngle(&pos) - (float)M_PI;
possibleAngles.push_back({angleTo, false});
}
float farestDis = 0.0f;
Position bestPos;
for (CheckAngle& checkAngle : possibleAngles)
{
float angle = checkAngle.angle;
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
if (!CheckLastFlee(angle, infoList))
{
continue;
}
bool strict = checkAngle.strict;
float fleeDis = std::min(radius + 1.0f, sPlayerbotAIConfig.fleeDistance);
float dx = bot->GetPositionX() + cos(angle) * fleeDis;
float dy = bot->GetPositionY() + sin(angle) * fleeDis;
float dz = bot->GetPositionZ();
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), dx, dy, dz))
{
continue;
}
Position fleePos{dx, dy, dz};
if (strict && currentTarget &&
fleePos.GetExactDist(currentTarget) - currentTarget->GetCombatReach() >
sPlayerbotAIConfig.tooCloseDistance &&
bot->IsWithinMeleeRange(currentTarget))
{
continue;
}
if (pos.GetExactDist(fleePos) > farestDis)
{
farestDis = pos.GetExactDist(fleePos);
bestPos = fleePos;
}
}
if (farestDis > 0.0f)
{
return bestPos;
}
return Position();
}
Position MovementAction::BestPositionForRangedToFlee(Position pos, float radius)
{
Unit* currentTarget = AI_VALUE(Unit*, "current target");
std::vector<CheckAngle> possibleAngles;
float angleToTarget = 0.0f;
float angleFleeFromCenter = bot->GetAngle(&pos) - (float)M_PI;
if (currentTarget)
{
// Normally, move to left or right is the best position
angleToTarget = bot->GetAngle(currentTarget);
float angleLeft = angleToTarget + (float)M_PI / 2;
float angleRight = angleToTarget - (float)M_PI / 2;
possibleAngles.push_back({angleLeft, false});
possibleAngles.push_back({angleRight, false});
possibleAngles.push_back({angleToTarget + (float)M_PI, true});
possibleAngles.push_back({angleToTarget, true});
possibleAngles.push_back({angleFleeFromCenter, true});
}
else
{
possibleAngles.push_back({angleFleeFromCenter, false});
}
float farestDis = 0.0f;
Position bestPos;
for (CheckAngle& checkAngle : possibleAngles)
{
float angle = checkAngle.angle;
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
if (!CheckLastFlee(angle, infoList))
{
continue;
}
bool strict = checkAngle.strict;
float fleeDis = std::min(radius + 1.0f, sPlayerbotAIConfig.fleeDistance);
float dx = bot->GetPositionX() + cos(angle) * fleeDis;
float dy = bot->GetPositionY() + sin(angle) * fleeDis;
float dz = bot->GetPositionZ();
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), dx, dy, dz))
{
continue;
}
Position fleePos{dx, dy, dz};
if (strict && currentTarget &&
fleePos.GetExactDist(currentTarget) - currentTarget->GetCombatReach() > sPlayerbotAIConfig.spellDistance)
{
continue;
}
if (strict && currentTarget &&
fleePos.GetExactDist(currentTarget) - currentTarget->GetCombatReach() <
(sPlayerbotAIConfig.tooCloseDistance))
{
continue;
}
if (pos.GetExactDist(fleePos) > farestDis)
{
farestDis = pos.GetExactDist(fleePos);
bestPos = fleePos;
}
}
if (farestDis > 0.0f)
{
return bestPos;
}
return Position();
}
bool MovementAction::FleePosition(Position pos, float radius, uint32 minInterval)
{
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
if (!infoList.empty() && infoList.back().timestamp + minInterval > getMSTime())
return false;
Position bestPos;
if (botAI->IsMelee(bot))
{
bestPos = BestPositionForMeleeToFlee(pos, radius);
}
else
{
bestPos = BestPositionForRangedToFlee(pos, radius);
}
if (bestPos != Position())
{
EmitDebugMove("FleePosition", "mmap", bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ());
if (MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), false,
false, true, false, MovementPriority::MOVEMENT_COMBAT))
{
uint32 curTS = getMSTime();
while (!infoList.empty())
{
if (infoList.size() > 10 || infoList.front().timestamp + 5000 < curTS)
{
infoList.pop_front();
}
else
{
break;
}
}
infoList.push_back({pos, radius, bot->GetAngle(&bestPos), curTS});
return true;
}
}
return false;
}
bool MovementAction::CheckLastFlee(float curAngle, std::list<FleeInfo>& infoList)
{
uint32 curTS = getMSTime();
curAngle = Position::NormalizeOrientation(curAngle);
while (!infoList.empty())
{
if (infoList.size() > 10 || infoList.front().timestamp + 5000 < curTS)
{
infoList.pop_front();
}
else
{
break;
}
}
for (FleeInfo& info : infoList)
{
// more than 5 sec
if (info.timestamp + 5000 < curTS)
{
continue;
}
float revAngle = Position::NormalizeOrientation(info.angle + M_PI);
// angle too close
if (fabs(revAngle - curAngle) < M_PI / 4)
{
return false;
}
}
return true;
}
bool CombatFormationMoveAction::isUseful()
{
if (getMSTime() - moveInterval < lastMoveTimer)
return false;
if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr)
return false;
return true;
}
bool CombatFormationMoveAction::Execute(Event /*event*/)
{
float dis = AI_VALUE(float, "disperse distance");
if (dis <= 0.0f || (!bot->IsInCombat() && botAI->HasStrategy("stay", BotState::BOT_STATE_NON_COMBAT)) ||
(bot->IsInCombat() && botAI->HasStrategy("stay", BotState::BOT_STATE_COMBAT)))
return false;
Player* playerToLeave = NearestGroupMember(dis);
if (playerToLeave && bot->GetExactDist(playerToLeave) < dis)
{
if (FleePosition(playerToLeave->GetPosition(), dis))
{
lastMoveTimer = getMSTime();
}
}
return false;
}
Position CombatFormationMoveAction::AverageGroupPos(float dis, bool ranged, bool self)
{
float averageX = 0, averageY = 0, averageZ = 0;
int cnt = 0;
Group* group = bot->GetGroup();
if (!group)
{
return Position();
}
Group::MemberSlotList const& groupSlot = group->GetMemberSlots();
for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); itr++)
{
Player* member = ObjectAccessor::FindPlayer(itr->guid);
if (!member)
continue;
if (!self && member == bot)
continue;
if (ranged && !PlayerbotAI::IsRanged(member))
continue;
if (!member->IsAlive() || member->GetMapId() != bot->GetMapId() || member->IsCharmed() ||
ServerFacade::instance().GetDistance2d(bot, member) > dis)
continue;
averageX += member->GetPositionX();
averageY += member->GetPositionY();
averageZ += member->GetPositionZ();
}
averageX /= cnt;
averageY /= cnt;
averageZ /= cnt;
return Position(averageX, averageY, averageZ);
}
float CombatFormationMoveAction::AverageGroupAngle(Unit* from, bool ranged, bool self)
{
Group* group = bot->GetGroup();
if (!from || !group)
{
return 0.0f;
}
// float average = 0.0f;
float sumX = 0.0f;
float sumY = 0.0f;
int cnt = 0;
Group::MemberSlotList const& groupSlot = group->GetMemberSlots();
for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); itr++)
{
Player* member = ObjectAccessor::FindPlayer(itr->guid);
if (!member)
continue;
if (!self && member == bot)
continue;
if (ranged && !PlayerbotAI::IsRanged(member))
continue;
if (!member->IsAlive() || member->GetMapId() != bot->GetMapId() || member->IsCharmed() ||
ServerFacade::instance().GetDistance2d(bot, member) > sPlayerbotAIConfig.sightDistance)
continue;
cnt++;
sumX += member->GetPositionX() - from->GetPositionX();
sumY += member->GetPositionY() - from->GetPositionY();
}
if (cnt == 0)
return 0.0f;
// unnecessary division
// sumX /= cnt;
// sumY /= cnt;
return atan2(sumY, sumX);
}
Position CombatFormationMoveAction::GetNearestPosition(const std::vector<Position>& positions)
{
Position result;
for (const Position& pos : positions)
{
if (bot->GetExactDist(pos) < bot->GetExactDist(result))
result = pos;
}
return result;
}
Player* CombatFormationMoveAction::NearestGroupMember(float dis)
{
float nearestDis = 10000.0f;
Player* result = nullptr;
Group* group = bot->GetGroup();
if (!group)
{
return result;
}
Group::MemberSlotList const& groupSlot = group->GetMemberSlots();
for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); itr++)
{
Player* member = ObjectAccessor::FindPlayer(itr->guid);
if (!member || !member->IsAlive() || member == bot || member->GetMapId() != bot->GetMapId() ||
member->IsCharmed() || ServerFacade::instance().GetDistance2d(bot, member) > dis)
continue;
if (nearestDis > bot->GetExactDist(member))
{
result = member;
nearestDis = bot->GetExactDist(member);
}
}
return result;
}
bool TankFaceAction::Execute(Event /*event*/)
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
if (!bot->GetGroup())
return false;
if (!bot->IsWithinMeleeRange(target) || target->isMoving())
return false;
if (!AI_VALUE2(bool, "has aggro", "current target"))
return false;
float averageAngle = AverageGroupAngle(target, true);
if (averageAngle == 0.0f)
return false;
float deltaAngle = Position::NormalizeOrientation(averageAngle - target->GetAngle(bot));
if (deltaAngle > M_PI)
deltaAngle -= 2.0f * M_PI; // -PI..PI
float tolerable = M_PI_2;
if (fabs(deltaAngle) > tolerable)
return false;
float goodAngle1 = Position::NormalizeOrientation(averageAngle + M_PI * 3 / 5);
float goodAngle2 = Position::NormalizeOrientation(averageAngle - M_PI * 3 / 5);
// if dist < bot->GetMeleeRange(target) / 2, target will move backward
float dist = std::max(bot->GetExactDist(target), bot->GetMeleeRange(target) / 2) - bot->GetCombatReach() -
target->GetCombatReach();
std::vector<Position> availablePos;
float x, y, z;
target->GetNearPoint(bot, x, y, z, 0.0f, dist, goodAngle1);
if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), x, y, z))
{
/// @todo: movement control now is a mess, prepare to rewrite
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
Position pos(x, y, z);
float angle = bot->GetAngle(&pos);
if (CheckLastFlee(angle, infoList))
{
availablePos.push_back(Position(x, y, z));
}
}
target->GetNearPoint(bot, x, y, z, 0.0f, dist, goodAngle2);
if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), x, y, z))
{
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
Position pos(x, y, z);
float angle = bot->GetAngle(&pos);
if (CheckLastFlee(angle, infoList))
{
availablePos.push_back(Position(x, y, z));
}
}
if (availablePos.empty())
return false;
Position nearest = GetNearestPosition(availablePos);
return MoveTo(bot->GetMapId(), nearest.GetPositionX(), nearest.GetPositionY(), nearest.GetPositionZ(), false, false,
false, true, MovementPriority::MOVEMENT_COMBAT);
}
bool RearFlankAction::isUseful()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
// Need to double the front angle check to account for mirrored angle.
bool inFront = target->HasInArc(2.f * minAngle, bot);
// Rear check does not need to double this angle as the logic is inverted
// and we are subtracting from 2pi.
bool inRear = !target->HasInArc((2.f * M_PI) - maxAngle, bot);
return inFront || inRear;
}
bool RearFlankAction::Execute(Event /*event*/)
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
float angle = frand(minAngle, maxAngle);
float baseDistance = bot->GetMeleeRange(target) * 0.5f;
Position leftFlank = target->GetPosition();
Position rightFlank = target->GetPosition();
Position* destination = nullptr;
leftFlank.RelocatePolarOffset(angle, baseDistance + distance);
rightFlank.RelocatePolarOffset(-angle, baseDistance + distance);
if (bot->GetExactDist2d(leftFlank) < bot->GetExactDist2d(rightFlank))
{
destination = &leftFlank;
}
else
{
destination = &rightFlank;
}
return MoveTo(bot->GetMapId(), destination->GetPositionX(), destination->GetPositionY(),
destination->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT);
}
bool DisperseSetAction::Execute(Event event)
{
std::string const text = event.getParam();
if (text == "disable")
{
RESET_AI_VALUE(float, "disperse distance");
botAI->TellMasterNoFacing("Disable disperse");
return true;
}
if (text == "enable" || text == "reset")
{
if (botAI->IsMelee(bot))
{
SET_AI_VALUE(float, "disperse distance", DEFAULT_DISPERSE_DISTANCE_MELEE);
}
else
{
SET_AI_VALUE(float, "disperse distance", DEFAULT_DISPERSE_DISTANCE_RANGED);
}
float dis = AI_VALUE(float, "disperse distance");
std::ostringstream out;
out << "Enable disperse distance " << std::setprecision(2) << dis;
botAI->TellMasterNoFacing(out.str());
return true;
}
if (text == "increase")
{
float dis = AI_VALUE(float, "disperse distance");
std::ostringstream out;
if (dis <= 0.0f)
{
out << "Enable disperse first";
botAI->TellMasterNoFacing(out.str());
return true;
}
dis += 1.0f;
SET_AI_VALUE(float, "disperse distance", dis);
out << "Increase disperse distance to " << std::setprecision(2) << dis;
botAI->TellMasterNoFacing(out.str());
return true;
}
if (text == "decrease")
{
float dis = AI_VALUE(float, "disperse distance");
dis -= 1.0f;
if (dis <= 0.0f)
{
dis += 1.0f;
}
SET_AI_VALUE(float, "disperse distance", dis);
std::ostringstream out;
out << "Increase disperse distance to " << std::setprecision(2) << dis;
botAI->TellMasterNoFacing(out.str());
return true;
}
if (text.starts_with("set"))
{
float dis = -1.0f;
;
sscanf(text.c_str(), "set %f", &dis);
std::ostringstream out;
if (dis < 0 || dis > 100.0f)
{
out << "Invalid disperse distance " << std::setprecision(2) << dis;
}
else
{
SET_AI_VALUE(float, "disperse distance", dis);
out << "Set disperse distance to " << std::setprecision(2) << dis;
}
botAI->TellMasterNoFacing(out.str());
return true;
}
std::ostringstream out;
out << "Usage: disperse [enable | disable | increase | decrease | set {distance}]";
float dis = AI_VALUE(float, "disperse distance");
if (dis > 0.0f)
{
out << "(Current disperse distance: " << std::setprecision(2) << dis << ")";
}
botAI->TellMasterNoFacing(out.str());
return true;
}
bool RunAwayAction::Execute(Event /*event*/) { return Flee(AI_VALUE(Unit*, "group leader")); }
bool MoveToLootAction::Execute(Event /*event*/)
{
LootObject loot = AI_VALUE(LootObject, "loot target");
if (!loot.IsLootPossible(bot))
return false;
return MoveNear(loot.GetWorldObject(bot), sPlayerbotAIConfig.contactDistance);
}
bool MoveOutOfEnemyContactAction::Execute(Event /*event*/)
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
return MoveTo(target, sPlayerbotAIConfig.contactDistance);
}
bool MoveOutOfEnemyContactAction::isUseful() { return AI_VALUE2(bool, "inside target", "current target"); }
bool SetFacingTargetAction::Execute(Event /*event*/)
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
if (bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
return true;
ServerFacade::instance().SetFacingTo(bot, target);
botAI->SetNextCheckDelay(sPlayerbotAIConfig.reactDelay);
return true;
}
bool SetFacingTargetAction::isUseful() { return !AI_VALUE2(bool, "facing", "current target"); }
bool SetFacingTargetAction::isPossible()
{
if (bot->isFrozen() || bot->IsPolymorphed() || (bot->isDead() && !bot->HasPlayerFlag(PLAYER_FLAGS_GHOST)) ||
bot->IsBeingTeleported() || bot->HasConfuseAura() || bot->IsCharmed() || bot->HasStunAura() ||
bot->IsInFlight() || bot->HasUnitState(UNIT_STATE_LOST_CONTROL))
return false;
return true;
}
bool SetBehindTargetAction::Execute(Event /*event*/)
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
if (target->GetVictim() == bot)
return false;
if (!bot->IsWithinMeleeRange(target) || target->isMoving())
return false;
float deltaAngle = Position::NormalizeOrientation(target->GetOrientation() - target->GetAngle(bot));
if (deltaAngle > M_PI)
deltaAngle -= 2.0f * M_PI; // -PI..PI
float tolerable = M_PI_2;
if (fabs(deltaAngle) > tolerable)
return false;
float goodAngle1 = Position::NormalizeOrientation(target->GetOrientation() + M_PI * 3 / 5);
float goodAngle2 = Position::NormalizeOrientation(target->GetOrientation() - M_PI * 3 / 5);
float dist = std::max(bot->GetExactDist(target), bot->GetMeleeRange(target) / 2) - bot->GetCombatReach() -
target->GetCombatReach();
std::vector<Position> availablePos;
float x, y, z;
target->GetNearPoint(bot, x, y, z, 0.0f, dist, goodAngle1);
if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), x, y, z))
{
/// @todo: movement control now is a mess, prepare to rewrite
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
Position pos(x, y, z);
float angle = bot->GetAngle(&pos);
if (CheckLastFlee(angle, infoList))
{
availablePos.push_back(Position(x, y, z));
}
}
target->GetNearPoint(bot, x, y, z, 0.0f, dist, goodAngle2);
if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), x, y, z))
{
std::list<FleeInfo>& infoList = AI_VALUE(std::list<FleeInfo>&, "recently flee info");
Position pos(x, y, z);
float angle = bot->GetAngle(&pos);
if (CheckLastFlee(angle, infoList))
{
availablePos.push_back(Position(x, y, z));
}
}
if (availablePos.empty())
return false;
Position nearest = GetNearestPosition(availablePos);
return MoveTo(bot->GetMapId(), nearest.GetPositionX(), nearest.GetPositionY(), nearest.GetPositionZ(), false, false,
false, true, MovementPriority::MOVEMENT_COMBAT);
}
bool MoveOutOfCollisionAction::Execute(Event /*event*/)
{
float angle = M_PI * 2000 / frand(1.f, 1000.f);
float distance = sPlayerbotAIConfig.followDistance;
return MoveTo(bot->GetMapId(), bot->GetPositionX() + cos(angle) * distance,
bot->GetPositionY() + sin(angle) * distance, bot->GetPositionZ());
}
bool MoveOutOfCollisionAction::isUseful()
{
// do not avoid collision on vehicle
if (botAI->IsInVehicle())
return false;
return AI_VALUE2(bool, "collision", "self target") &&
botAI->GetAiObjectContext()->GetValue<GuidVector>("nearest friendly players")->Get().size() < 15;
}
bool MoveRandomAction::Execute(Event /*event*/)
{
float distance = sPlayerbotAIConfig.tooCloseDistance + urand(10, 30);
Map* map = bot->GetMap();
for (int i = 0; i < 3; ++i)
{
float x = bot->GetPositionX();
float y = bot->GetPositionY();
float z = bot->GetPositionZ();
float angle = (float)rand_norm() * static_cast<float>(M_PI);
x += urand(0, distance) * cos(angle);
y += urand(0, distance) * sin(angle);
if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), x, y, z))
continue;
if (map->IsInWater(bot->GetPhaseMask(), x, y, z, bot->GetCollisionHeight()))
continue;
bool moved = MoveTo(bot->GetMapId(), x, y, z, false, false, false, true);
if (moved)
return true;
}
return false;
}
bool MoveRandomAction::isUseful() { return !AI_VALUE(GuidPosition, "rpg target"); }
bool MoveInsideAction::Execute(Event /*event*/) { return MoveInside(bot->GetMapId(), x, y, bot->GetPositionZ(), distance); }
bool RotateAroundTheCenterPointAction::Execute(Event /*event*/)
{
uint32 next_point = GetCurrWaypoint();
if (MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, bot->GetPositionZ(), false,
false, false, false, MovementPriority::MOVEMENT_COMBAT))
{
call_counters += 1;
return true;
}
return false;
}
bool MoveFromGroupAction::Execute(Event event)
{
float distance = atoi(event.getParam().c_str());
if (!distance)
distance = 20.0f; // flee distance from config is too small for this
return MoveFromGroup(distance);
}
bool MoveAwayFromCreatureAction::Execute(Event /*event*/)
{
GuidVector targets = AI_VALUE(GuidVector, "nearest npcs");
// Find all creatures with the specified Id
std::vector<Unit*> creatures;
for (auto const& guid : targets)
{
Unit* unit = botAI->GetUnit(guid);
if (unit && (alive && unit->IsAlive()) && unit->GetEntry() == creatureId)
{
creatures.push_back(unit);
}
}
if (creatures.empty())
{
return false;
}
// Search for a safe position
const int directions = 8;
const float increment = 3.0f;
float bestX = bot->GetPositionX();
float bestY = bot->GetPositionY();
float bestZ = bot->GetPositionZ();
float maxSafetyScore = -1.0f;
for (int i = 0; i < directions; ++i)
{
float angle = (i * 2 * M_PI) / directions;
for (float distance = increment; distance <= 30.0f; distance += increment)
{
float moveX = bot->GetPositionX() + distance * cos(angle);
float moveY = bot->GetPositionY() + distance * sin(angle);
float moveZ = bot->GetPositionZ();
// Check creature distance constraints
bool isSafeFromCreatures = true;
float minCreatureDist = std::numeric_limits<float>::max();
for (Unit* creature : creatures)
{
float dist = creature->GetExactDist2d(moveX, moveY);
if (dist < range)
{
isSafeFromCreatures = false;
break;
}
if (dist < minCreatureDist)
{
minCreatureDist = dist;
}
}
if (isSafeFromCreatures && bot->IsWithinLOS(moveX, moveY, moveZ))
{
// A simple safety score: the minimum distance to any creature. Higher is better.
if (minCreatureDist > maxSafetyScore)
{
maxSafetyScore = minCreatureDist;
bestX = moveX;
bestY = moveY;
bestZ = moveZ;
}
}
}
}
// Move to the best position found
if (maxSafetyScore > 0.0f)
{
return MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, false,
MovementPriority::MOVEMENT_COMBAT);
}
return false;
}
bool MoveAwayFromCreatureAction::isPossible() { return bot->CanFreeMove(); }
bool MoveAwayFromPlayerWithDebuffAction::Execute(Event /*event*/)
{
Group* const group = bot->GetGroup();
if (!group)
return false;
std::vector<Player*> debuffedPlayers;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* player = gref->GetSource();
if (player && player->IsAlive() && player->HasAura(spellId))
{
debuffedPlayers.push_back(player);
}
}
if (debuffedPlayers.empty())
{
return false;
}
// Search for a safe position
const int directions = 8;
const float increment = 3.0f;
float bestX = bot->GetPositionX();
float bestY = bot->GetPositionY();
float bestZ = bot->GetPositionZ();
float maxSafetyScore = -1.0f;
for (int i = 0; i < directions; ++i)
{
float angle = (i * 2 * M_PI) / directions;
for (float distance = increment; distance <= (range + 5.0f); distance += increment)
{
float moveX = bot->GetPositionX() + distance * cos(angle);
float moveY = bot->GetPositionY() + distance * sin(angle);
float moveZ = bot->GetPositionZ();
// Check creature distance constraints
bool isSafeFromDebuffedPlayer = true;
float minDebuffedPlayerDistance = std::numeric_limits<float>::max();
for (Unit* debuffedPlayer : debuffedPlayers)
{
float dist = debuffedPlayer->GetExactDist2d(moveX, moveY);
if (dist < range)
{
isSafeFromDebuffedPlayer = false;
break;
}
if (dist < minDebuffedPlayerDistance)
{
minDebuffedPlayerDistance = dist;
}
}
if (isSafeFromDebuffedPlayer && bot->IsWithinLOS(moveX, moveY, moveZ))
{
// A simple safety score: the minimum distance to any debuffed player. Higher is better.
if (minDebuffedPlayerDistance > maxSafetyScore)
{
maxSafetyScore = minDebuffedPlayerDistance;
bestX = moveX;
bestY = moveY;
bestZ = moveZ;
}
}
}
}
// Move to the best position found
if (maxSafetyScore > 0.0f)
{
return MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, false,
MovementPriority::MOVEMENT_COMBAT, true);
}
return false;
}
bool MoveAwayFromPlayerWithDebuffAction::isPossible()
{
return bot->CanFreeMove();
}
bool MovementAction::LaunchWalkSpline(TravelPlan& state)
{
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return false;
}
// Trim past any stored points the bot has already moved past — useful
// when a spline is interrupted (combat, knockback, mid-spline reissue)
// and we re-launch from a position later in the route.
G3D::Vector3 botPos(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
float closestDist = FLT_MAX;
size_t closestIdx = 0;
for (size_t i = 0; i < state.walkPoints.size(); ++i)
{
float distance = (state.walkPoints[i] - botPos).squaredLength();
if (distance < closestDist)
{
closestDist = distance;
closestIdx = i;
}
}
if (closestIdx > 0)
state.walkPoints.erase(state.walkPoints.begin(), state.walkPoints.begin() + closestIdx);
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return true;
}
// Sparse-segment clip (cmangos parity): truncate the chain at the
// first segment longer than ~11.18y. Spline interpolation between
// sparse waypoints can cut corners through visual obstacles (trees,
// walls) the navmesh routed around. Bot re-plans from a closer
// position next tick where the resolved poly chain is denser.
{
constexpr float SPARSE_SEG_SQ = 125.0f; // sqrt(125) ≈ 11.18y
for (size_t i = 1; i < state.walkPoints.size(); ++i)
{
G3D::Vector3 d = state.walkPoints[i] - state.walkPoints[i - 1];
if (d.squaredLength() > SPARSE_SEG_SQ)
{
state.walkPoints.resize(i);
break;
}
}
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return true;
}
}
// Re-clamp cached waypoints to current valid Z. Rows in
// playerbots_travelnode_path store absolute coords baked at
// offline generation; if the live navmesh has shifted since
// (mmap regen, terrain change, vmap update), the stored z can
// be above ground — MoveSplinePath plays back coords verbatim
// and the bot looks like it's walking through the air.
// UpdateAllowedPositionZ factors mmap polygon Z, water surface,
// swimming, flying and transport state, so cave floors above
// the terrain plane snap correctly.
for (auto& pt : state.walkPoints)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// Mount up
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);
float totalDist = 0;
for (size_t i = 1; i < state.walkPoints.size(); ++i)
totalDist += (state.walkPoints[i] - state.walkPoints[i - 1]).length();
float speed = bot->GetSpeed(MOVE_RUN);
state.expectedDuration = static_cast<uint32>((totalDist / speed) * IN_MILLISECONDS);
bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN);
G3D::Vector3 const& last = state.walkPoints.back();
// 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));
delay = std::max(delay, 0.f);
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), delay, MovementPriority::MOVEMENT_NORMAL);
// Cache the dispatched waypoint chain so MoveFarTo's 10%
// lastPath reuse and "no worse" reuse can pick it up next tick.
std::vector<WorldPosition> wpts;
wpts.reserve(state.walkPoints.size());
for (auto const& pt : state.walkPoints)
wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z);
lastMove.setPath(TravelPath(wpts));
}
EmitDebugMove("TravelPlan:walk-start", "mmap", last.x, last.y, last.z);
return false; // Walking
}
bool MovementAction::RefineWalkPoints(std::vector<G3D::Vector3>& walkPoints)
{
if (walkPoints.size() < 2)
return true;
std::vector<G3D::Vector3> refined;
refined.reserve(walkPoints.size() * 4);
uint32 const mapId = bot->GetMapId();
for (size_t i = 0; i + 1 < walkPoints.size(); ++i)
{
G3D::Vector3 const& a = walkPoints[i];
G3D::Vector3 const& b = walkPoints[i + 1];
WorldPosition aPos(mapId, a.x, a.y, a.z);
WorldPosition bPos(mapId, b.x, b.y, b.z);
// Per-segment mmap query: routes around geometry the offline
// graph didn't account for, or returns empty if unreachable.
std::vector<WorldPosition> segPath = bPos.getPathStepFrom(aPos, bot);
// Trust the raw waypoint pair when mmap can't validate it —
// navmesh gaps/tile-edge artifacts shouldn't kill an active plan.
bool const trustRaw = segPath.empty() ||
TravelPath::IsPathCheating(segPath, aPos.distance(bPos));
if (trustRaw)
{
if (i == 0)
refined.emplace_back(a);
refined.emplace_back(b);
continue;
}
// Include the first segment's start; skip subsequent starts
// to avoid duplicating the prior segment's tail.
size_t startK = (i == 0) ? 0 : 1;
for (size_t k = startK; k < segPath.size(); ++k)
refined.emplace_back(segPath[k].GetPositionX(),
segPath[k].GetPositionY(),
segPath[k].GetPositionZ());
}
walkPoints = std::move(refined);
return true;
}
bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target)
{
if (!IsMovingAllowed())
return false;
EmitDebugMove("TravelPlan:walk-waypoint", "mmap", target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
// Generate path
state.walkPoints.clear();
PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ());
// Reject paths that PathGenerator marked unreachable. The default
// accept mask is NORMAL | INCOMPLETE; anything else (NOT_USING_PATH
// from BuildShortcut on invalid polys, NOPATH, etc.) means the
// dispatched waypoints would either be a straight-line through
// geometry or stop short of the target. Abort the plan instead so
// MoveFarTo can re-derive via its own probe.
if (!path.reachable)
{
state.walkPoints.clear();
return false;
}
for (auto const& pt : path.points)
state.walkPoints.push_back(G3D::Vector3(pt.x, pt.y, pt.z));
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return false;
}
// Launch spline movement
LaunchWalkSpline(state);
return true;
}
bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
{
WorldPosition botPos(bot->GetMapId(), bot->GetPositionX(),
bot->GetPositionY(), bot->GetPositionZ());
return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot);
}
bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
{
if (!state.IsActive())
return false;
if (bot->IsInFlight())
return true;
// Per-step labels (`walk`, `segment`, `flight`, `transport-*`,
// `teleport(reason)`) cover every actual movement decision; emitting
// an executor-ran-this-tick label here would whisper every tick
// while the plan is active.
if (state.stepIdx >= state.steps.size())
{
state.Reset();
return true;
}
const PathNodePoint& pt = state.steps[state.stepIdx];
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 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_PATH && wp.type != PathNodeType::NODE_NODE)
break;
state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(),
wp.point.GetPositionY(), wp.point.GetPositionZ()));
state.stepIdx++;
}
if (state.walkPoints.empty())
return true;
// Already near end of batch?
G3D::Vector3 const& last = state.walkPoints.back();
float dist = bot->GetExactDist(last.x, last.y, last.z);
if (dist < 10.0f)
{
state.walkPoints.clear();
return true;
}
// Too far from first point — abort the plan and let the
// caller's stuck-recovery decide what to do. An abandoned
// plan is recovered by the next MoveFarTo cycle.
if (state.walkPoints.size() >= 2)
{
G3D::Vector3 const& first = state.walkPoints.front();
float distToFirst = bot->GetExactDist(first.x, first.y, first.z);
if (distToFirst > MAX_PATHFINDING_DISTANCE)
{
state.walkPoints.clear();
state.Reset();
return false;
}
}
// Single point — use PathGenerator directly
if (state.walkPoints.size() < 2)
{
WorldPosition target(bot->GetMapId(), last.x, last.y, last.z);
MoveToSpline(state, target);
state.walkPoints.clear();
return true;
}
// Re-validate each segment against the live navmesh and
// substitute mmap-routed sub-paths where needed.
if (!RefineWalkPoints(state.walkPoints))
{
G3D::Vector3 const& failPt = state.walkPoints.empty()
? G3D::Vector3(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ())
: state.walkPoints.front();
EmitDebugMove("TravelPlan", "segment-unwalkable",
failPt.x, failPt.y, failPt.z);
state.walkPoints.clear();
state.Reset();
return false;
}
LaunchWalkSpline(state);
return true;
}
case PathNodeType::NODE_AREA_TRIGGER:
{
// 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.
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& trigger = state.steps[state.stepIdx];
const PathNodePoint& dst = state.steps[state.stepIdx + 1];
// Already on destination map — trigger fired, advance.
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());
if (dist > INTERACTION_DISTANCE)
return MoveTo(trigger.point.GetMapId(),
trigger.point.GetPositionX(),
trigger.point.GetPositionY(),
trigger.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, MAX_GAMEOBJECT_TYPE))
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
state.Reset();
return false;
}
case PathNodeType::NODE_TRANSPORT:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& board = state.steps[state.stepIdx];
const PathNodePoint& arrive = state.steps[state.stepIdx + 1];
// Arrived at destination?
if (bot->GetMapId() == arrive.point.GetMapId() && !bot->GetTransport())
{
state.stepIdx += 2;
return true;
}
// On transport — wait
if (bot->GetTransport())
{
if (bot->GetMapId() == arrive.point.GetMapId())
{
bot->GetTransport()->RemovePassenger(bot);
bot->StopMovingOnCurrentPos();
state.stepIdx += 2;
}
return true;
}
// Walk to boarding point
float dist = bot->GetExactDist(board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
if (dist > 60.0f)
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
// Try to board
if (board.entry)
{
Map* map = bot->GetMap();
if (map)
{
Transport* transport =
GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), board.point.GetPositionX(),
board.point.GetPositionY(), board.point.GetPositionZ());
if (transport && transport->GetEntry() == board.entry)
{
BoardTransport(transport);
return true;
}
}
}
// Wait at boarding point
if (dist > INTERACTION_DISTANCE)
return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ());
return true;
}
case PathNodeType::NODE_FLIGHTPATH:
{
if (state.stepIdx + 1 >= state.steps.size())
{
state.Reset();
return false;
}
const PathNodePoint& dep = state.steps[state.stepIdx];
const PathNodePoint& arr = state.steps[state.stepIdx + 1];
if (bot->IsInFlight())
return true;
// Resolve taxi path
if (state.route.empty())
{
uint32 fromTaxi = sObjectMgr->GetNearestTaxiNode(dep.point.GetPositionX(), dep.point.GetPositionY(),
dep.point.GetPositionZ(), dep.point.GetMapId(), bot->GetTeamId());
uint32 toTaxi = sObjectMgr->GetNearestTaxiNode(arr.point.GetPositionX(), arr.point.GetPositionY(),
arr.point.GetPositionZ(), arr.point.GetMapId(), bot->GetTeamId());
if (fromTaxi && toTaxi && fromTaxi != toTaxi)
state.route = sTravelNodeMap.FindTaxiPath(fromTaxi, toTaxi);
if (state.route.empty())
{
state.stepIdx += 2;
return true;
}
}
TravelMgr::FlightMasterInfo const* fmInfo = sTravelMgr.GetNearestFlightMasterInfo(bot);
if (!fmInfo)
{
state.route.clear();
state.stepIdx += 2;
return true;
}
if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE)
return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(),
fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ());
ObjectGuid fmGuid = ObjectGuid::Create<HighGuid::Unit>(fmInfo->templateEntry, fmInfo->dbGuid);
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid);
if (!flightMaster || !flightMaster->IsAlive())
{
state.route.clear();
state.stepIdx += 2;
return true;
}
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
bot->ActivateTaxiPathTo(state.route, flightMaster, 0);
state.route.clear();
state.stepIdx += 2;
return true;
}
default:
{
LOG_ERROR("playerbots",
"[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan",
bot->GetName(), static_cast<uint32>(pt.type));
state.Reset();
return false;
}
}
return false;
}
Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z)
{
if (!map || !ref)
return nullptr;
std::array<float, 4> const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f };
for (float const pz : probes)
{
if (Transport* transport = map->GetTransportForPos(phaseMask, x, y, pz, ref))
return transport;
}
return nullptr;
}
bool MovementAction::FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref,
float refX, float refY, float refZ, float botX, float botY, float botZ, float& outX, float& outY, float& outZ)
{
if (!map || !expectedTransport || !ref)
return false;
uint32 const phaseMask = ref->GetPhaseMask();
if (GetTransportForPosTolerant(map, ref, phaseMask, refX, refY, refZ)
!= expectedTransport)
return false;
float const probeZ = std::max(refZ, botZ);
float const dx2 = botX - refX;
float const dy2 = botY - refY;
float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2);
int32 const steps = std::clamp(static_cast<int32>(dist2d / 0.75f), 10, 28);
float const dx = (botX - refX) / static_cast<float>(steps);
float const dy = (botY - refY) / static_cast<float>(steps);
if (map->GetTransportForPos(phaseMask, refX, refY, probeZ, ref) != expectedTransport)
return false;
float lastX = refX;
float lastY = refY;
bool found = false;
for (int32 i = 1; i <= steps; ++i)
{
float const px = refX + dx * i;
float const py = refY + dy * i;
Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ);
if (t != expectedTransport)
break;
lastX = px;
lastY = py;
found = true;
}
if (!found)
return false;
outX = lastX;
outY = lastY;
outZ = refZ;
return true;
}
bool MovementAction::BoardTransport(Transport* transport)
{
if (!transport || transport->IsStaticTransport())
return false;
Map* map = bot->GetMap();
if (!map)
return false;
// Already on this transport
if (bot->GetTransport() == transport)
return true;
// Check if bot is on the transport surface
float probeZ = std::max(bot->GetPositionZ(), transport->GetPositionZ());
Transport* surface = GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), bot->GetPositionX(),
bot->GetPositionY(), probeZ);
if (surface == transport)
{
transport->AddPassenger(bot, true);
bot->StopMovingOnCurrentPos();
EmitDebugMove("TravelPlan:transport-board", "teleport", transport->GetPositionX(),
transport->GetPositionY(), transport->GetPositionZ());
return true;
}
// Not on surface — move toward the transport
float destX = transport->GetPositionX();
float destY = transport->GetPositionY();
float destZ = transport->GetPositionZ();
// Try to find nearest boarding edge
float edgeX, edgeY, edgeZ;
if (FindBoardingPointOnTransport(map, transport, transport, transport->GetPositionX(), transport->GetPositionY(),
transport->GetPositionZ(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), edgeX, edgeY, edgeZ))
{
destX = edgeX;
destY = edgeY;
destZ = edgeZ;
}
// MovePoint without pathfinding (transport is a moving object)
if (MotionMaster* mm = bot->GetMotionMaster())
{
if (bot->IsSitState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
mm->MovePoint(0, destX, destY, destZ, FORCED_MOVEMENT_NONE, 0.0f, 0.0f, false, false);
EmitDebugMove("TravelPlan:transport-walk", "spline", destX, destY, destZ);
}
return false;
}