mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp

2167 lines
74 KiB
C++

#include "NewRpgBaseAction.h"
#include <limits>
#include <sstream>
#include "BroadcastHelper.h"
#include "ChatHelper.h"
#include "Creature.h"
#include "G3D/Vector2.h"
#include "GameObject.h"
#include "GossipDef.h"
#include "GridTerrainData.h"
#include "IVMapMgr.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "LootMgr.h"
#include "Map.h"
#include "ModelIgnoreFlags.h"
#include "MotionMaster.h"
#include "MoveSplineInitArgs.h"
#include "NewRpgInfo.h"
#include "NewRpgStrategy.h"
#include "Object.h"
#include "ObjectAccessor.h"
#include "OutdoorPvPMgr.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "PathGenerator.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "Position.h"
#include "QuestDef.h"
#include "Random.h"
#include "RandomPlayerbotMgr.h"
#include "SharedDefines.h"
#include "Spell.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "StatsWeightCalculator.h"
#include "Timer.h"
#include "TravelMgr.h"
bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
{
if (dest == WorldPosition())
return false;
// performance optimization
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
return false;
// Already-at-dest short-stop. Below targetPosRecalcDistance the
// move is effectively done — stop any active spline and clear
// the cached path if it pointed here, so we don't keep gliding.
{
float const totalDistance = bot->GetExactDist(dest);
if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (!lastMove.lastPath.empty() &&
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
{
lastMove.clear();
}
bot->StopMoving();
return false;
}
}
// Let an in-flight spline finish before recomputing — prevents
// oscillation when re-resolve produces a slightly different endpoint.
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
{
float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
if (remaining > 10.0f)
{
EmitDebugMove("MoveFar", "spline-plan",
lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
return true;
}
}
}
// 10% lastPath reuse — if the cached path's endpoint is still
// close (within 10%) to the new dest, trim the cached path to
// the bot's current position via makeShortCut and re-dispatch.
// Per-tick re-dispatch of the (trimmed) last path keeps the bot
// on-route after interrupts (knockback, combat, manual move)
// without needing a full replan.
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (!lastMove.lastPath.empty())
{
WorldPosition lastBack = lastMove.lastPath.getBack();
if (lastBack.GetMapId() == dest.GetMapId())
{
float totalDist = bot->GetExactDist(dest);
float maxDistChange = totalDist * 0.10f;
float distFromBotToBack = bot->GetExactDist(&lastBack);
if (lastBack.distance(dest) < maxDistChange && distFromBotToBack > 10.0f)
{
WorldPosition botPos(bot);
lastMove.lastPath.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
// makeShortCut may clear the path if the bot drifted
// too far off (>reactDistance from any waypoint). In
// that case fall through to fresh planning.
if (lastMove.lastPath.empty())
{
EmitDebugMove("MoveFar", "reuse-trim-failed",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
}
if (!lastMove.lastPath.empty())
{
std::vector<WorldPosition> const& pts = lastMove.lastPath.getPointPath();
if (pts.size() >= 2)
{
Movement::PointsArray points;
points.reserve(pts.size());
for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
return DispatchPathPoints(dest, points, "reuse");
}
}
// Path was cleared or collapsed — fall through to fresh planning.
}
}
}
}
float disToDest = bot->GetDistance(dest);
float dis = bot->GetExactDist(dest);
// Try the travel-node graph first for cross-map or > 50y moves;
// fall back to chained mmap probe otherwise. BGs skip the graph.
constexpr float TRAVELNODE_THRESHOLD = 50.0f;
bool tryNodes = sPlayerbotAIConfig.enableTravelNodes &&
!bot->InBattleground() &&
((bot->GetMapId() != dest.GetMapId()) ||
(dis > TRAVELNODE_THRESHOLD));
// Ride the active node plan only if its dest still matches.
// A stale plan would steer the bot past a new target.
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
{
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
botAI->rpgInfo.ClearTravel();
else
return UpdateTravelPlan();
}
// PRIORITY: try the travel-node graph FIRST when the move is
// long enough to need it.
if (tryNodes)
{
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
{
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
}
// Graph returned no plan — fall through to mmap probe.
}
else if (botAI->rpgInfo.HasActiveTravelPlan())
{
// Move dropped below node-first threshold — drop any leftover plan.
botAI->rpgInfo.ClearTravel();
}
// 40-step chained mmap probe — primary for short moves and
// fallback when the node graph returned no plan.
WorldPosition botPos(bot);
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
// Regression guard: prefer cached lastPath if it still ends closer
// to dest than the new probe — catches probes blocked by geometry.
{
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
{
WorldPosition lastBack = lastMove.lastPath.getBack();
if (lastBack.GetMapId() == dest.GetMapId())
{
float cachedToDest = lastBack.distance(dest);
float probeToDest = dest.GetExactDist(probe.back().GetPositionX(),
probe.back().GetPositionY(),
probe.back().GetPositionZ());
if (cachedToDest <= probeToDest)
{
WorldPosition botPosNow(bot);
lastMove.lastPath.makeShortCut(botPosNow, sPlayerbotAIConfig.reactDistance, bot);
if (!lastMove.lastPath.empty())
{
std::vector<WorldPosition> const& pts = lastMove.lastPath.getPointPath();
if (pts.size() >= 2)
{
Movement::PointsArray points;
points.reserve(pts.size());
for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
return DispatchPathPoints(dest, points, "regress-keep");
}
}
}
}
}
}
// Walk the chained probe's full waypoint chain via DispatchPathPoints.
if (!probe.empty() && probe.size() >= 2)
{
WorldPosition stepDest = probe.back();
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
stepDest.GetPositionY(), stepDest.GetPositionZ());
if (endDistToDest + 5.0f < disToDest)
{
// Z gap check: if the probe's last waypoint is well below
// the requested destination Z, the chain walked the ground
// polygon graph toward an elevated target it can't reach
// (quest giver on top of Aldrassil etc.). Refuse to dispatch
// — bot waits instead of tunneling into the visual model.
// 10y tolerates normal terrain variation (ramp ends, hill
// tops) while still catching clearly unreachable elevations.
if (std::fabs(stepDest.GetPositionZ() - dest.GetPositionZ()) > 10.0f)
{
EmitDebugMove("MoveFar", "z-mismatch",
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
return false;
}
Movement::PointsArray points;
points.reserve(probe.size());
for (auto const& wp : probe)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
if (points.size() >= 2)
{
// Mount up if outdoors and not in combat.
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);
return DispatchPathPoints(dest, points, "mmap");
}
}
}
// Probe failed or didn't progress — emit visibility whisper so
// the user can see WHY mmap didn't dispatch.
{
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
(dest.GetExactDist(probe.back().GetPositionX(),
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
if (!probeProgressed)
{
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
EmitDebugMove("MoveFar", reason,
dest.GetPositionX(), dest.GetPositionY(),
dest.GetPositionZ());
}
}
// Empty-probe fallback: single-waypoint MoveTo via engine PathGenerator.
// Cross-map can't be served by a single-map spline — bail.
if (bot->GetMapId() != dest.GetMapId())
return false;
// LOS gate: don't air-walk through trees/walls when the engine
// would otherwise drop to a straight-line BuildShortcut spline.
if (!bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()))
{
EmitDebugMove("MoveFar", "spline-blocked",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
EmitDebugMove("MoveFar", "spline",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
false, false, false, false);
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label)
{
if (points.size() < 2)
return false;
// Prefix trim (cmangos parity: makeShortCut on every dispatch).
// Drop leading waypoints behind the bot's current position so the
// spline begins from where the bot actually is, not from a stale
// planner-start. Picks the waypoint closest to the bot in 3D and
// erases everything before it.
{
float const bx = bot->GetPositionX();
float const by = bot->GetPositionY();
float const bz = bot->GetPositionZ();
float minSq = std::numeric_limits<float>::max();
size_t closest = 0;
for (size_t i = 0; i < points.size(); ++i)
{
float dx = points[i].x - bx;
float dy = points[i].y - by;
float dz = points[i].z - bz;
float sq = dx * dx + dy * dy + dz * dz;
if (sq < minSq)
{
minSq = sq;
closest = i;
}
}
if (closest > 0)
points.erase(points.begin(), points.begin() + closest);
if (points.size() < 2)
return false;
}
// Sparse-segment clip (cmangos parity): if any consecutive segment
// is longer than ~11.18y, truncate the path at that point. Short,
// dense waypoints reduce spline interpolation across visual
// obstacles between sparse points; bot re-plans from a closer
// position next tick.
{
constexpr float SPARSE_SEG_SQ = 125.0f; // sqrt(125) ≈ 11.18y
for (size_t i = 1; i < points.size(); ++i)
{
float dx = points[i].x - points[i - 1].x;
float dy = points[i].y - points[i - 1].y;
float dz = points[i].z - points[i - 1].z;
if (dx * dx + dy * dy + dz * dz > SPARSE_SEG_SQ)
{
points.resize(i);
break;
}
}
if (points.size() < 2)
return false;
}
// LOS gate: reject paths whose segments pass through visual
// geometry. mmap is blind to M2 models (trees, decorative props)
// and will route through them; vmap LOS catches the cases that
// matter — solid trunks, walls, terrain features.
if (Map* map = bot->GetMap())
{
float const eye = bot->GetCollisionHeight();
for (size_t i = 0; i + 1 < points.size(); ++i)
{
if (!map->isInLineOfSight(points[i].x, points[i].y, points[i].z + eye,
points[i + 1].x, points[i + 1].y, points[i + 1].z + eye,
bot->GetPhaseMask(),
LINEOFSIGHT_ALL_CHECKS,
VMAP::ModelIgnoreFlags::Nothing))
{
EmitDebugMove("MoveFar", "blocked",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return false;
}
}
}
// Save planner output before clip/fixup so next-tick reuse sees
// the original intent, not a truncated tail.
{
LastMovement& lm = AI_VALUE(LastMovement&, "last movement");
std::vector<WorldPosition> wpts;
wpts.reserve(points.size());
for (auto const& pt : points)
wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z);
lm.setPath(TravelPath(wpts));
}
// Underwater fixup: lift submerged waypoints to the surface,
// unless the destination is itself underwater.
if (Map* map = bot->GetMap())
{
WorldPosition destWp = dest;
if (!destWp.isUnderWater())
{
for (auto& pt : points)
{
WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z);
if (wp.isUnderWater())
{
float surface = map->GetWaterLevel(pt.x, pt.y);
if (surface != INVALID_HEIGHT && surface > pt.z)
pt.z = surface;
}
}
}
}
for (auto& pt : points)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// ClipPath — truncate path at first hostile creature within its
// own attack range. Skipped while in combat or dead.
if (botAI->GetState() != BOT_STATE_COMBAT && bot->IsAlive())
{
GuidVector targets = AI_VALUE(GuidVector, "possible targets");
if (!targets.empty())
{
size_t clipAt = points.size();
for (size_t i = 0; i < points.size() && clipAt == points.size(); ++i)
{
for (ObjectGuid const& guid : targets)
{
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive())
continue;
Creature* cre = unit->ToCreature();
if (!cre)
continue;
if (unit->GetLevel() > bot->GetLevel() + 5)
continue;
float range = cre->GetAttackDistance(bot);
float dx = unit->GetPositionX() - points[i].x;
float dy = unit->GetPositionY() - points[i].y;
float dz = unit->GetPositionZ() - points[i].z;
if (dx * dx + dy * dy + dz * dz > range * range)
continue;
if (!unit->IsWithinLOSInMap(bot))
continue;
clipAt = i;
break;
}
}
if (clipAt < points.size() && clipAt + 1 < points.size())
points.erase(points.begin() + clipAt + 1, points.end());
}
}
if (points.size() < 2)
return false;
G3D::Vector3 const& last = points.back();
float totalDist = 0.f;
for (size_t i = 1; i < points.size(); ++i)
totalDist += (points[i] - points[i - 1]).length();
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Skip cosmetic walking for random bots with no nearby player —
// teleport to the path tail and schedule a cooldown instead.
if (sRandomPlayerbotMgr.IsRandomBot(bot))
{
WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
time_t now = time(nullptr);
if (totalDist > sPlayerbotAIConfig.reactDistance &&
lastMove.nextTeleport <= now &&
!botAI->HasPlayerNearby(&tail))
{
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
lastMove.nextTeleport = now + (time_t)(totalDist / speed);
EmitDebugMove("MoveFar", "teleport",
tail.GetPositionX(), tail.GetPositionY(), tail.GetPositionZ());
WorldPosition botPos(bot);
return bot->TeleportTo(dest.GetMapId(),
tail.GetPositionX(), tail.GetPositionY(),
tail.GetPositionZ(),
botPos.getAngleTo(tail));
}
}
// Match master's walk pace when they're nearby and walking.
ForcedMovement moveMode = FORCED_MOVEMENT_RUN;
if (sPlayerbotAIConfig.walkDistance > 0.0f)
{
if (Player* master = botAI->GetMaster())
{
if (bot->IsFriendlyTo(master) && master->IsWalking() &&
bot->GetExactDist2d(master) < sPlayerbotAIConfig.walkDistance)
{
moveMode = FORCED_MOVEMENT_WALK;
}
}
}
// Clear emote/sit/cast so the spline can begin cleanly.
bot->ClearEmoteState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
bot->InterruptNonMeleeSpells(true);
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, moveMode);
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
// WaitForReach: leave ~10y headroom on long paths.
float waitDist = totalDist > sPlayerbotAIConfig.reactDistance
? std::max(totalDist - 10.0f, 0.0f) : totalDist;
UnitMoveType const speedType = (moveMode == FORCED_MOVEMENT_WALK) ? MOVE_WALK : MOVE_RUN;
float speed = std::max(bot->GetSpeed(speedType), 0.1f);
float duration = 1000.0f * (waitDist / speed) + sPlayerbotAIConfig.reactDelay;
duration = std::min(duration, (float)sPlayerbotAIConfig.maxWaitForMove);
if (duration < 0.0f)
duration = 0.0f;
lastMove.Set(bot->GetMapId(), last.x, last.y, last.z,
bot->GetOrientation(), (uint32)duration,
MovementPriority::MOVEMENT_NORMAL);
return true;
}
void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
{
TravelPlan& plan = botAI->rpgInfo.travelPlan;
GetTravelPlan(plan, dest);
}
bool NewRpgBaseAction::UpdateTravelPlan()
{
TravelPlan& plan = botAI->rpgInfo.travelPlan;
bool result = ExecuteTravelPlan(plan);
if (!plan.IsActive())
botAI->rpgInfo.ClearTravel();
return result;
}
bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance)
{
WorldObject* object = botAI->GetWorldObject(guid);
if (!object)
return false;
Map* map = bot->GetMap();
if (!map)
return false;
// 8-angle deterministic iteration around the target. For each angle,
// validate the candidate against the navmesh with a strict ground-only
// filter (NAV_GROUND, exclude STEEP/WATER/MAGMA/SLIME). Reject if no
// valid poly within 5y XY+Z or if the snap drifts the Z by >10y.
// First angle that passes both LOS and navmesh-snap wins.
dtNavMeshQuery const* navMeshQuery =
map->GetMapCollisionData().GetMMapData().GetNavMeshQuery();
float const baseAngle = object->GetAngle(bot);
for (float step = 0.0f; step < 2.0f * static_cast<float>(M_PI);
step += static_cast<float>(M_PI) / 4.0f)
{
float const angle = baseAngle + step;
float x = object->GetPositionX() + std::cos(angle) * distance;
float y = object->GetPositionY() + std::sin(angle) * distance;
float z = object->GetPositionZ();
// LOS check at eye height.
if (!bot->IsWithinLOS(x, y, z + bot->GetCollisionHeight()))
continue;
// Strict navmesh-snap validation (cmangos ClosestCorrectPoint port).
if (navMeshQuery)
{
dtQueryFilter filter;
filter.setIncludeFlags(NAV_GROUND);
filter.setExcludeFlags(NAV_GROUND_STEEP | NAV_WATER | NAV_MAGMA | NAV_SLIME);
float const point[VERTEX_SIZE] = { y, z, x };
float const extents[VERTEX_SIZE] = { 5.0f, 5.0f, 5.0f };
float closest[VERTEX_SIZE] = { 0.0f, 0.0f, 0.0f };
dtPolyRef polyRef = INVALID_POLYREF;
if (!dtStatusSucceed(navMeshQuery->findNearestPoly(
point, extents, &filter, &polyRef, closest)) ||
polyRef == INVALID_POLYREF)
continue;
float const snappedZ = closest[1];
if (std::fabs(snappedZ - z) > 10.0f)
continue;
x = closest[2];
y = closest[0];
z = snappedZ;
}
return MoveFarTo(WorldPosition(object->GetMapId(), x, y, z));
}
return false;
}
bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, WorldObject* center)
{
if (IsWaitingForLastMove(priority))
return false;
Map* map = bot->GetMap();
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
// Retry random samples so one bad roll doesn't lock the bot in place.
for (int attempt = 0; attempt < 8; ++attempt)
{
float distance = (0.4f + rand_norm() * 0.6f) * moveStep;
float angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float dx = x + distance * cos(angle);
float dy = y + distance * sin(angle);
float dz = z;
PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK, /*forceDestination=*/false);
if (!path.reachable)
continue;
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
continue;
if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight()))
continue;
// Reject samples whose straight-line passes through visual
// obstacles (trees, models) that aren't in the navmesh. The
// smooth-path step can otherwise interpolate a waypoint inside
// a tree, making the bot visibly walk through it.
if (!bot->IsWithinLOS(dx, dy, dz))
continue;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority);
if (moved)
{
EmitDebugMove("MoveRandomNear", "mmap", dx, dy, dz);
return true;
}
}
EmitDebugMove("MoveRandomNear", "all-fail", x, y, z);
return false;
}
bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority)
{
AI_VALUE(LastMovement&, "last movement")
.Set(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetOrientation(),
duration, priority);
return true;
}
bool NewRpgBaseAction::TakeFlight(std::vector<uint32> const& taxiNodes, Creature* flightMaster)
{
if (taxiNodes.size() < 2 || !flightMaster || !flightMaster->IsAlive())
return false;
botAI->RemoveShapeshift();
if (bot->IsMounted())
bot->Dismount();
if (!bot->ActivateTaxiPathTo(taxiNodes, flightMaster, 0))
{
LOG_DEBUG("playerbots", "[New RPG] Bot {} flight ({} nodes, {} to {}) failed",
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
return false;
}
LOG_DEBUG("playerbots", "[New RPG] Bot {} taking flight ({} nodes, {} to {})",
bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back());
EmitDebugMove("TravelPlan:flight", "taxi", flightMaster->GetPositionX(), flightMaster->GetPositionY(),
flightMaster->GetPositionZ());
return true;
}
/// @TODO: Fix redundant code
/// Quest related method refer to TalkToQuestGiverAction.h
bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid)
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid);
if (!object || !bot->CanInteractWithQuestGiver(object))
return false;
// Creature* creature = bot->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_NONE);
// if (creature)
// {
// WorldPacket packet(CMSG_GOSSIP_HELLO);
// packet << guid;
// bot->GetSession()->HandleGossipHelloOpcode(packet);
// }
bot->PrepareQuestMenu(guid);
const QuestMenu& menu = bot->PlayerTalkClass->GetQuestMenu();
if (menu.Empty())
return true;
for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++)
{
const QuestMenuItem& item = menu.GetItem(idx);
const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId);
if (!quest)
continue;
const QuestStatus& status = bot->GetQuestStatus(item.QuestId);
if (status == QUEST_STATUS_NONE && bot->CanTakeQuest(quest, false) && bot->CanAddQuest(quest, false) &&
IsQuestWorthDoing(quest) && IsQuestCapableDoing(quest))
{
AcceptQuest(quest, guid);
if (botAI->GetMaster())
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"new_rpg_quest_accepted",
"Quest accepted %quest",
{{"%quest", ChatHelper::FormatQuest(quest)}}));
BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest);
botAI->rpgStatistic.questAccepted++;
LOG_DEBUG("playerbots", "[New RPG] {} accept quest {}", bot->GetName(), quest->GetQuestId());
}
if (status == QUEST_STATUS_COMPLETE && bot->CanRewardQuest(quest, 0, false))
{
TurnInQuest(quest, guid);
if (botAI->GetMaster())
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"new_rpg_quest_rewarded",
"Quest rewarded %quest",
{{"%quest", ChatHelper::FormatQuest(quest)}}));
BroadcastHelper::BroadcastQuestTurnedIn(botAI, bot, quest);
botAI->rpgStatistic.questRewarded++;
LOG_DEBUG("playerbots", "[New RPG] {} turned in quest {}", bot->GetName(), quest->GetQuestId());
}
}
return true;
}
bool NewRpgBaseAction::CanInteractWithQuestGiver(Object* questGiver)
{
// This is a variant of Player::CanInteractWithQuestGiver
// that removes the distance check and keeps all other checks
switch (questGiver->GetTypeId())
{
case TYPEID_UNIT: // Player::GetNPCIfCanInteractWith
{
ObjectGuid guid = questGiver->GetGUID();
// unit checks
if (!guid)
return false;
if (!bot->IsInWorld() || bot->IsDuringRemoveFromWorld())
return false;
if (bot->IsInFlight())
return false;
// exist (we need look pets also for some interaction (quest/etc)
Creature* creature = ObjectAccessor::GetCreatureOrPetOrVehicle(*bot, guid);
if (!creature)
return false;
// Deathstate checks
if (!bot->IsAlive() &&
!(creature->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_VISIBLE_TO_GHOSTS))
return false;
// alive or spirit healer
if (!creature->IsAlive() &&
!(creature->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_INTERACT_WHILE_DEAD))
return false;
// appropriate npc type
if (!creature->HasNpcFlag(UNIT_NPC_FLAG_QUESTGIVER))
return false;
// not allow interaction under control, but allow with own pets
if (creature->GetCharmerGUID())
return false;
// xinef: perform better check
if (creature->GetReactionTo(bot) <= REP_UNFRIENDLY)
return false;
return true;
}
case TYPEID_GAMEOBJECT: // Player::GetGameObjectIfCanInteractWith
{
ObjectGuid guid = questGiver->GetGUID();
if (GameObject* go = bot->GetMap()->GetGameObject(guid))
{
if (go->GetGoType() == GAMEOBJECT_TYPE_QUESTGIVER)
{
// Players cannot interact with gameobjects that use the "Point" icon
if (go->GetGOInfo()->IconName == "Point")
return false;
return true;
}
}
return false;
}
// unused for now
// case TYPEID_PLAYER:
// return bot->IsAlive() && questGiver->ToPlayer()->IsAlive();
// case TYPEID_ITEM:
// return bot->IsAlive();
default:
break;
}
return false;
}
bool NewRpgBaseAction::IsWithinInteractionDist(Object* questGiver)
{
// This is a variant of Player::CanInteractWithQuestGiver
// that only keep the distance check
switch (questGiver->GetTypeId())
{
case TYPEID_UNIT:
{
ObjectGuid guid = questGiver->GetGUID();
// unit checks
if (!guid)
return false;
// exist (we need look pets also for some interaction (quest/etc)
Creature* creature = ObjectAccessor::GetCreatureOrPetOrVehicle(*bot, guid);
if (!creature)
return false;
if (!creature->IsWithinDistInMap(bot, INTERACTION_DISTANCE))
return false;
return true;
}
case TYPEID_GAMEOBJECT:
{
ObjectGuid guid = questGiver->GetGUID();
if (GameObject* go = bot->GetMap()->GetGameObject(guid))
{
if (go->IsWithinDistInMap(bot))
{
return true;
}
}
return false;
}
// case TYPEID_PLAYER:
// return bot->IsAlive() && questGiver->ToPlayer()->IsAlive();
// case TYPEID_ITEM:
// return bot->IsAlive();
default:
break;
}
return false;
}
bool NewRpgBaseAction::AcceptQuest(Quest const* quest, ObjectGuid guid)
{
WorldPacket p(CMSG_QUESTGIVER_ACCEPT_QUEST);
uint32 unk1 = 0;
p << guid << quest->GetQuestId() << unk1;
p.rpos(0);
bot->GetSession()->HandleQuestgiverAcceptQuestOpcode(p);
return true;
}
bool NewRpgBaseAction::TurnInQuest(Quest const* quest, ObjectGuid guid)
{
uint32 questID = quest->GetQuestId();
if (bot->GetQuestRewardStatus(questID))
{
return false;
}
if (!bot->CanRewardQuest(quest, false))
{
return false;
}
bot->PlayDistanceSound(621);
WorldPacket p(CMSG_QUESTGIVER_CHOOSE_REWARD);
p << guid << quest->GetQuestId();
if (quest->GetRewChoiceItemsCount() <= 1)
{
p << 0;
bot->GetSession()->HandleQuestgiverChooseRewardOpcode(p);
}
else
{
uint32 bestId = BestRewardIndex(quest);
p << bestId;
bot->GetSession()->HandleQuestgiverChooseRewardOpcode(p);
}
return true;
}
uint32 NewRpgBaseAction::BestRewardIndex(Quest const* quest)
{
ItemIds returnIds;
ItemUsage bestUsage = ITEM_USAGE_NONE;
if (quest->GetRewChoiceItemsCount() <= 1)
return 0;
else
{
for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i)
{
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]);
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE)
bestUsage = ITEM_USAGE_EQUIP;
else if (usage == ITEM_USAGE_BAD_EQUIP && bestUsage != ITEM_USAGE_EQUIP)
bestUsage = usage;
else if (usage != ITEM_USAGE_NONE && bestUsage == ITEM_USAGE_NONE)
bestUsage = usage;
}
StatsWeightCalculator calc(bot);
uint32 best = 0;
float bestScore = 0;
for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i)
{
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]);
if (usage == bestUsage || usage == ITEM_USAGE_REPLACE)
{
float score = calc.CalculateItem(quest->RewardChoiceItemId[i]);
if (score > bestScore)
{
bestScore = score;
best = i;
}
}
}
return best;
}
}
bool NewRpgBaseAction::IsQuestWorthDoing(Quest const* quest)
{
bool isLowLevelQuest =
bot->GetLevel() > (bot->GetQuestLevel(quest) + sWorld->getIntConfig(CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF));
if (isLowLevelQuest)
return false;
if (quest->IsRepeatable())
return false;
if (quest->IsSeasonal())
return false;
return true;
}
bool NewRpgBaseAction::IsQuestCapableDoing(Quest const* quest)
{
bool highLevelQuest = bot->GetLevel() + 3 < bot->GetQuestLevel(quest);
if (highLevelQuest)
return false;
// Elite quest and dungeon quest etc
if (quest->GetType() != 0)
return false;
// now we only capable of doing solo quests
if (quest->GetSuggestedPlayers() >= 2)
return false;
return true;
}
bool NewRpgBaseAction::OrganizeQuestLog()
{
int32 freeSlotNum = 0;
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
freeSlotNum++;
}
// it's ok if we have two more free slots
if (freeSlotNum >= 2)
return false;
int32 dropped = 0;
// remove quests that not worth doing or not capable of doing
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
continue;
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
if (!IsQuestWorthDoing(quest) || !IsQuestCapableDoing(quest) ||
bot->GetQuestStatus(questId) == QUEST_STATUS_FAILED)
{
LOG_DEBUG("playerbots", "[New RPG] {} drop quest {}", bot->GetName(), questId);
WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST);
packet << (uint8)i;
bot->GetSession()->HandleQuestLogRemoveQuest(packet);
if (botAI->GetMaster())
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"new_rpg_quest_dropped",
"Quest dropped %quest",
{{"%quest", ChatHelper::FormatQuest(quest)}}));
botAI->rpgStatistic.questDropped++;
dropped++;
}
}
// drop more than 8 quests at once to avoid repeated accept and drop
if (dropped >= 8)
return true;
// remove festival/class quests and quests in different zone
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
continue;
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
const int64_t botZoneId = this->bot->GetZoneId();
if (quest->GetZoneOrSort() < 0 || (quest->GetZoneOrSort() > 0 && quest->GetZoneOrSort() != botZoneId))
{
LOG_DEBUG("playerbots", "[New RPG] {} drop quest {}", bot->GetName(), questId);
WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST);
packet << (uint8)i;
bot->GetSession()->HandleQuestLogRemoveQuest(packet);
if (botAI->GetMaster())
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"new_rpg_quest_dropped",
"Quest dropped %quest",
{{"%quest", ChatHelper::FormatQuest(quest)}}));
botAI->rpgStatistic.questDropped++;
dropped++;
}
}
if (dropped >= 8)
return true;
// clear quests log
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
continue;
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
LOG_DEBUG("playerbots", "[New RPG] {} drop quest {}", bot->GetName(), questId);
WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST);
packet << (uint8)i;
bot->GetSession()->HandleQuestLogRemoveQuest(packet);
if (botAI->GetMaster())
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"new_rpg_quest_dropped",
"Quest dropped %quest",
{{"%quest", ChatHelper::FormatQuest(quest)}}));
botAI->rpgStatistic.questDropped++;
}
return true;
}
bool NewRpgBaseAction::SearchQuestGiverAndAcceptOrReward()
{
OrganizeQuestLog();
if (ObjectGuid npcOrGo = ChooseNpcOrGameObjectToInteract(true, 80.0f))
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, npcOrGo);
if (bot->CanInteractWithQuestGiver(object))
{
InteractWithNpcOrGameObjectForQuest(npcOrGo);
ForceToWait(5000);
return true;
}
return MoveWorldObjectTo(npcOrGo);
}
return false;
}
static bool BotNeedsItemForQuest(Player* bot, uint32 itemId)
{
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i)
{
if (!quest->RequiredItemCount[i])
continue;
if (qs.ItemCount[i] >= quest->RequiredItemCount[i])
continue;
if (quest->RequiredItemId[i] == itemId)
return true;
}
}
return false;
}
bool NewRpgBaseAction::TryLootQuestGO(ObjectGuid& pursuedGO, float searchRange)
{
if (!bot->IsAlive() || bot->IsBeingTeleported() || bot->IsInFlight() ||
bot->GetVehicle() || bot->GetTransport())
return false;
// valid = spawned, selectable, holds a quest item we still need.
// INTERACT_COND is fine — ConditionMgr already gates on quest state.
auto isValidTarget = [&](GameObject* go) -> bool
{
if (!go || !go->IsInWorld() || !go->isSpawned())
return false;
if (!(go->GetPhaseMask() & bot->GetPhaseMask()))
return false;
if (go->HasGameObjectFlag(GO_FLAG_NOT_SELECTABLE))
return false;
GameObjectTemplate const* info = go->GetGOInfo();
if (!info)
return false;
// per-player quest drops via gameobject_questitem (Webwood Eggs…)
if (GameObjectQuestItemList const* items =
sObjectMgr->GetGameObjectQuestItemList(go->GetEntry()))
{
for (size_t i = 0; i < MAX_GAMEOBJECT_QUEST_ITEMS && i < items->size(); ++i)
{
uint32 itemId = uint32((*items)[i]);
if (!itemId)
continue;
if (BotNeedsItemForQuest(bot, itemId))
return true;
}
}
// standard loot template (chests, fishing holes)
if (uint32 lootId = info->GetLootId())
{
if (LootTemplates_Gameobject.HaveQuestLootForPlayer(lootId, bot))
return true;
}
return false;
};
// 2.5y sits inside the 3.5y loot gate with headroom
const float lootRange = 2.5f;
// stick with the committed target — re-picking nearest every tick
// causes zig-zag walks in dense spawn clusters
if (pursuedGO)
{
GameObject* existing = botAI->GetGameObject(pursuedGO);
if (existing && isValidTarget(existing) &&
bot->GetDistance(existing) <= searchRange)
{
if (bot->GetDistance(existing) > lootRange)
return MoveWorldObjectTo(existing->GetGUID(), lootRange);
// in range — loot strategy opens it
return true;
}
pursuedGO.Clear();
}
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
if (possibleGameObjects.empty())
return false;
GameObject* best = nullptr;
float bestDist = searchRange;
for (ObjectGuid guid : possibleGameObjects)
{
GameObject* go = botAI->GetGameObject(guid);
if (!isValidTarget(go))
continue;
float d = bot->GetDistance(go);
if (d >= bestDist)
continue;
best = go;
bestDist = d;
}
if (!best)
return false;
// commit
pursuedGO = best->GetGUID();
if (bot->GetDistance(best) > lootRange)
return MoveWorldObjectTo(best->GetGUID(), lootRange);
// in range — consume the tick so we don't fall through to wander
return true;
}
bool NewRpgBaseAction::TryUseQuestGO(ObjectGuid& pursuedGO, float searchRange)
{
if (!bot->IsAlive() || bot->IsBeingTeleported() || bot->IsInFlight() ||
bot->GetVehicle() || bot->GetTransport())
return false;
std::unordered_set<uint32> neededGoEntries;
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = quest->RequiredNpcOrGo[i];
if (entry >= 0)
continue;
if (qs.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
continue;
neededGoEntries.insert(uint32(-entry));
}
}
if (neededGoEntries.empty())
return false;
auto isValidTarget = [&](GameObject* go) -> bool
{
if (!go || !go->IsInWorld() || !go->isSpawned())
return false;
if (!(go->GetPhaseMask() & bot->GetPhaseMask()))
return false;
if (go->HasGameObjectFlag(GO_FLAG_NOT_SELECTABLE))
return false;
return neededGoEntries.count(go->GetEntry()) > 0;
};
// commitment first
if (pursuedGO)
{
GameObject* existing = botAI->GetGameObject(pursuedGO);
if (existing && isValidTarget(existing) &&
bot->GetDistance(existing) <= searchRange)
{
if (bot->GetDistance(existing) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(existing->GetGUID(), INTERACTION_DISTANCE);
existing->Use(bot);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
pursuedGO.Clear();
}
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
GameObject* best = nullptr;
float bestDist = searchRange;
for (ObjectGuid guid : possibleGameObjects)
{
GameObject* go = botAI->GetGameObject(guid);
if (!isValidTarget(go))
continue;
float d = bot->GetDistance(go);
if (d >= bestDist)
continue;
best = go;
bestDist = d;
}
if (!best)
return false;
pursuedGO = best->GetGUID();
if (bot->GetDistance(best) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(best->GetGUID(), INTERACTION_DISTANCE);
best->Use(bot);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
bool NewRpgBaseAction::TryUseQuestItem(ObjectGuid& pursuedGO, ObjectGuid& pursuedTarget, float searchRange)
{
if (!bot->IsAlive() || bot->IsBeingTeleported() || bot->IsInFlight() ||
bot->GetVehicle() || bot->GetTransport())
return false;
std::unordered_set<uint32> candidateItemEntries;
// src items (the quest gave the bot a single item to use); branch C
// (self/area cast) is only safe to fire on these — auto-firing every
// ItemDrop on self can burn kill-credit sentinels and trigger
// unintended scripted side effects.
std::unordered_set<uint32> srcItemEntries;
std::unordered_set<uint32> neededCreatureEntries;
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
if (uint32 src = quest->GetSrcItemId())
{
candidateItemEntries.insert(src);
srcItemEntries.insert(src);
}
// handed out by the quest (brands, flares, nets, standards)
for (int i = 0; i < QUEST_SOURCE_ITEM_IDS_COUNT; ++i)
{
if (uint32 drop = quest->ItemDrop[i])
candidateItemEntries.insert(drop);
}
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = quest->RequiredNpcOrGo[i];
if (entry <= 0)
continue;
if (qs.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
continue;
neededCreatureEntries.insert(uint32(entry));
}
}
if (candidateItemEntries.empty())
return false;
for (uint32 itemEntry : candidateItemEntries)
{
Item* item = bot->GetItemByEntry(itemEntry);
if (!item)
continue;
ItemTemplate const* proto = item->GetTemplate();
if (!proto)
continue;
uint32 useSpellId = 0;
for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
{
if (proto->Spells[i].SpellTrigger != ITEM_SPELLTRIGGER_ON_USE)
continue;
if (proto->Spells[i].SpellId <= 0)
continue;
useSpellId = proto->Spells[i].SpellId;
break;
}
if (!useSpellId)
continue;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(useSpellId);
if (!spellInfo)
continue;
// A: spell needs a focus GO (moonwell / lectern / anvil)
if (uint32 focusId = spellInfo->RequiresSpellFocus)
{
auto focusRadius = [](GameObject* go) -> float
{
GameObjectTemplate const* info = go->GetGOInfo();
// half radius so we end up inside, not on the rim
return std::max<float>(1.0f, float(info->spellFocus.dist) * 0.5f);
};
auto isValidFocus = [&](GameObject* go) -> bool
{
if (!go || !go->IsInWorld() || !go->isSpawned())
return false;
if (!(go->GetPhaseMask() & bot->GetPhaseMask()))
return false;
if (go->HasGameObjectFlag(GO_FLAG_NOT_SELECTABLE))
return false;
GameObjectTemplate const* info = go->GetGOInfo();
if (!info || info->type != GAMEOBJECT_TYPE_SPELL_FOCUS)
return false;
return info->spellFocus.focusId == focusId;
};
// commitment first
if (pursuedGO)
{
GameObject* existing = botAI->GetGameObject(pursuedGO);
if (existing && isValidFocus(existing) &&
bot->GetDistance(existing) <= searchRange)
{
float radius = focusRadius(existing);
if (bot->GetDistance(existing) > radius)
return MoveWorldObjectTo(existing->GetGUID(), radius);
SpellCastTargets targets;
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
pursuedGO.Clear();
}
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
GameObject* best = nullptr;
float bestDist = searchRange;
float bestRadius = INTERACTION_DISTANCE;
for (ObjectGuid guid : possibleGameObjects)
{
GameObject* go = botAI->GetGameObject(guid);
if (!isValidFocus(go))
continue;
float d = bot->GetDistance(go);
if (d >= bestDist)
continue;
best = go;
bestDist = d;
bestRadius = focusRadius(go);
}
if (best)
{
pursuedGO = best->GetGUID();
if (bot->GetDistance(best) > bestRadius)
return MoveWorldObjectTo(best->GetGUID(), bestRadius);
SpellCastTargets targets;
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedGO.Clear();
return true;
}
continue;
}
// B: spell needs a unit target — walk to the required creature
if (spellInfo->NeedsExplicitUnitTarget() && !neededCreatureEntries.empty())
{
auto isValidCreature = [&](Creature* c) -> bool
{
if (!c || !c->IsInWorld() || !c->IsAlive())
return false;
if (!(c->GetPhaseMask() & bot->GetPhaseMask()))
return false;
return neededCreatureEntries.count(c->GetEntry()) > 0;
};
// commitment first
if (pursuedTarget)
{
Creature* existing = botAI->GetCreature(pursuedTarget);
if (existing && isValidCreature(existing) &&
bot->GetDistance(existing) <= searchRange)
{
if (bot->GetDistance(existing) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(existing->GetGUID(), INTERACTION_DISTANCE);
SpellCastTargets targets;
targets.SetUnitTarget(existing);
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedTarget.Clear();
return true;
}
pursuedTarget.Clear();
}
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
Creature* best = nullptr;
float bestDist = searchRange;
for (ObjectGuid guid : possibleTargets)
{
Creature* c = botAI->GetCreature(guid);
if (!isValidCreature(c))
continue;
float d = bot->GetDistance(c);
if (d >= bestDist)
continue;
best = c;
bestDist = d;
}
if (best)
{
pursuedTarget = best->GetGUID();
if (bot->GetDistance(best) > INTERACTION_DISTANCE)
return MoveWorldObjectTo(best->GetGUID(), INTERACTION_DISTANCE);
SpellCastTargets targets;
targets.SetUnitTarget(best);
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
pursuedTarget.Clear();
return true;
}
continue;
}
// C: self / area — fire at bot's position. Restrict to GetSrcItemId
// items (the single item the quest hands the bot for self-use, e.g.
// a potion). ItemDrop entries can be kill-credit sentinels or
// scripted items that should never be auto-used on self.
if (!srcItemEntries.count(itemEntry))
continue;
SpellCastTargets targets;
if (spellInfo->IsTargetingArea())
targets.SetDst(*bot);
else
targets.SetUnitTarget(bot);
bot->CastItemUseSpell(item, targets, 1, 0);
ForceToWait(2000);
return true;
}
return false;
}
bool NewRpgBaseAction::HasNearbyQuestMob(float range)
{
// kill objectives + mobs that drop required quest items
std::unordered_set<uint32> neededCreatureEntries;
std::unordered_set<uint32> neededItemIds;
for (uint16 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
if (bot->GetQuestStatus(questId) != QUEST_STATUS_INCOMPLETE)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
QuestStatusData const& qs = bot->getQuestStatusMap().at(questId);
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i)
{
int32 entry = quest->RequiredNpcOrGo[i];
if (entry <= 0)
continue;
if (qs.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i])
continue;
neededCreatureEntries.insert(uint32(entry));
}
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i)
{
if (!quest->RequiredItemCount[i])
continue;
if (qs.ItemCount[i] >= quest->RequiredItemCount[i])
continue;
if (quest->RequiredItemId[i])
neededItemIds.insert(quest->RequiredItemId[i]);
}
}
if (neededCreatureEntries.empty() && neededItemIds.empty())
return false;
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible targets");
for (ObjectGuid guid : possibleTargets)
{
Creature* c = botAI->GetCreature(guid);
if (!c || !c->IsInWorld() || !c->IsAlive())
continue;
if (!(c->GetPhaseMask() & bot->GetPhaseMask()))
continue;
if (bot->GetDistance(c) > range)
continue;
// direct kill objective
if (neededCreatureEntries.count(c->GetEntry()))
return true;
// drops a required quest item — HaveQuestLootForPlayer
// already filters by what this player still needs
if (!neededItemIds.empty())
{
CreatureTemplate const* tmpl = c->GetCreatureTemplate();
if (tmpl && tmpl->lootid &&
LootTemplates_Creature.HaveQuestLootForPlayer(tmpl->lootid, bot))
{
return true;
}
}
}
return false;
}
ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit)
{
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
if (possibleTargets.empty() && possibleGameObjects.empty())
return ObjectGuid();
WorldObject* nearestObject = nullptr;
for (ObjectGuid& guid : possibleTargets)
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid);
if (!object || !object->IsInWorld())
continue;
if (distanceLimit && bot->GetDistance(object) > distanceLimit)
continue;
if (CanInteractWithQuestGiver(object) && HasQuestToAcceptOrReward(object))
{
if (!nearestObject || bot->GetExactDist(nearestObject) > bot->GetExactDist(object))
nearestObject = object;
break;
}
}
for (ObjectGuid& guid : possibleGameObjects)
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid);
if (!object || !object->IsInWorld())
continue;
if (distanceLimit && bot->GetDistance(object) > distanceLimit)
continue;
if (CanInteractWithQuestGiver(object) && HasQuestToAcceptOrReward(object))
{
if (!nearestObject || bot->GetExactDist(nearestObject) > bot->GetExactDist(object))
nearestObject = object;
break;
}
}
if (nearestObject)
return nearestObject->GetGUID();
// No questgiver to accept or reward
if (questgiverOnly)
return ObjectGuid();
if (possibleTargets.empty())
return ObjectGuid();
int idx = urand(0, possibleTargets.size() - 1);
ObjectGuid guid = possibleTargets[idx];
WorldObject* object = ObjectAccessor::GetCreatureOrPetOrVehicle(*bot, guid);
if (!object)
object = ObjectAccessor::GetGameObject(*bot, guid);
if (object && object->IsInWorld())
{
return object->GetGUID();
}
return ObjectGuid();
}
bool NewRpgBaseAction::HasQuestToAcceptOrReward(WorldObject* object)
{
ObjectGuid guid = object->GetGUID();
bot->PrepareQuestMenu(guid);
const QuestMenu& menu = bot->PlayerTalkClass->GetQuestMenu();
if (menu.Empty())
return false;
for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++)
{
const QuestMenuItem& item = menu.GetItem(idx);
const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId);
if (!quest)
continue;
const QuestStatus& status = bot->GetQuestStatus(item.QuestId);
if (status == QUEST_STATUS_COMPLETE && bot->CanRewardQuest(quest, 0, false))
{
return true;
}
}
for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++)
{
const QuestMenuItem& item = menu.GetItem(idx);
const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId);
if (!quest)
continue;
const QuestStatus& status = bot->GetQuestStatus(item.QuestId);
if (status == QUEST_STATUS_NONE && bot->CanTakeQuest(quest, false) && bot->CanAddQuest(quest, false) &&
IsQuestWorthDoing(quest) && IsQuestCapableDoing(quest))
{
return true;
}
}
return false;
}
static std::vector<float> GenerateRandomWeights(int n)
{
std::vector<float> weights(n);
float sum = 0.0;
for (int i = 0; i < n; ++i)
{
weights[i] = rand_norm();
sum += weights[i];
}
for (int i = 0; i < n; ++i)
{
weights[i] /= sum;
}
return weights;
}
bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete)
{
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
return false;
const QuestPOIVector* poiVector = sObjectMgr->GetQuestPOIVector(questId);
if (!poiVector)
{
return false;
}
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
if (toComplete && q_status.Status == QUEST_STATUS_COMPLETE)
{
for (const QuestPOI& qPoi : *poiVector)
{
if (qPoi.MapId != bot->GetMapId())
continue;
// not the poi pos to reward quest
if (qPoi.ObjectiveIndex != -1)
continue;
if (qPoi.points.size() == 0)
continue;
float dx = 0, dy = 0;
std::vector<float> weights = GenerateRandomWeights(qPoi.points.size());
for (size_t i = 0; i < qPoi.points.size(); i++)
{
const QuestPOIPoint& point = qPoi.points[i];
dx += point.x * weights[i];
dy += point.y * weights[i];
}
if (bot->GetDistance2d(dx, dy) >= 1500.0f)
continue;
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
continue;
if (bot->GetZoneId() != bot->GetMap()->GetZoneId(bot->GetPhaseMask(), dx, dy, dz))
continue;
poiInfo.push_back({{dx, dy}, qPoi.ObjectiveIndex});
}
if (poiInfo.empty())
return false;
return true;
}
if (q_status.Status != QUEST_STATUS_INCOMPLETE)
return false;
// Get incomplete quest objective index
std::vector<int32> incompleteObjectiveIdx;
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; i++)
{
int32 npcOrGo = quest->RequiredNpcOrGo[i];
if (!npcOrGo)
continue;
if (q_status.CreatureOrGOCount[i] < quest->RequiredNpcOrGoCount[i])
incompleteObjectiveIdx.push_back(i);
}
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++)
{
uint32 itemId = quest->RequiredItemId[i];
if (!itemId)
continue;
if (q_status.ItemCount[i] < quest->RequiredItemCount[i])
incompleteObjectiveIdx.push_back(QUEST_OBJECTIVES_COUNT + i);
}
// Get POIs to go
for (const QuestPOI& qPoi : *poiVector)
{
if (qPoi.MapId != bot->GetMapId())
continue;
bool inComplete = false;
for (uint32 objective : incompleteObjectiveIdx)
{
if (qPoi.ObjectiveIndex == objective)
{
inComplete = true;
break;
}
}
if (!inComplete)
continue;
if (qPoi.points.size() == 0)
continue;
float dx = 0, dy = 0;
std::vector<float> weights = GenerateRandomWeights(qPoi.points.size());
for (size_t i = 0; i < qPoi.points.size(); i++)
{
const QuestPOIPoint& point = qPoi.points[i];
dx += point.x * weights[i];
dy += point.y * weights[i];
}
if (bot->GetDistance2d(dx, dy) >= 1500.0f)
continue;
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
continue;
if (bot->GetZoneId() != bot->GetMap()->GetZoneId(bot->GetPhaseMask(), dx, dy, dz))
continue;
poiInfo.push_back({{dx, dy}, qPoi.ObjectiveIndex});
}
if (poiInfo.size() == 0)
{
// LOG_DEBUG("playerbots", "[New rpg] {}: No available poi can be found for quest {}", bot->GetName(), questId);
return false;
}
return true;
}
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
{
const std::vector<WorldLocation>& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel());
float hiRange = 500.0f;
float loRange = 2500.0f;
if (bot->GetLevel() < 5)
{
hiRange /= 3;
loRange /= 3;
}
std::vector<WorldLocation> lo_prepared_locs, hi_prepared_locs;
bool inCity = false;
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(bot->GetZoneId()))
{
if (zone->flags & AREA_FLAG_CAPITAL)
inCity = true;
}
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
continue;
if (bot->GetExactDist(loc) > 2500.0f)
continue;
if (!inCity && bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(),
loc.GetPositionZ()) != bot->GetZoneId())
continue;
if (bot->GetExactDist(loc) < hiRange)
{
hi_prepared_locs.push_back(loc);
}
if (bot->GetExactDist(loc) < loRange)
{
lo_prepared_locs.push_back(loc);
}
}
WorldPosition dest{};
if (urand(1, 100) <= 50 && !hi_prepared_locs.empty())
{
uint32 idx = urand(0, hi_prepared_locs.size() - 1);
dest = hi_prepared_locs[idx];
}
else if (!lo_prepared_locs.empty())
{
uint32 idx = urand(0, lo_prepared_locs.size() - 1);
dest = lo_prepared_locs[idx];
}
if (!dest.IsValid())
return dest;
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size());
return dest;
}
WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
{
const std::vector<WorldLocation> locs = sTravelMgr.GetTravelHubs(bot);
bool inCity = false;
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(bot->GetZoneId()))
{
if (zone->flags & AREA_FLAG_CAPITAL)
inCity = true;
}
std::vector<WorldLocation> prepared_locs;
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
continue;
float range = bot->GetLevel() <= 5 ? 500.0f : 2500.0f;
if (bot->GetExactDist(loc) > range)
continue;
if (bot->GetExactDist(loc) < 50.0f)
continue;
if (!inCity && bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(),
loc.GetPositionZ()) != bot->GetZoneId())
continue;
prepared_locs.push_back(loc);
}
WorldPosition dest{};
if (!prepared_locs.empty())
{
uint32 idx = urand(0, prepared_locs.size() - 1);
dest = prepared_locs[idx];
}
if (!dest.IsValid())
return dest;
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
prepared_locs.size(), locs.size());
return dest;
}
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path)
{
TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot);
if (!info)
return false;
std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
if (availablePaths.empty())
return false;
flightMasterEntry = info->templateEntry;
flightMasterPos = info->pos;
path = availablePaths[urand(0, availablePaths.size() - 1)];
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)",
bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size());
return true;
}
bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus)
{
std::vector<NewRpgStatus> availableStatus;
uint32 probSum = 0;
for (NewRpgStatus status : candidateStatus)
{
if (sPlayerbotAIConfig.RpgStatusProbWeight[status] == 0)
continue;
if (CheckRpgStatusAvailable(status))
{
availableStatus.push_back(status);
probSum += sPlayerbotAIConfig.RpgStatusProbWeight[status];
}
}
if (availableStatus.empty() || probSum == 0)
{
botAI->rpgInfo.ChangeToRest();
bot->SetStandState(UNIT_STAND_STATE_SIT);
return true;
}
uint32 rand = urand(1, probSum);
uint32 accumulate = 0;
NewRpgStatus chosenStatus = RPG_STATUS_END;
for (NewRpgStatus status : availableStatus)
{
accumulate += sPlayerbotAIConfig.RpgStatusProbWeight[status];
if (accumulate >= rand)
{
chosenStatus = status;
break;
}
}
switch (chosenStatus)
{
case RPG_WANDER_RANDOM:
{
botAI->rpgInfo.ChangeToWanderRandom();
return true;
}
case RPG_WANDER_NPC:
{
botAI->rpgInfo.ChangeToWanderNpc();
return true;
}
case RPG_GO_GRIND:
{
WorldPosition pos = SelectRandomGrindPos(bot);
if (pos != WorldPosition())
{
botAI->rpgInfo.ChangeToGoGrind(pos);
return true;
}
return false;
}
case RPG_GO_CAMP:
{
WorldPosition pos = SelectRandomCampPos(bot);
if (pos != WorldPosition())
{
botAI->rpgInfo.ChangeToGoCamp(pos);
return true;
}
return false;
}
case RPG_DO_QUEST:
{
std::vector<uint32> availableQuests;
for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (botAI->lowPriorityQuest.find(questId) != botAI->lowPriorityQuest.end())
continue;
std::vector<POIInfo> poiInfo;
if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo, true))
{
availableQuests.push_back(questId);
}
}
if (availableQuests.size())
{
uint32 questId = availableQuests[urand(0, availableQuests.size() - 1)];
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest)
{
botAI->rpgInfo.ChangeToDoQuest(questId, quest);
return true;
}
}
return false;
}
case RPG_TRAVEL_FLIGHT:
{
uint32 flightMasterEntry = 0;
WorldPosition flightMasterPos;
std::vector<uint32> path;
if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path))
{
botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path);
return true;
}
return false;
}
case RPG_IDLE:
{
botAI->rpgInfo.ChangeToIdle();
return true;
}
case RPG_REST:
{
botAI->rpgInfo.ChangeToRest();
bot->SetStandState(UNIT_STAND_STATE_SIT);
return true;
}
case RPG_OUTDOOR_PVP:
{
botAI->rpgInfo.ChangeToOutdoorPvp();
return true;
}
default:
{
botAI->rpgInfo.ChangeToRest();
bot->SetStandState(UNIT_STAND_STATE_SIT);
return true;
}
}
return false;
}
bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
{
switch (status)
{
case RPG_IDLE:
case RPG_REST:
return true;
case RPG_WANDER_RANDOM:
{
Unit* target = AI_VALUE(Unit*, "grind target");
return target != nullptr;
}
case RPG_GO_GRIND:
{
WorldPosition pos = SelectRandomGrindPos(bot);
return pos != WorldPosition();
}
case RPG_GO_CAMP:
{
WorldPosition pos = SelectRandomCampPos(bot);
return pos != WorldPosition();
}
case RPG_WANDER_NPC:
{
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
return possibleTargets.size() >= 3;
}
case RPG_DO_QUEST:
{
std::vector<uint32> availableQuests;
for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (botAI->lowPriorityQuest.find(questId) != botAI->lowPriorityQuest.end())
continue;
std::vector<POIInfo> poiInfo;
if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo, true))
{
return true;
}
}
return false;
}
case RPG_TRAVEL_FLIGHT:
{
uint32 flightMasterEntry = 0;
WorldPosition flightMasterPos;
std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path);
}
case RPG_OUTDOOR_PVP:
{
if (!bot->IsPvP())
return false;
uint32 zoneId = bot->GetZoneId();
if (zoneId == AREA_NAGRAND)
return false;
OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId);
return outdoorPvP != nullptr;
}
default:
return false;
}
return false;
}