/* * Copyright (C) 2016+ AzerothCore , 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 #include #include #include #include #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(&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(&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(&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(2 * M_PI); angle += static_cast(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("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("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("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("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(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(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("current target"); Unit* player = *botAI->GetAiObjectContext()->GetValue("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 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("stay time")->Set(0); context->GetValue("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 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& infoList = AI_VALUE(std::list&, "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 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& infoList = AI_VALUE(std::list&, "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& infoList = AI_VALUE(std::list&, "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& 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& 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 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& infoList = AI_VALUE(std::list&, "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& infoList = AI_VALUE(std::list&, "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 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& infoList = AI_VALUE(std::list&, "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& infoList = AI_VALUE(std::list&, "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("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(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 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::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 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::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::CheckSplineProgress(TravelPlan& state) { if (!state.splineActive) return false; // walkPoints may have been cleared by a map transfer or external reset // while the spline was still flagged active; bail out safely. if (state.walkPoints.empty()) { state.splineActive = false; return false; } if (bot->movespline->Finalized()) { G3D::Vector3 const& endPt = state.walkPoints.back(); float distToEnd = bot->GetExactDist(endPt.x, endPt.y, endPt.z); if (distToEnd < 10.0f) { state.splineActive = false; state.walkPoints.clear(); return true; // Arrived } // Spline finalized short of target — interrupted (combat/knockback/etc). // Caller will re-launch. state.splineActive = false; return false; } // Stuck detection if (state.splineStartTime && GetMSTimeDiffToNow(state.splineStartTime) > state.expectedDuration * 2 + (30 * IN_MILLISECONDS)) { G3D::Vector3 const& endPt = state.walkPoints.back(); botAI->TeleportTo(WorldLocation(bot->GetMapId(), endPt.x, endPt.y, endPt.z)); state.splineActive = false; state.walkPoints.clear(); return true; } return false; // Still moving } bool MovementAction::LaunchWalkSpline(TravelPlan& state) { if (state.walkPoints.size() < 2) { 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((totalDist / speed) * IN_MILLISECONDS); bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN); state.splineStartTime = getMSTime(); state.splineActive = true; 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(state.expectedDuration); delay = std::min(delay, static_cast(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 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& walkPoints) { if (walkPoints.size() < 2) return true; std::vector 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 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. // Handle active spline if (state.splineActive) { if (!CheckSplineProgress(state)) { if (state.splineActive) return true; // Still moving else LaunchWalkSpline(state); // Interrupted, re-launch } return true; } if (state.stepIdx >= state.steps.size()) { state.Reset(); 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_PORTAL: { // Pair: source (pointIdx) + dest (pointIdx+1) if (state.stepIdx + 1 >= state.steps.size()) { state.Reset(); return false; } const PathNodePoint& src = state.steps[state.stepIdx]; const PathNodePoint& dst = state.steps[state.stepIdx + 1]; // Already on destination map? if (bot->GetMapId() == dst.point.GetMapId()) { state.stepIdx += 2; return true; } // Walk to portal source float dist = bot->GetExactDist(src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ()); if (dist > INTERACTION_DISTANCE) return MoveTo(src.point.GetMapId(), src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ()); // At portal but didn't cross — natural collision missed. // Abort the plan; stuck-recovery in MoveFarTo will decide // whether to retry or teleport the bot. state.Reset(); return false; } 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(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; } case PathNodeType::NODE_TELEPORT: { // Teleport-spell node: hearthstone (item 6948 → spell 8690) // or class teleport spells (mage portals, druid teleport). // entry holds the spell ID; 8690 is the canonical hearthstone. uint32 spellId = pt.entry; if (!spellId) { state.Reset(); return false; } // Can't cast mid-flight or mid-cast; bail and retry next tick. if (bot->IsInFlight() || bot->IsNonMeleeSpellCast(false)) return true; if (bot->IsMounted()) bot->Dismount(); botAI->RemoveShapeshift(); // 8690 is Hearthstone — the AI has a dedicated action for it // that handles cooldown and inventory checks. Other teleport // spells go through the generic cast path. bool cast = false; if (spellId == 8690) cast = botAI->DoSpecificAction("hearthstone", Event(), true); else if (bot->HasSpell(spellId) && !bot->HasSpellCooldown(spellId)) cast = botAI->CastSpell(spellId, bot); if (!cast) { state.Reset(); return false; } // Cast started — advance past the teleport step; the spell // will move the bot, next tick picks up from wherever it lands. state.stepIdx++; return true; } case PathNodeType::NODE_AREA_TRIGGER: { // Walk into an area trigger, server handles teleport on entry. // Pair: trigger (pointIdx) + dest (pointIdx+1). if (state.stepIdx + 1 >= state.steps.size()) { state.Reset(); return false; } const PathNodePoint& trigger = state.steps[state.stepIdx]; const PathNodePoint& dest = state.steps[state.stepIdx + 1]; // Already on destination map — area trigger fired, advance. if (bot->GetMapId() == dest.point.GetMapId()) { state.stepIdx += 2; return true; } // Walk to the trigger; entering its radius 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 required // quest/key. Abort; stuck-recovery in MoveFarTo decides next. state.Reset(); return false; } case PathNodeType::NODE_FLYING_MOUNT: { // Flying-mount node not implemented — abort. The graph // generator produces these but their execution is // server-specific; we treat them as unreachable rather // than papering over with a teleport. state.Reset(); return false; } default: { LOG_ERROR("playerbots", "[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan", bot->GetName(), static_cast(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 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(dist2d / 0.75f), 10, 28); float const dx = (botX - refX) / static_cast(steps); float const dy = (botY - refY) / static_cast(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; }