Implements SpellPvpModifier and SpellAttr16 support for WoW 12.0: PvP Spell Utilities (new PvPSpellUtils.h): - GetPvPMultiplier() accessing SpellEffectEntry::PvpMultiplier from DB2 - ApplyPvPModifier() for PvP damage/healing adjustments - SpellAttr16 infrastructure (all 32 flags currently UNK) - PvP combat detection and spell damage estimation - SpellPvpModifier type classification PvPCombatAI Enhancements: - Enhanced EstimateDPS() with PvP-aware calculations - Added HasBurstCooldownActive() for offensive cooldown detection - Integrated PvPSpellUtils for accurate damage estimation InterruptManager Updates: - Added HasSpellAttr16() and HasAnySpellAttr16() methods - Added GetPvPInterruptMultiplier() for priority calculation - Added IsPvPHighPriorityInterrupt() for target selection Documentation Updates (11.2 → 12.0): - Updated 30+ files with version references - C++ source comments, header documentation - SQL schema version comments and defaults - Configuration files and markdown docs - Preserved statistical data values unchanged Co-Authored-By: Claude Opus 4.5 <[email protected]> Signed-off-by: luis <[email protected]>
718 lines
29 KiB
C++
718 lines
29 KiB
C++
/*
|
|
* Copyright (C) 2024 TrinityCore <https://www.trinitycore.org/>
|
|
*
|
|
* BotMovementUtil - Centralized Movement Deduplication Implementation
|
|
*
|
|
* Uses MotionMaster::MovePoint() with deduplication to prevent movement spam.
|
|
* NOTE: We cannot use MoveSplineInit directly because it's NOT thread-safe -
|
|
* bot AI runs on worker threads but MoveSplineInit manipulates unit->movespline
|
|
* which is accessed by Unit::Update() on the main thread, causing crashes.
|
|
*/
|
|
|
|
#include "BotMovementUtil.h"
|
|
#include "Battleground.h"
|
|
#include "Log.h"
|
|
#include "Map.h"
|
|
#include "PhaseShift.h"
|
|
|
|
namespace Playerbot
|
|
{
|
|
|
|
// ============================================================================
|
|
// Z-VALUE CORRECTION FUNCTIONS
|
|
// ============================================================================
|
|
// CRITICAL: These functions prevent bots from falling through the ground or
|
|
// hovering above terrain. ALL position calculations MUST use these functions
|
|
// before passing positions to movement systems.
|
|
// ============================================================================
|
|
|
|
bool BotMovementUtil::CorrectPositionToGround(Player* bot, Position& pos, float heightOffset)
|
|
{
|
|
// Validate input
|
|
if (!bot)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.movement",
|
|
"CorrectPositionToGround: NULL bot pointer");
|
|
return false;
|
|
}
|
|
|
|
// CRITICAL: Use FindMap() instead of GetMap() to avoid ASSERT crash
|
|
// GetMap() has ASSERT(m_currMap) which crashes if map is null
|
|
// This can happen during loading, logout, or map transitions
|
|
Map* map = bot->FindMap();
|
|
if (!map)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"CorrectPositionToGround: Bot {} has no map, cannot correct Z",
|
|
bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
// Use bot's phase shift for proper visibility/collision
|
|
return CorrectPositionToGroundWithMap(map, bot->GetPhaseShift(), pos, heightOffset);
|
|
}
|
|
|
|
bool BotMovementUtil::CorrectPositionToGroundWithMap(Map* map, PhaseShift const& phaseShift,
|
|
Position& pos, float heightOffset)
|
|
{
|
|
// Validate input
|
|
if (!map)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.movement",
|
|
"CorrectPositionToGroundWithMap: NULL map pointer");
|
|
return false;
|
|
}
|
|
|
|
// Store original Z for logging
|
|
float originalZ = pos.GetPositionZ();
|
|
|
|
// Get ground height at this X/Y position
|
|
// Parameters:
|
|
// - phaseShift: visibility/phase info
|
|
// - x, y: position to check
|
|
// - z: starting height for downward search
|
|
// - checkVMap: true to use vmaps for indoor areas (default)
|
|
// - maxSearchDist: how far down to search (100.0f covers most terrain)
|
|
float groundZ = map->GetHeight(phaseShift,
|
|
pos.GetPositionX(),
|
|
pos.GetPositionY(),
|
|
pos.GetPositionZ() + 10.0f, // Search from slightly above current Z
|
|
true, // Use VMaps
|
|
100.0f); // Max search distance
|
|
|
|
// Check if we got a valid height
|
|
if (groundZ <= INVALID_HEIGHT)
|
|
{
|
|
// No valid ground found - this can happen over water, void, etc.
|
|
// Try searching from higher up in case we're in a deep hole
|
|
groundZ = map->GetHeight(phaseShift,
|
|
pos.GetPositionX(),
|
|
pos.GetPositionY(),
|
|
pos.GetPositionZ() + 50.0f, // Search from much higher
|
|
true,
|
|
200.0f); // Larger search distance
|
|
|
|
if (groundZ <= INVALID_HEIGHT)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"CorrectPositionToGroundWithMap: No valid ground at ({:.1f}, {:.1f}, {:.1f}) - keeping original Z",
|
|
pos.GetPositionX(), pos.GetPositionY(), originalZ);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Apply the corrected Z with height offset
|
|
float newZ = groundZ + heightOffset;
|
|
|
|
// Calculate the correction amount for diagnostics
|
|
float correction = std::abs(newZ - originalZ);
|
|
|
|
// Only log if the correction was significant (> 1 yard)
|
|
if (correction > 1.0f)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"CorrectPositionToGroundWithMap: Z corrected by {:.1f}yd ({:.1f} -> {:.1f}) at ({:.1f}, {:.1f})",
|
|
correction, originalZ, newZ, pos.GetPositionX(), pos.GetPositionY());
|
|
}
|
|
|
|
// Apply the correction
|
|
pos.m_positionZ = newZ;
|
|
|
|
return true;
|
|
}
|
|
|
|
float BotMovementUtil::GetGroundHeight(Player* bot, float x, float y, float z)
|
|
{
|
|
// Validate input
|
|
if (!bot)
|
|
return INVALID_HEIGHT;
|
|
|
|
// Use FindMap() instead of GetMap() to avoid ASSERT crash
|
|
Map* map = bot->FindMap();
|
|
if (!map)
|
|
return INVALID_HEIGHT;
|
|
|
|
// Get height using bot's phase shift
|
|
return map->GetHeight(bot->GetPhaseShift(), x, y, z + 10.0f, true, 100.0f);
|
|
}
|
|
|
|
bool BotMovementUtil::HasValidGround(Player* bot, Position const& pos, float maxHeightDifference)
|
|
{
|
|
// Validate input
|
|
if (!bot)
|
|
return false;
|
|
|
|
// Get ground height at position
|
|
float groundZ = GetGroundHeight(bot, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ());
|
|
|
|
// Check if we got a valid height
|
|
if (groundZ <= INVALID_HEIGHT)
|
|
return false;
|
|
|
|
// Check if the ground is within acceptable range
|
|
float heightDiff = std::abs(pos.GetPositionZ() - groundZ);
|
|
return heightDiff <= maxHeightDifference;
|
|
}
|
|
|
|
// ============================================================================
|
|
// MOVEMENT FUNCTIONS
|
|
// ============================================================================
|
|
|
|
bool BotMovementUtil::MoveToPosition(Player* bot, Position const& destination, uint32 pointId, float minDistanceChange)
|
|
{
|
|
if (!bot)
|
|
return false;
|
|
|
|
// CRITICAL: Bot must be in world before any movement operations
|
|
if (!bot->IsInWorld())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot", "⚠️ BotMovement: Bot {} not in world, skipping movement", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot", "BotMovementUtil::MoveToPosition: Bot {} has NULL MotionMaster", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
// MINE/CAVE FIX: Use 3D distance to properly detect vertical distance
|
|
// Previously used 2D distance which caused bots to think they were "at destination"
|
|
// when standing at mine entrance but spawn is directly below at different Z
|
|
float distToDestination2D = bot->GetExactDist2d(destination.GetPositionX(), destination.GetPositionY());
|
|
float distToDestination3D = bot->GetExactDist(destination);
|
|
float zDifference = std::abs(bot->GetPositionZ() - destination.GetPositionZ());
|
|
|
|
// DIAGNOSTIC: Log movement attempts (DEBUG level to avoid spam)
|
|
TC_LOG_DEBUG("module.playerbot.movement", "🔍 MoveToPosition: Bot {} dist2D={:.1f} dist3D={:.1f} zDiff={:.1f} minDist={:.1f} dest=({:.1f},{:.1f},{:.1f})",
|
|
bot->GetName(), distToDestination2D, distToDestination3D, zDifference, minDistanceChange,
|
|
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ());
|
|
|
|
// MINE/CAVE FIX: Use 3D distance for "already at destination" check
|
|
// This ensures bots will still move if there's significant vertical distance
|
|
// even if horizontal distance is small (e.g., mine entrance above spawn inside)
|
|
if (distToDestination3D < minDistanceChange)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "✅ BotMovement: Bot {} already at destination (3D dist {:.1f}yd < {:.1f})",
|
|
bot->GetName(), distToDestination3D, minDistanceChange);
|
|
return true;
|
|
}
|
|
|
|
// CRITICAL SAFETY CHECK: Re-verify bot is still valid before accessing MotionMaster
|
|
// Threading issue: Bot could have been removed from world between initial check and here
|
|
if (!bot->IsInWorld() || !bot->GetSession())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "⚠️ BotMovement: Bot became invalid during MoveToPosition, aborting");
|
|
return false;
|
|
}
|
|
|
|
// Check if bot is already moving
|
|
MovementGeneratorType currentMoveType = mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE);
|
|
|
|
// ========================================================================
|
|
// CRITICAL FIX: Do NOT interrupt CHASE_MOTION_TYPE!
|
|
// ========================================================================
|
|
// When the bot is chasing a target (combat), calling MovePoint() interrupts
|
|
// the chase and causes stuttering as SoloCombatStrategy immediately re-issues
|
|
// MoveChase(). This creates rapid oscillation between CHASE and POINT motion.
|
|
//
|
|
// During combat, SoloCombatStrategy is the ONLY system that should control
|
|
// movement. If MoveToPosition is called during chase, we should NOT interrupt.
|
|
// ========================================================================
|
|
if (currentMoveType == CHASE_MOTION_TYPE)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "⚠️ BotMovement: Bot {} in CHASE mode - NOT INTERRUPTING chase to move to position (let combat strategy handle movement)",
|
|
bot->GetName());
|
|
return false; // Indicate that we did NOT start movement - chase continues
|
|
}
|
|
|
|
// Similarly, don't interrupt FOLLOW_MOTION_TYPE (following group leader)
|
|
// CRITICAL FIX: EXCEPT in battlegrounds where BG tactics take priority over following!
|
|
// Without this exception, bots in groups just follow the master and ignore BG objectives.
|
|
if (currentMoveType == FOLLOW_MOTION_TYPE)
|
|
{
|
|
// Check if bot is in a battleground - BG tactics should override follow behavior
|
|
if (bot->InBattleground())
|
|
{
|
|
::Battleground* bg = bot->GetBattleground();
|
|
if (bg && bg->GetStatus() == STATUS_IN_PROGRESS)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"🎮 BotMovement: Bot {} in FOLLOW mode BUT in active BG - INTERRUPTING follow for BG tactics",
|
|
bot->GetName());
|
|
// Clear follow motion so BG movement can take over
|
|
mm->Clear(MOTION_SLOT_ACTIVE);
|
|
// Continue to execute the BG movement below
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"⚠️ BotMovement: Bot {} in FOLLOW mode, BG not in progress - NOT INTERRUPTING",
|
|
bot->GetName());
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"⚠️ BotMovement: Bot {} in FOLLOW mode - NOT INTERRUPTING follow to move to position",
|
|
bot->GetName());
|
|
return false; // Indicate that we did NOT start movement - follow continues
|
|
}
|
|
}
|
|
|
|
// If already moving via spline, check if we should interrupt
|
|
if (currentMoveType == POINT_MOTION_TYPE || currentMoveType == EFFECT_MOTION_TYPE)
|
|
{
|
|
// Check if the spline is still active and heading somewhere reasonable
|
|
if (bot->movespline && !bot->movespline->Finalized())
|
|
{
|
|
// BUG FIX: Check if current spline is going to the CORRECT destination!
|
|
// The spline might be going somewhere completely different (e.g., follow target)
|
|
// We need to interrupt and redirect if the destination doesn't match
|
|
G3D::Vector3 const& splineDestination = bot->movespline->FinalDestination();
|
|
|
|
// Skip zero destination check (means spline not properly initialized)
|
|
if (splineDestination != G3D::Vector3::zero())
|
|
{
|
|
float splineDestDist = std::sqrt(
|
|
(splineDestination.x - destination.GetPositionX()) * (splineDestination.x - destination.GetPositionX()) +
|
|
(splineDestination.y - destination.GetPositionY()) * (splineDestination.y - destination.GetPositionY()) +
|
|
(splineDestination.z - destination.GetPositionZ()) * (splineDestination.z - destination.GetPositionZ())
|
|
);
|
|
|
|
// If spline destination is close to requested destination (< 10 yards), let it continue
|
|
if (splineDestDist < 10.0f)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "⏭ BotMovement: Bot {} spline ACTIVE going to correct destination (diff={:.1f}yd) - {:.1f}yd to dest (3D)",
|
|
bot->GetName(), splineDestDist, distToDestination3D);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Spline is going to WRONG destination - must interrupt and redirect!
|
|
TC_LOG_DEBUG("module.playerbot.movement", "🔄 BotMovement: Bot {} spline going WRONG DIRECTION! Spline dest ({:.1f},{:.1f},{:.1f}) is {:.1f}yd from requested ({:.1f},{:.1f},{:.1f}) - INTERRUPTING",
|
|
bot->GetName(),
|
|
splineDestination.x, splineDestination.y, splineDestination.z, splineDestDist,
|
|
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ());
|
|
// Fall through to start new movement
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "⚠️ BotMovement: Bot {} spline has zero destination, will restart",
|
|
bot->GetName());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "⚠️ BotMovement: Bot {} spline FINALIZED or NULL (moveType={}), will restart - {:.1f}yd to destination (3D)",
|
|
bot->GetName(), static_cast<int>(currentMoveType), distToDestination3D);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement", "📍 BotMovement: Bot {} not in POINT/EFFECT motion (moveType={}), will start new movement",
|
|
bot->GetName(), static_cast<int>(currentMoveType));
|
|
}
|
|
|
|
// Start new movement via MotionMaster
|
|
// NOTE: We previously used MoveSplineInit directly for smoother movement, but that caused
|
|
// ASSERTION FAILED: Initialized() crashes in MoveSpline::updateState because MoveSplineInit
|
|
// is NOT thread-safe - it directly manipulates unit->movespline which is also accessed by
|
|
// Unit::Update() on the main thread.
|
|
//
|
|
// MotionMaster::MovePoint() is the safer approach, and we maintain deduplication above
|
|
// by checking if a spline is already active before calling this.
|
|
TC_LOG_DEBUG("module.playerbot.movement", "🚶 BotMovement: Bot {} STARTING MOVEMENT to ({:.2f},{:.2f},{:.2f}) - {:.1f}yd (3D)",
|
|
bot->GetName(), destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(),
|
|
distToDestination3D);
|
|
|
|
// Use MotionMaster for thread-safe movement initiation
|
|
// The deduplication check above prevents the "60+ MovePoint calls/second" bug
|
|
mm->MovePoint(pointId, destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BotMovementUtil::MoveToTarget(Player* bot, WorldObject* target, uint32 pointId, float minDistanceChange)
|
|
{
|
|
if (!bot || !target)
|
|
return false;
|
|
|
|
Position destination;
|
|
destination.Relocate(target->GetPositionX(), target->GetPositionY(), target->GetPositionZ());
|
|
|
|
// CRITICAL FIX: Correct Z to actual ground level
|
|
// Target's Z may be different from ground level at bot's approach position
|
|
// (e.g., target flying, on a different elevation, or Z from database not matching terrain)
|
|
CorrectPositionToGround(bot, destination);
|
|
|
|
return MoveToPosition(bot, destination, pointId, minDistanceChange);
|
|
}
|
|
|
|
bool BotMovementUtil::MoveToUnit(Player* bot, Unit* unit, float distance, uint32 pointId)
|
|
{
|
|
if (!bot || !unit)
|
|
return false;
|
|
|
|
// Check if already within desired distance
|
|
float currentDistance = bot->GetExactDist(unit);
|
|
if (currentDistance <= distance)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot", "✅ BotMovement: Bot {} already within {:.1f}yd of {} (current: {:.1f}yd)",
|
|
bot->GetName(), distance, unit->GetName(), currentDistance);
|
|
return true;
|
|
}
|
|
|
|
// Calculate position at desired distance from unit
|
|
// Use angle from bot to unit for approach vector
|
|
float angle = bot->GetAbsoluteAngle(unit);
|
|
|
|
Position destination;
|
|
// Calculate position that is 'distance' yards from unit, on the line between bot and unit
|
|
// NOTE: We initially use unit's Z, but it will be corrected to ground level below
|
|
destination.Relocate(
|
|
unit->GetPositionX() - distance * std::cos(angle),
|
|
unit->GetPositionY() - distance * std::sin(angle),
|
|
unit->GetPositionZ()
|
|
);
|
|
|
|
// CRITICAL FIX: Correct Z to actual ground level at the calculated position
|
|
// The calculated position may be over terrain that's at a completely different Z level
|
|
// than the unit (e.g., unit on a hill, calculated position is over a cliff)
|
|
// Without this correction, bots can fall through the ground or hover in mid-air
|
|
CorrectPositionToGround(bot, destination);
|
|
|
|
TC_LOG_DEBUG("module.playerbot", "🚶 BotMovement: Bot {} approaching {} to within {:.1f}yd at ({:.2f},{:.2f},{:.2f})",
|
|
bot->GetName(), unit->GetName(), distance,
|
|
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ());
|
|
|
|
return MoveToPosition(bot, destination, pointId);
|
|
}
|
|
|
|
bool BotMovementUtil::ChaseTarget(Player* bot, Unit* target, float distance)
|
|
{
|
|
if (!bot || !target)
|
|
return false;
|
|
|
|
// CRITICAL: Verify bot is still in world (threading safety)
|
|
if (!bot->IsInWorld() || !bot->GetSession())
|
|
return false;
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
return false;
|
|
|
|
// Check if already chasing this target at this distance
|
|
if (mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE) == CHASE_MOTION_TYPE)
|
|
{
|
|
float currentDistance = bot->GetExactDist(target);
|
|
float tolerance = 2.0f;
|
|
|
|
if (std::abs(currentDistance - distance) <= tolerance)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot", "⏭ BotMovement: Bot {} already chasing at optimal distance", bot->GetName());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Start or update chase
|
|
TC_LOG_DEBUG("module.playerbot", "🏃 BotMovement: Bot {} chasing {} at {:.1f}yd",
|
|
bot->GetName(), target->GetName(), distance);
|
|
mm->MoveChase(target, distance);
|
|
return true;
|
|
}
|
|
|
|
void BotMovementUtil::StopMovement(Player* bot)
|
|
{
|
|
if (!bot)
|
|
return;
|
|
|
|
bot->StopMoving();
|
|
|
|
if (MotionMaster* mm = bot->GetMotionMaster())
|
|
mm->Clear();
|
|
|
|
TC_LOG_DEBUG("module.playerbot", "⏹ BotMovement: Bot {} stopped movement", bot->GetName());
|
|
}
|
|
|
|
bool BotMovementUtil::IsMoving(Player* bot)
|
|
{
|
|
if (!bot)
|
|
return false;
|
|
|
|
// CRITICAL: Verify bot is still in world (threading safety)
|
|
if (!bot->IsInWorld())
|
|
return false;
|
|
|
|
// Check spline state first (most accurate)
|
|
if (bot->movespline && !bot->movespline->Finalized())
|
|
return true;
|
|
|
|
// Fall back to MotionMaster check
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
return false;
|
|
|
|
MovementGeneratorType type = mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE);
|
|
return (type == POINT_MOTION_TYPE || type == CHASE_MOTION_TYPE || type == FOLLOW_MOTION_TYPE || type == EFFECT_MOTION_TYPE);
|
|
}
|
|
|
|
bool BotMovementUtil::IsMovingToDestination(Player* bot, Position const& destination, float tolerance)
|
|
{
|
|
if (!bot)
|
|
return false;
|
|
|
|
// Check if spline is active
|
|
if (!bot->movespline || bot->movespline->Finalized())
|
|
return false;
|
|
|
|
// Check if destination is close to where we're heading
|
|
float distToDestination = bot->GetExactDist2d(destination.GetPositionX(), destination.GetPositionY());
|
|
return (distToDestination <= tolerance);
|
|
}
|
|
|
|
// ============================================================================
|
|
// NEW: TrinityCore 12.0 Movement Features - Implementation
|
|
// ============================================================================
|
|
// These methods leverage the new MoveRandom() and MovePath() player support
|
|
// added in TrinityCore 12.0 (commits 12743dd0e7, 1db1a0e57f)
|
|
// ============================================================================
|
|
|
|
bool BotMovementUtil::MoveRandomAround(Player* bot, float wanderDistance,
|
|
Optional<Milliseconds> duration, bool forceWalk)
|
|
{
|
|
if (!bot)
|
|
return false;
|
|
|
|
// CRITICAL: Bot must be in world before any movement operations
|
|
if (!bot->IsInWorld())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveRandomAround: Bot {} not in world, skipping random movement", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
// Use bot's current position as center
|
|
Position centerPos;
|
|
centerPos.Relocate(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
|
|
|
return MoveRandomAroundPosition(bot, centerPos, wanderDistance, duration, forceWalk);
|
|
}
|
|
|
|
bool BotMovementUtil::MoveRandomAroundPosition(Player* bot, Position const& centerPos,
|
|
float wanderDistance, Optional<Milliseconds> duration, bool forceWalk)
|
|
{
|
|
if (!bot)
|
|
return false;
|
|
|
|
// CRITICAL: Bot must be in world before any movement operations
|
|
if (!bot->IsInWorld())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveRandomAroundPosition: Bot {} not in world, skipping random movement", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.movement",
|
|
"MoveRandomAroundPosition: Bot {} has NULL MotionMaster", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
// Validate wander distance
|
|
if (wanderDistance < 1.0f)
|
|
wanderDistance = 1.0f;
|
|
if (wanderDistance > 50.0f)
|
|
wanderDistance = 50.0f; // Reasonable max to prevent extreme wandering
|
|
|
|
// Correct the center position Z to ground level
|
|
Position correctedCenter = centerPos;
|
|
CorrectPositionToGround(bot, correctedCenter);
|
|
|
|
// Determine speed selection mode
|
|
MovementWalkRunSpeedSelectionMode speedMode = forceWalk
|
|
? MovementWalkRunSpeedSelectionMode::ForceWalk
|
|
: MovementWalkRunSpeedSelectionMode::Default;
|
|
|
|
// Check if already doing random movement near this center
|
|
MovementGeneratorType currentMoveType = mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE);
|
|
if (currentMoveType == RANDOM_MOTION_TYPE)
|
|
{
|
|
// Already wandering - check if center is similar
|
|
float distFromCenter = bot->GetExactDist2d(correctedCenter.GetPositionX(), correctedCenter.GetPositionY());
|
|
if (distFromCenter <= wanderDistance)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveRandomAroundPosition: Bot {} already wandering near center ({:.1f}yd away)",
|
|
bot->GetName(), distFromCenter);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Don't interrupt important movement types
|
|
if (currentMoveType == CHASE_MOTION_TYPE ||
|
|
currentMoveType == FOLLOW_MOTION_TYPE ||
|
|
currentMoveType == POINT_MOTION_TYPE)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveRandomAroundPosition: Bot {} has active movement (type={}), not starting wander",
|
|
bot->GetName(), static_cast<int>(currentMoveType));
|
|
return false;
|
|
}
|
|
|
|
// Use TrinityCore 12.0's MoveRandom() for players
|
|
// NEW: This now works for players (previously creature-only)
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveRandomAroundPosition: Bot {} starting random wander (center={:.1f},{:.1f},{:.1f}, radius={:.1f}yd, walk={})",
|
|
bot->GetName(),
|
|
correctedCenter.GetPositionX(), correctedCenter.GetPositionY(), correctedCenter.GetPositionZ(),
|
|
wanderDistance, forceWalk ? "yes" : "no");
|
|
|
|
// First teleport to center if needed (only if far from center)
|
|
float distToCenter = bot->GetExactDist(correctedCenter);
|
|
if (distToCenter > wanderDistance * 2)
|
|
{
|
|
// Too far from center - move there first
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveRandomAroundPosition: Bot {} is {:.1f}yd from center, moving there first",
|
|
bot->GetName(), distToCenter);
|
|
mm->MovePoint(0, correctedCenter.GetPositionX(), correctedCenter.GetPositionY(),
|
|
correctedCenter.GetPositionZ());
|
|
return true;
|
|
}
|
|
|
|
// Start random movement using TrinityCore 12.0 API
|
|
mm->MoveRandom(wanderDistance, duration, Optional<float>{}, speedMode);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BotMovementUtil::MoveAlongPath(Player* bot, uint32 pathId, bool repeatable,
|
|
bool forceWalk, Optional<float> speed)
|
|
{
|
|
if (!bot)
|
|
return false;
|
|
|
|
// CRITICAL: Bot must be in world before any movement operations
|
|
if (!bot->IsInWorld())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveAlongPath: Bot {} not in world, skipping path movement", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
// Path ID 0 is invalid
|
|
if (pathId == 0)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.movement",
|
|
"MoveAlongPath: Bot {} - invalid pathId 0", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.movement",
|
|
"MoveAlongPath: Bot {} has NULL MotionMaster", bot->GetName());
|
|
return false;
|
|
}
|
|
|
|
// Don't interrupt important movement types
|
|
MovementGeneratorType currentMoveType = mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE);
|
|
if (currentMoveType == CHASE_MOTION_TYPE || currentMoveType == FOLLOW_MOTION_TYPE)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveAlongPath: Bot {} has active chase/follow (type={}), not starting path",
|
|
bot->GetName(), static_cast<int>(currentMoveType));
|
|
return false;
|
|
}
|
|
|
|
// Check if already on the same path
|
|
if (currentMoveType == WAYPOINT_MOTION_TYPE)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveAlongPath: Bot {} already on a waypoint path", bot->GetName());
|
|
// Could check if same path ID, but for now just let it continue
|
|
return true;
|
|
}
|
|
|
|
// Determine speed selection mode
|
|
MovementWalkRunSpeedSelectionMode speedMode = forceWalk
|
|
? MovementWalkRunSpeedSelectionMode::ForceWalk
|
|
: MovementWalkRunSpeedSelectionMode::Default;
|
|
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"MoveAlongPath: Bot {} starting path {} (repeatable={}, walk={})",
|
|
bot->GetName(), pathId, repeatable ? "yes" : "no", forceWalk ? "yes" : "no");
|
|
|
|
// Use TrinityCore 12.0's MovePath() for players
|
|
// NEW: This now works for players (previously creature-only)
|
|
mm->MovePath(pathId, repeatable,
|
|
Optional<Milliseconds>{}, // duration
|
|
speed, // speed override
|
|
speedMode, // walk/run mode
|
|
Optional<::std::pair<Milliseconds, Milliseconds>>{}, // wait time at end
|
|
Optional<float>{}, // wander at ends
|
|
Optional<bool>{}, // follow backwards
|
|
Optional<bool>{}, // exact spline
|
|
true); // generate path
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BotMovementUtil::IsWandering(Player* bot)
|
|
{
|
|
if (!bot || !bot->IsInWorld())
|
|
return false;
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
return false;
|
|
|
|
return mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE) == RANDOM_MOTION_TYPE;
|
|
}
|
|
|
|
bool BotMovementUtil::IsFollowingPath(Player* bot)
|
|
{
|
|
if (!bot || !bot->IsInWorld())
|
|
return false;
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
return false;
|
|
|
|
return mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE) == WAYPOINT_MOTION_TYPE;
|
|
}
|
|
|
|
void BotMovementUtil::StopWanderingOrPath(Player* bot)
|
|
{
|
|
if (!bot || !bot->IsInWorld())
|
|
return;
|
|
|
|
MotionMaster* mm = bot->GetMotionMaster();
|
|
if (!mm)
|
|
return;
|
|
|
|
MovementGeneratorType currentMoveType = mm->GetCurrentMovementGeneratorType(MOTION_SLOT_ACTIVE);
|
|
|
|
if (currentMoveType == RANDOM_MOTION_TYPE)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"StopWanderingOrPath: Bot {} stopping random movement", bot->GetName());
|
|
mm->Remove(RANDOM_MOTION_TYPE, MOTION_SLOT_ACTIVE);
|
|
}
|
|
else if (currentMoveType == WAYPOINT_MOTION_TYPE)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.movement",
|
|
"StopWanderingOrPath: Bot {} stopping waypoint path", bot->GetName());
|
|
mm->Remove(WAYPOINT_MOTION_TYPE, MOTION_SLOT_ACTIVE);
|
|
}
|
|
}
|
|
|
|
} // namespace Playerbot
|